Scalaの開発環境について

いつもは IntelliJ IDEA + Scala プラグインで Scala の開発をしていますが、ファイルによってすごくもっさりするので、他の開発環境はどうだろうかと試してみました。

Read more »

Scala 2.10 からの型クラス2(Functorはどうする?)

前回の記事にコメントをいただいて、Functor とか Monad なんかの高階型はどうするのか改めて考えてみました。

結論から言うと Scala は関数リテラルで型パラメータとれないので、関数リテラルだけでなんとかしようというのは不可能っぽいです(いまんところの私の頭では)。

しかし、implicity 使ったり context-bound を使わなくてもなんとかなりそうです。ここまで来ると型クラスじゃねーだろっ。と言われるかもしれませんがアドホック多相にはなってると思います。

ということで、Functor をやってみます。Haskell だとこんな感じですね。

class Functor f where
    fmap :: (a -> b) -> f a -> f b

Scala だと

import language.higherKinds
 
trait Functor[A] {
  type F[_]
  def fmap[B](func: A => B): F[B]
}
trait FunctorFunctions {
  def fmap[A, B](func: A => B)(f: Functor[A]): m.F[B] = f.fmap(func)
}

Option 型に適用してみます。

trait FunctorInstance {
  implicit class OptionFunctor[A](option: Option[A]) extends Functor[A] {
    type F[_] = Option[_]
    def fmap[B](func: A => B): Option[B] = option.map(func)
  }
}

使い方。

val allInstances = new FunctorInstance with FunctorFunctions {}
import allInstances._
 
val maybeInt = Option(3)
val func: Int => Int = _ * 2
 
println(maybeInt.fmap(func))
println(fmap(func)(maybeInt))
 
// こんな風にも使えます
def doubleInt(f: Functor[Int]): f.F[Int] = f.fmap(func)
 
println(doubleInt(maybeInt))

def は撲滅できませんでしたが、context-bound が撲滅できるのが私個人としては嬉しいです。それにやっぱりコード量はこっちのほうが少ないし!

でも、この fmap、カリー化を用いた部分適用(という言い方でいい?)できない! ぎゃふん!

Scala 2.10 からの型クラス

Scala で型クラス使おうという人ならば一度は目を通したことがあるだろう「空飛ぶサンドイッチのパーツ」。

Scala での型クラスの実装方法といったらたいていこの記事に書いてあるような方式になるのではないでしょうか(というか、私がググった限りではこのパターンしか見つけられなかった)。

でもこの方式、context-bound 型パラメータ使うんですよね。これのせいで def が登場しちゃうし、カリー化ができない。空飛ぶサンドイッチのパーツ自身、関数リテラル使おうぜ! と言いながらこのせいで def 使っちゃう。悔しい。それから、もっと便利にしようということで Scalaz みたいに Ops とか入れちゃって、型クラス実現するのにめんどうなこと!

ということで、Scala 2.10 から入った implicit classes 使うと型クラス簡単ですぜっていうのが今回の話です。空飛ぶサンドイッチのパーツの Depth をやってみます。

まず型クラス。ジェネリクスはいりません。

trait Depth {
  val: depth: Int
}

// お好みで関数バージョンも作りましょう
trait DepthFunctions {
  val depth: Depth => Int = _.depth
}

そして型インスタンス。

trait DepthInstances {
  implicit class TreeDepth(tree: Tree) extends Depth {
    val depth: Int = tree match {
      case Empty()    => 0
      case Leaf(_)    => 1
      case Node(l, r) => 1 + math.max(l.depth, r.depth)
    }
  }
  implicit class ListDepth(list: List[Int]) extends Depth {
    val depth: Int = list match {
      case xs => xs.size
    }
  }
}

使い方。空飛ぶサンドイッチのパーツでは、def になっていた halfDepth もこれで晴れて関数リテラルに!

val allInstances = new DepthInstances 
  with DepthFunctions {}
import allInstances._
val halfDepth: Depth => Int = _.depth / 2
// ↓ 関数バージョンを使えばこんな書き方もできます。
// val halfDepth: Depth => Int = d => depth(d) / 2
halfDepth(List(1, 2, 3, 4))

簡単! やった!

Xcode 5.0 で不調になった Haskell 環境をなんとかする

iOS 7 のリリースに合わせて Xcode 5.0 がリリースになり、思わずアップデートしてしまった人はきっと私だけじゃないはず。しかし、これによって私の Haskell 環境は調子が悪くなってしまいました。特に Haskell コード中のシングルクォートの扱いがおかしいケースが目立ちました。

なお、私の Haskell 環境は Homebrew でインストールしたものです。

GHC に使わせる gcc を変更する

問題の原因は、Xcode 5.0 付属の gcc のようです。そこで、gcc のバージョン 4.7 を別途インストールします。

brew tap homebrew/versions
brew install gcc47

GHC の設定を変更するため /usr/local/Cellar/ghc/7.6.3/lib/ghc-7.6.3/settings を修正します。

2
 ("C compiler command", "/usr/bin/gcc"),

