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)