ScalazのTagged typeとshapelessのNewtype
Scalaz Advent Calendarの二日目です。
Tagged type
ある型クラスのインスタンスが同じ型に対して複数定義されている場合、importによって使用するインスタンスを制御することができます。
object s1 extends App { type User = String object Hatena { import scalaz.Show implicit val HatenaUserShow: Show[User] = Show.shows("id:" + _) } object Twitter { import scalaz.Show implicit val TwitterUserShow: Show[User] = Show.shows("@" + _) } locally { import scalaz.syntax.show._ import Hatena._ "kxbmap".println //> id:kxbmap } locally { import scalaz.syntax.show._ import Twitter._ "kxbmap".println //> @kxbmap } }
しかし、この方法では同一スコープ内で複数のインスタンスを利用することはできません。
そこでScalazではTagged typeというものが導入されています。
これはある型を別の型として(type User = String
のようにただ別名をつけるだけではなく)コンパイラに見せるものです。
object s2 extends App { import scalaz._, Scalaz._ type User = String sealed trait Hatena sealed trait Twitter def HatenaUser(id: User): User @@ Hatena = Tag(id) def TwitterUser(id: User): User @@ Twitter = Tag(id) implicit val HatenaUserShow: Show[User @@ Hatena] = Show.shows("id:" + _) implicit val TwitterUserShow: Show[User @@ Twitter] = Show.shows("@" + _) HatenaUser("kxbmap").println //> id:kxbmap TwitterUser("kxbmap").println //> @kxbmap val s = "kxbmap" assert(s eq HatenaUser(s)) assert(s eq TwitterUser(s)) }
User @@ Hatena
、User @@ Twitter
というのがそれです。User
にそれぞれHatena
、Twitter
というタグを付け別の型に見せることで同じ型クラスのインスタンスを別々に供給することができています。
また、型が変わっても実行時の値はそのままであることがassertで確認できます。*1
しかし、Tagged typeは元の型に対する操作をそのまま受け付けるので次のようなことができてしまいます。
(HatenaUser("kxbmap") + TwitterUser("kxbmap")).println //> "kxbmapkxbmap"
今回の使用例ではあまり嬉しくない動作です。
Newtype
shapelessというライブラリには、この問題を解決するNewtype
というものがあります。
元々Tagged typeもshapeless作者のMiles Sabinさんが考案したものです。(のでshapelessにもTagged typeは定義されています)
object s3 extends App { import shapeless.TypeOperators._ type User = String trait Printable extends Any { def shows: String def println: Unit = Console.println(shows) } implicit class HatenaOps(val id: User) extends AnyVal with Printable { def shows: String = "id:" + id } implicit class TwitterOps(val id: User) extends AnyVal with Printable { def shows: String = "@" + id } def HatenaUser(id: User): Newtype[User, HatenaOps] = newtype[User, HatenaOps](id) def TwitterUser(id: User): Newtype[User, TwitterOps] = newtype[User, TwitterOps](id) HatenaUser("kxbmap").println //> id:kxbmap TwitterUser("kxbmap").println //> @kxbmap val s = "kxbmap" assert(s eq HatenaUser(s)) assert(s eq TwitterUser(s)) }
型クラス関係なくなっちゃいましたが。
Newtype[A, Ops]
で、Ops
に定義されたメソッドを持ち、実体はA
のままの別の型を表します。
newtype
はA => Newtype[A, Ops]
の変換をしてくれる関数です。
Tagged typeでは動作した以下のコードはNewtypeではコンパイルエラーになります。
HatenaUser("kxbmap") + TwitterUser("kxbmap")
他のStringのメソッドも呼び出せません。やったね。
Scalaz + shapeless
NewtypeのOpsをただのマーカートレイトにして、例えばNewtype[User, Hatena]
に対してScalazの型クラスのインスタンスを定義するという方法もありだと思います。
その場合はimportするものに気をつけないとTagged typeなどは衝突してしまうので注意が必要です。
build.sbt
今回使用したbuid.sbtです。
scalaVersion := "2.10.0-RC3" libraryDependencies ++= Seq( "org.scalaz" % "scalaz-core" % "7.0.0-M5", "com.chuusai" % "shapeless" % "1.2.3" ) map (_ cross CrossVersion.full)
Squirrelの関数を持ち上げる
前回の続きです。
Squirrelの関数をcurry化してどうすんのって話ですが、関数を持ち上げるのに使ってみます。
というよりそれをやりたくて必要になったのでcurryを書いたのでした。
関数を持ち上げるとは?普通の関数を文脈の中で作用する関数に変換することです。
例えば、前に書いたOption
は、成功(Some
)かもしれないし失敗(None
)かもしれない、という文脈を表します。
その中で作用する関数とはどういうものでしょうか。実際書いてみます。
function add(a, b) { return a + b; } local r5 = add(1, 2); // r5 = 3 addM <- liftM(add); local r6 = addM(Some(1), Some(2)); // r6 = Some(3) local r7 = addM(Some(1), None); // r7 = None
add
は普通の関数ですが、liftM
関数によって文脈を持った値に適用できるaddM
に持ち上げられています。
そこにOption
の値を渡すと、両方成功の場合は成功の結果が返り、失敗が含まれると結果も失敗となります。
型を見てみると、add
の型は(Scala風に書くと) (A, B) => C
、addM
は (M[A], M[B]) => M[C]
ですね。
よってここで欲しい関数であるliftM
の型は ((A, B) => C) => (M[A], M[B]) => M[C]
となります。
このような関数を定義できればいい訳です。
また文脈といっても色々ある訳ですが、ここではmap
とflatMap
が適切に実装されているもののこととします。
適切な実装とかの詳しいことは略です。
さて、map
とflatMap
があればこう書けますね。2引数関数用なので名前はliftM2
とします。
// ((A, B) => C) => (M[A], M[B]) => M[C] function liftM2(f) { local cf = curry2(f); return function (ma, mb) : (cf) { return ma.map(cf) .flatMap(mb.map.bindenv(mb)); } }
ここで前回作ったcurry2
が出てきます。文脈の中の値に1個ずつ関数を適用していきたいので、そのためには関数がカリー化されている必要がある訳です。
最初からliftM2
にカリー化された関数(A => B => C
)を渡すことにしてもいいんですが、普通にSquirrelを書く上では普通の関数の方が書きやすいのでカリー化は内部でやることにしました。
さて、liftM2
はカリー化した関数cf
をmap/flatMap
を使って順にma
,mb
に適用する関数を返します。この部分です。
function (ma, mb) : (cf) { return ma.map(cf) .flatMap(mb.map.bindenv(mb)); }
最初のadd
の例を使ってこの関数がちゃんと期待した型になっているか見てみます。
まずadd
がcurry2
でカリー化されているのでcf
は次のようになっているイメージです。
// 型はA => B => C local cf = function (a) { function (b) : (a) { return a + b; } }
そしてliftM2
が返す関数の中では、まずma
の中身にmap
でcf
を適用します。
先の例ではma
にSome(1)
を渡しているので、そう置き換えていくと
return ma.map(cf) .flatMap(mb.map.bindenv(mb)); // ↓ return Some(1).map(function (a) { /* 略 */ }) .flatMap(mb.map.bindenv(mb)); // ↓ return Some(function (b) { return 1 + b; }) .flatMap(mb.map.bindenv(mb));
と、こうなります。map
の結果、Some
の中身がcf
を部分適用した関数になっているのが分かると思います。
型で言えば Option[A]
を A => B => C
でmapしたので、Option[B => C]
になっています。
そしてflatMap
で中身の関数にmb.map
を適用します。ここでmb
はSome(2)
です。
// ↓ return mb.map(function (b) { return 1 + b; }); // ↓ return Some(2).map(function (b) { return 1 + b; }); // ↓ return Some(1 + 2);
はい。Some(1)
、Some(2)
を渡してSome(3)
が得られました。
確かにこの関数は (M[A], M[B]) => M[C]
という型になっているようです。M
は Option
、A,B,C
は全部 integer
ですね。例が悪かったですね。
ともあれ、引数2つのliftM2
は出来たので次は引数3つを書きましょう。
// ((A, B, C) => D) => (M[A], M[B], M[C]) => M[D] function liftM3(f) { local cf = curry3(f); return function (ma, mb, mc) : (cf) { return ma.map(cf) .flatMap(mb.map.bindenv(mb)) .flatMap(mc.map.bindenv(mc)); } }
引数を増やしてcurry3
を使ってflatMap
を一段追加して……
liftM4
も引数を増やしてcurry4
を使ってflatMap
を一段追加して……
これは抽象化できそうです。
まず文脈内の関数を文脈内の値に適用するap
関数を用意します。
// (M[A => B], M[A]) => M[B] local ap = function (mf, m) { return mf.flatMap(m.map.bindenv(m)); }
カリー化関数は引数の数をキーにしてテーブルにまとめておきます。
local curryN = { [1] = function (f) { return f; } }; local rt = getroottable(); for (local i = 2; "curry" + i in rt; ++i) { curryN[i] <- rt["curry" + i]; }
引数1つの関数というのは既にカリー化されていると言えるので、curry[1]には恒等関数を入れておきます。
すると、liftM
を以下のように定義できます。
function liftM(f) : (ap, curryN) { return function (...) : (f, ap, curryN) { local r = vargv[0].map(curryN[vargc](f)); for (local i = 1; i < vargc; ++i) { r = ap(r, vargv[i]); } return r; } }
liftM
は関数f
を引数にとり、そのまま可変長引数を取る関数を返します。
その関数は呼び出し時の引数の数に合わせてf
をカリー化、map
/ap
を使って文脈内の値に関数を適用していき最終的な値を返します。
使ってみます。
function product(a, ...) { local r = a; for (local i = 0; i < vargc; ++i) { r *= vargv[i]; } return r; } productM <- liftM(product); local r8 = productM(Some(2), Some(5), Some(9)); // r8 = Some(90) local r9 = productM(Some(4), None); // r9 = None local r10 = productM(Some(1)); // r10 = Some(1) productM(Some(1), Some(2), Some(3), Some(4), Some(5)); // Error! curry5がない
元が可変長引数を取る関数のproductM
は、curryN
が定義されている範囲内で同様に可変長引数を取る関数として扱えます。
またOption
以外でもmap
/flatMap
が定義されていればよいので、例えばarray
をラップするList
クラスを定義すればそのまま次のような計算に使えます。
local r11 = productM(List([2, 3]), List([4, 5, 6])); // r11 = List([8, 10, 12, 12, 15, 18]) local r12 = productM(List([2, 3]), List([]), List([4, 5, 6])); // r12 = List([])
非常に便利、なのですが…… 例によって使う側で型を合わせなければ実行時エラーになるのはどうしようもないです。
Squirrelでカリー化
カリー化のオレオレ定義がまたひとつ / javascript で遊ぶラムダ式、クロージャ、カリー化 by rti 7743 on Prezi prezi.com/9brwewgcxtr2/j…
— kxbmapさん (@kxbmap) 9月 27, 2012
これの資料自体は結構古かったですね。部分適用の特殊な場合をカリー化としている例は今まで見たことなかったのでつい反射的に。カリー化そのものについては以下のリンクを参照してください。丸投げです。
カリー化の誤用についてはこのマサカリをどぞー > togetter.com/li/183700 d.hatena.ne.jp/kazu-yamamoto/… / “javascript で遊ぶラムダ式、クロージャ、カリー化 by rti 7743 on Pr…” htn.to/a5yGTU
— 這い寄る債務者ゆるよろ(旧支配者)さん (@yuroyoro) 9月 27, 2012
ではSquirrel 2.xで関数をカリー化する関数を書いてみます。
function curry2(f) { return function (a) : (f) { return function (b) : (f, a) { return f(a, b); }} } function curry3(f) { return function (a) : (f) { return function (b) : (f, a) { return function (c) : (f, a, b) { return f(a, b, c); }}} } function curry4(f) { return function (a) : (f) { return function (b) : (f, a) { return function (c) : (f, a, b) { return function (d) : (f, a, b, c) { return f(a, b, c, d); }}}} }
はい。*1
関数fの仮引数の数を判別する手段がないので必要な分だけベタ書きでございます。可変長引数もあるししょうがない(?)です。
Squirrel3やJavaScriptと違って、外側のスコープのローカル変数の値を使うには関数定義時に明示的に渡しておく必要があります。
見た目愉快ですね。
curry2
には引数を2つ取る関数を渡すことができます。
function add(a, b) { return a + b; } curried_add <- curry2(add); local r0 = curried_add(2)(3); // r0 = 5 // 部分適用 add1 <- curried_add(1); local r1 = add1(5); // r1 = 6
もしくは、その数の引数を取ることのできる可変長引数を取る関数をカリー化(?)することもできます。
// 1つ以上の引数を取る関数を function product(a, ...) { local r = a; for (local i = 0; i < vargc; ++i) { r *= vargv[i]; } return r; } // 引数を3つ取る関数としてカリー化 curried_product <- curry3(product); local r2 = curried_product(2)(3)(4); // r2 = 24
この時、curried_product
の引数の数は固定化されています。
元の関数から変わってしまうのでカリー化と言えるのかは分かりません。
curried_product(2)(3)(4)(5); // => Error! 24(integer)に対するcall
また、カリー化された関数の引数の順番を入れ替えるflip
関数を以下のように定義できます。
function flip(f) { return function (a) : (f) { return function (b) : (f, a) { return f(b)(a); }} }
function join(a, b, c) { return a + "," + b + "," + c; } curried_join <- curry3(join); local r3 = curried_join(1)(2)(3); // r3 = "1,2,3" flipped_join <- flip(curried_join); local r4 = flipped_join(1)(2)(3); // r4 = "2,1,3"
Squirrelでカリー化とかして何が嬉しいのか、というのはまた次に。
IntelliJ IDEA Scalaプラグインのimplicit関連機能
IntelliJのScalaプラグインでよく使うimplicit関係の機能について。
code completion
暗黙型変換によって使えるようになるメソッドも補完されます。また、それらには名前に下線が引かれます。
あるデータに対して何かやりたいとき、とりあえずimport scalaz._, Scalaz._
して補完候補からそれらしい名前と型のメソッド探す、ということを自分は結構やります。Scalazなんてそんな感じで使えばいいと思うのです。
implicit conversion method
Ctrl+Shift+Q でカーソル位置のオブジェクトを引数に取って呼び出すことのできるimplicitメソッドの一覧を表示します。メソッドを選択してEnterで定義元にジャンプします。
また、実際にその場所で暗黙型変換が行われている場合、使用されているメソッドが太字になります。ここではpureが定義されているのはmkIdentityによって変換されるIdentityだということが分かります。
actual implicit parameters
Ctrl+Shift+P ではメソッドが取る暗黙的な引数について、実際に何が渡されているのかが表示されます。ここでもEnterでジャンプします。
def pure[F[_]](implicit p: Pure[F]): F[A] = p pure value
pureの定義はこうなっています。1.pure[List]
という呼び出しでは、この implicit p には TraversablePure(の戻り値)が渡されているということです。
残念ながらTraversablePureに渡されるimplicit parameterを更に表示、ということは今のところできないようです。
util-evalでゲームの設定を書く
Scala Advent Calendar jp 2011 - [PARTAKE]の8日目です。
Scalaを利用している企業というとTwitterが有名ですね。そのTwitterは開発したScalaライブラリを色々と公開しています。
util-evalはその内のひとつで、実行時にScalaコードをコンパイル&評価するライブラリです。主に設定ファイルをScalaで記述するために利用されます。
で、ゲームの設定もScalaで書いたら(自分が)幸せになれるので使ってみました。
設定の定義
ゲームの設定として、IDが割り振られた武器のMapがあるとします。
武器には名前と攻撃力が設定できます。
case class Weapon(name: String, attack: Int) trait GameConfig { def weapons: Map[Int, Weapon] }
武器設定ファイル
1つの設定ファイルに1つの武器を書くとします。
1 -> Weapon( name = "ナイフ", attack = 10 )
(key, value) のタプルです。
読み込み時にディレクトリを指定するとその下のファイルを全部拾ってきてMapを構築します。
また、1ファイルにMap全体を書くこともできて、その場合は読み込み時にファイルを指定することにします。
設定フォルダはこんな構成。
config ├weapons │ ├axe.scala │ ├bow.scala │ └knife.scala └config.scala
weapons以下のファイルには先程のように武器が1つずつ定義されています。
config.scalaは後述。
Eval!
そんな感じで設定を読み込んで評価するtraitです。
util-evalがscala-compilerに依存していて楽だったのでnsc.ioを使っていますが、ホントは別のものを使った方がいいでしょう。
import com.twitter.util.Eval import scala.tools.nsc.io.{File, Path} trait ConfigReader { private val eval = new Eval() def read[A](file: File): A = { val in = file.inputStream() try eval[A](in) finally in.close() } def readMap[A, B](path: Path): Map[A, B] = if (path.isFile) read[Map[A, B]](path.toFile) else readDirectoryMap(path) private def readDirectoryMap[A, B](dir: Path): Map[A, B] = dir.walkFilter(_.isFile) .map(_.toFile) .map(read[(A, B)]) .foldLeft(Map.empty[A, B])(_ + _) }
readメソッド内の eval[A](in)
が実際にファイルを読んで評価している部分になります。
内部ではファイルの内容を Function0[Any]
を継承したクラスのapplyで包んでコンパイル、そのクラスのインスタンスを生成してapplyを呼び、結果を型キャストしています。*1
要するに、設定ファイル内ではメソッド内で書けるものが全て書けて*2、ファイル末尾に書いた値が帰ってくるということです。
readMap[A,B](Path)
は引数にファイルが渡されたらそのままread
を、ディレクトリならPath#walkFilter
を利用してディレクトリ内のファイルを再帰的に評価し、Mapにまとめます。
読み込み
ゲームからはconfig/config.scalaを読んでGameConfigを取得します。
config.scalaの中身はこんな感じ。
import scala.tools.nsc.io.Path (path: Path) => new GameConfig with ConfigReader { val weapons = readMap[Int, Weapon](path / "weapons") }
これを以下のようにして読み込み。
import scala.tools.nsc.io.Path object Main extends App with ConfigReader { val path = Path("config") val config = read[Path => GameConfig](path / "config.scala" toFile)(path) println(config.weapons) // Map(1 -> Weapon(ナイフ,10), 2 -> Weapon(弓,20), 3 -> Weapon(斧,30)) }
設定ファイルに関数を記述できるのがポイントですね。
武器以外の設定が必要になったらGameConfigに追加していくわけです。
ローカライズ
さて、作ったゲームを沢山の国の人に遊んでもらうにはローカライズが必要です。
ConfigReaderに以下のようなメソッドを追加します。
def localize[A, B](base: Map[A, B], path: Path): Map[A, B] = { val loc = readMap[A, B => B](path) for ((id, config) <- base) yield id -> (loc.get(id).map(_(config)) getOrElse config) }
型Bを変換する関数を値に持つマップを読み込み、IDが一致する設定に対して適用します。
config/localize.scalaとして以下の内容のファイルを作成します。
import scala.tools.nsc.io.Path (base: GameConfig, path: Path) => new GameConfig with ConfigReader { val weapons = localize(base.weapons, path / "weapons.scala") }
weapons.scalaはこんな内容です。
Map[Int, Weapon => Weapon]( 1 -> (_.copy(name = "knife", attack = 15)), 2 -> (_.copy(name = "bow")), 3 -> (_.copy(name = "axe")) )
case classのcopyメソッドで名前だけ変更することができます。
ナイフは弱かったので攻撃力も上げてみました。
読み込みはこうなります。
object Main extends App with ConfigReader { /* 省略 */ val localized = read[(GameConfig, Path) => GameConfig](path / "localize.scala" toFile)(config, path / "localize" / "en") println(localized.weapons) // Map(1 -> Weapon(knife,15), 2 -> Weapon(bow,20), 3 -> Weapon(axe,30)) }
ローカライズできました!
嬉しいこと
設定ファイルがScalaで何が嬉しいか。
Scalaそのものを書けることで自由度が高いというのはもちろんですが、IDEの支援をフルに受けることができるのが大きいでしょう。
ちょっと例を挙げてみます。
おまけ
Scalaz*3を使ってreadMapを以下のように書き換えます。
def readMap[A, B: Semigroup](path: Path): Map[A, B] = /* 省略 */ private def readDirectoryMap[A, B: Semigroup](dir: Path): Map[A, B] = { implicit val r = Reducer[(A, B), Map[A, B]](Map(_)) dir.walkFilter(_.isFile).toStream .map(_.toFile) .map(readFile[(A, B)]) .foldReduce }
そしてWeaponのコンパニオンオブジェクトを定義。
object Weapon { implicit val WeaponSemigroup: Semigroup[Weapon] = semigroup((w1, w2) => throw new IllegalArgumentException("duplicate key: " + w1 + ", " + w2)) }
元のコードではweaponsのkeyが被っていた場合後から読んだもので上書きされていましたが、これによって被ってたらエラーを吐くようになります。他にも先に読んだものを優先したり、どうにかして合成する、という定義もできますね。
さらにはこんな感じ
object Weapon extends ConfigReader { implicit lazy val WeaponSemigroup: Semigroup[Weapon] = read("typeclasses" / "WeaponSemigroup.scala" toFile) }
色々楽しいことができそうな予感がします。
IntelliJ IDEAのプラグイン開発
ScalaでIntelliJのプラグインを作ろうと思ったのでメモついでに。
参考
http://confluence.jetbrains.net/display/IDEADEV/PluginDevelopment
http://www.jetbrains.com/idea/training/demos/google_search.html
下のLive Demoと同じものを作るところまでやってみます。
環境設定
IntelliJのJDKとScalaライブラリの設定、Scalaプラグインのインストールは済んでいるとします。
IntelliJ IDEA Community Editionをインストール
次の設定に使うもので、開発はUltimate Editionでするとしてもこちらも入れる必要がありそうです。*1
今回はちょっと試してみたかったのでIntelliJ IDEA 11(Nika)のEAP(記事時点でbuild110.187)をここから取ってきてインストール。
ソースも一緒にダウンロードするか、リポジトリから取ってきます。
git clone git://github.com/JetBrains/intellij-community.git git checkout idea/110.187
IntelliJ IDEA Plugin SDKの設定
開発に使うIntelliJを起動します。自分はNika Ultimateを別にインストールして使いましたが、先にインストールしたものでもいいはずです。
File > Project Structure > Platform Settings > SDKs から「+」ボタンで「IntelliJ IDEA Plugin SDK」を追加。
SDKのホームを求められるのでCommunity Editionのパスを指定します。次にJDKも選択。
追加されたSDKのSourcepathにダウンロードしたソースのディレクトリを追加します。
プラグイン作成
最初に貼ったデモ内で作成しているプラグインを作ります。エディタのカーソル位置にある単語をブラウザ起動してGoogle検索するものです。
META-INF/plugin.xml
プロジェクト作成時にplugin.xmlというファイルが生成されています。プラグインの情報などはこのファイルに書いていくようです。
各要素を設定していきます。プラグインの名前、説明、バージョン、ベンダーと。
idea-versionはsince-buildやuntil-build属性にプラグインの動作するIntelliJのビルド番号を指定します。互換性についてはよく調べてないですが*5、とりあえずsince-buildにSDKに利用しているIntelliJのビルド番号を入れておきます。