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 @@ HatenaUser @@ Twitterというのがそれです。UserにそれぞれHatenaTwitterというタグを付け別の型に見せることで同じ型クラスのインスタンスを別々に供給することができています。 また、型が変わっても実行時の値はそのままであることが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のままの別の型を表します。 newtypeA => Newtype[A, Ops]の変換をしてくれる関数です。

Tagged typeでは動作した以下のコードはNewtypeではコンパイルエラーになります。

HatenaUser("kxbmap") + TwitterUser("kxbmap")

他のStringのメソッドも呼び出せません。やったね。

Scalaz + shapeless

NewtypeOpsをただのマーカートレイトにして、例えば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)

*1:eqJavaでいう==で参照の比較です