↑これを↓このように修正します。

2
 ("C compiler command", "/usr/local/bin/gcc-4.7"),

これでだいたい良さそうでしたが、これだけでは Yesod が依存している entropy のコンパイルが通りませんでした。どうも単純にパス上の gcc を見ているくさいのです。ですので、Homebrew を使っているのでしたら、/usr/bin よりも /usr/local/bin を優先してパスを通しているかと思いますので、ここにシンボリックリンクを作成してしのぎましょう。

ln -s /usr/local/bin/gcc-4.7 /usr/local/bin/gcc

ただしこれをやると、ほとんど Xcode の gcc をパス上から隠蔽することになるわけですから、その点十分に注意してください(コマンドラインで Xcode に依存するような開発をする場合など)。

それから、haskell-platform も念の為に再インストールしたほうがいいかもしれません。

brew uninstall haskell-platform
brew install haskell-platform

Yesod-Auth の HashDB を使うサンプルできたよ

日々のすきをみてドン亀のごときペースで Haskell の勉強をしているのですが、入門書片手に学ぶのも飽きてきたので、実際になんか作ってみようとしています。

そこで Haskell の Web  アプリケーションフレームワーク「Yesod」に挑戦しています。

Yesod はデフォルト状態(プロジェクトを作成直後の状態)ですでにログイン認証の仕組みが使えるようになっているのですが、この認証方式が Google の仕組みを使う一種のシングルサインオン方式になっているのです。これはこれで技術的に興味深いのですが、しかしじゃあ実際にどれぐらい使うんだというと、実案件ではそうそう使うものではないと思います。実際によく使う方式といえばやはり DB にユーザ名とパスワードを保存しておき、入力値がそれにマッチするかを見る方式が多いのではないでしょうか?

Yesod には標準で Yesod-Auth という認証の仕組みがあって、このサブパッケージによって前述の Google を使った認証方法や BrowserID を使った認証方法など様々な認証方法を提供しています。そして当然 DB を使った認証方法も提供しています。それが Yesod.Auth.HashDB パッケージ(以下 HashDB)です。

しかし、これを使うためのドキュメントでわかりやすいのがなかなか見つからないのです! 英文のリーディング自体がたどたどしい Haskell と Yesod の初学者にはイバラの道以外の何物でもないような状態だと私は感じました。しかしなんとか最低限何をすればいいのかまとめることができましたので紹介します。

なお、今回使用した Yesod のバージョンは 1.2.3.3 です。この記事に関するソースコードは Github で公開しています。diff を見てみるとどこに手を付けたのかわかりやすいかもしれません。

手を付けるべきファイルは3つ

そもそもデフォルトの状態から最低どのファイルに手をいれるべきかもなかなか定かにならなかったのですが、以下の3つが修正対象です。

  • config/models
  • Models.hs
  • Foundation.hs

config/models

まず、デフォルトでもユーザ情報を格納するためのテーブル定義を行っているのですが、HashDB で使用するには若干手直ししなければなりません。ポイントは以下の点です。

  • パスワードは必須項目でなければなりません。
  • salt カラムが必要です。

デフォルトでは password カラムに Maybe がついているのでパスワードが必須項目になっていません。また、HashDB ではパスワードは SHA-1 でハッシュ化して扱います。その際に salt の値を使います。具体的な例をあげると、パスワードが “secret”、salt が “shio” だとすると “shiosecret” をハッシュ化した値を使用するわけです。この salt を保存するカラムが必要になります。

以上を踏まえると、config/models の User 定義は以下のようになります。

1
2
3
4
5
6
User
    ident Text
    password Text
    salt Text
    UniqueUser ident
    deriving Typeable

Model.hs

config/models の定義によってモデルが User 型が定義されますが、HashDB で使用するには HashDBUser 型クラス制約を満たさなければなりません。それのための修正を Model.hs で行います。まず、HashDB を Import します。

5
import Yesod.Auth.HashDB (HashDBUser(..))

次に型インスタンスの作成です。

17
18
19
20
21
22
instance HashDBUser User where
  userPasswordHash = Just . userPassword
  userPasswordSalt = Just . userSalt
  setSaltAndPasswordHash s h p = p { userSalt = s
                                  , userPassword = h
                                  }

Foundation.hs

最後の Foundation.hs で認証方式を設定します。まず、BrowserID 認証方式と GoogleEmail 認証方式を使用しないので、以下の Import は削除してかまいません。

7
8
import Yesod.Auth.BrowserId
import Yesod.Auth.GoogleEmail

かわりに HashDB のための Import を追加します。

7
import Yesod.Auth.HashDB (authHashDB, getAuthIdHashDB)

getAuthId と authPlugins を修正します。

124
125
126
127
    getAuthId = getAuthIdHashDB AuthR (Just . UniqueUser)
 
    -- You can add other plugins like BrowserID, email or OAuth here
    authPlugins _ = [authHashDB (Just . UniqueUser)]

以上で完了です。

その他、使い方等は Github にあげている readme.md を参照してください。