Scalaの `:+` と `+:` にまつわる話

これはClassi Advent Calendar 2018の3日目の記事です。

Classiでいろいろやってるしんぺいです。最近は「Scala入学式」と称してScalaに入門してもらう社内勉強会を主催しています。

というわけで今日もScalaの話です。

Seqの +::+

Scalaには順序を持ったコレクションの抽象としてSeqというtraitが定義されています。そして、そのSeqの先頭に要素を追加するのに +: が、最後に要素を追加するのには :+ が使えます。

val s = Seq(1, 2, 3)
println(0 +: s) // => List(0, 1, 2, 3)
println(s :+ 4) // => List(1, 2, 3, 4)

Seq は抽象なので、デバッグプリントすると具象クラスであるところの List が表示されますが、List is a Seq なので期待通りです。

一見「演算子」に見える +::+ ですが、じつはこれは Seq のメソッドです。まずはわかりやすい :+ から見てみましょう。

val s = Seq(1, 2, 3)
println(s :+ 4) // => List(1, 2, 3, 4)
println(s.:+(4)) // => List(1, 2, 3, 4)

s :+ 4 と書いた場合も s.:+(4) と書いた場合も同じ結果を返しています。というわけで、じつはscalaでは、object.method(arg)object method argとも書けるのですね。これは :+ メソッドに独特の挙動ではなく、ほかのメソッドでも同じことです。

println("myself".splitAt(2)) // => (my, self)
println("myself" splitAt 2) // => (my, self)

では +: についてはどうでしょうか。

val s = Seq(1, 2, 3)
println(s.+:(0)) // => List(0, 1, 2, 3)
println(0 +: s) // => List(0, 1, 2, 3)

なんと、object method arg という書き方ではなく、 arg method object という書き方になっています!

これはなぜかと言うと、scalaにおいて : で終わるメソッドの場合、dotなしの記法だと arg method object という書きかたになるというルールがあるからです。このようなルールがあるとなぜうれしいのでしょうか。じつは +::+ を同時に使うと嬉しさが伝わってきます。

val s = Seq(1, 2, 3)
println(0 +: s :+ 4) // => List(0, 1, 2, 3, 4)

「先頭に0を、末尾に4を追加したSeqを返している」というのがとてもわかりやすく表現されているのではないでしょうか。こういうようなときに、たしかに arg method objectという書き方ができると嬉しいかもしれません。

+::+とパターンマッチ

さて、+::+ はパターンマッチでも出てきます。

val s = Seq(0, 1, 2, 3)
s match {
 case _ +: xs :+ _ => println(xs)
 case _ => ()
} // => List(1, 2)

先頭要素 +: Seq :+ 末尾要素 で、「Seqの先頭に先頭要素を、末尾に末尾要素を追加Seq」が構築できるのと同じような感じで、パターンマッチで 先頭要素 +: Seq :+ 末尾要素 を使うことでSeqを分解することができています。さきほどの +: と  :+ の正体はメソッドでしたが、パターンマッチ内ではもちろんメソッドでマッチさせることはできません。ではこれらの正体は一体なんなのでしょう。

じつはこれらはobjectとして定義されています。定義を見に行きましょう。

https://www.scala-lang.org/api/current/scala/collection/$plus$colon$.html

https://www.scala-lang.org/api/current/scala/collection/$colon$plus$.html

unapllyが定義された+:というobjectと:+が定義されていることがわかると思います。

パターンマッチは内部でunapplyを呼び、成功した場合「マッチした」とみなして返り値をそれぞれの変数にbindするような挙動をしますが、+:というobjectや:+というobjectがunapplyを持っているおかげで、パターンマッチ内でこの「演算子に見えるようななにか」が使えるというからくりなのでした。

まとめ

Scala+::+ について見てきました。これらの「演算子に見えるようななにか」は、じつは普段はSeqのメソッドであり、パターンマッチの中ではunapplyを持ったobjectとして振る舞っていたんですね!「普段構築するとき」と「パターンマッチで分解するとき」の対応が取れているため、利用する側としては違和感なく使えていますが、じつは内部ではこんな工夫がなされているんだよ、というのが伝われば幸いです。

Functor における map の引数の順序を考えてたらいっこストンと腑に落ちた話

別に知見は書いてないですが、なるほどなーと思ったという感想を書いたエントリです。

ScalazとHaskellのFunctorの提供するmap(fmap)は、引数の順番が異なります。

  • Scalaz の Functor

    • def map[A, B](r: F[A])(f: A => B): F[B]
    • F[A] なFunctor値が最初の引数で、A => B な関数が次の引数で、F[B]なファンクター値が返り値
  • Haskell の Functor

    • fmap :: f => (a -> b) -> f a -> f b
    • (a -> b) な関数が最初の引数で、f a なファンクター値が次の引数で、f b なファンクター値が返り値。

つまり第一引数と第二引数が逆。

ふつうに考えると Scalaz のやつが直感的に思えます。C言語とかでも、ある構造体を操作するための関数って大体第一引数にその構造体を渡して、他のパラメータをその後に渡すし、Perlとかだって $nyan->do_something したら $nyan が第一引数に渡ってくるし。

なんでなんでそうなっているのか調べたわけではないんだけど、関数のリフティングするときにはHaskellみたいになってたほうが便利だよなーと思い当たって「ふーむなるほど」となりました。

要するに、Haskell スタイルだと f :: (a -> b) な関数を g :: (f a -> f b) に変換したいというときに、なにも特別なことをしなくても fmap f とすれば部分適応されるので自動的にそっから g :: (f a -> f b)を得ることができてうれしい。

一方 Scalaz はデータに注目した場合は直感的ではあるのだけれど、Liftingのためのメソッド lift の実装は def lift[A, B](f: A => B): F[A] => F[B] = map(_)(f) となっていて、引数の順番が違うせいで一枚噛ませる必要がある。

これ、言語の特徴を捉えていて面白いな、と思いました。つまり、Scalaオブジェクト指向的な考えで、第一の関心が「オブジェクト」の側にある。だから「最初にFunctor値を受け取って、それに対して関数を適用しますよ」という引数の順序のほうが自然なんだけど、liftみたいな"関数に主眼が置かれた操作"をやるときにScalazスタイルだと「引数の順番が逆だったらな〜」ってなる。

逆に、Haskellスタイルは第一の関心が「関数の側」にある。だから「関数をリフティングしたい」みたいなときは自然にかけるんだけど、データ(この例で言えば Functor値)のほうに注目していると、この引数の順序ってなんとなく不自然に思える。

で、思ったんですけど、これ、fmapに限らず、filterとかでもそうですね。Haskellはだいたい関数を第一引数に取るようになっていて、部分適応してやることによって新しい関数を作り出しやすいようになっている。

なるほどな〜〜〜という感じでした。

(さらに気づいたことを追記)

これ、型推論上の都合もありそう。型推論が左から右に流れるから、先にF[A]が来てないと、実際のFunctor値の型から A => B の A を推論してくれないという都合があるかもしれない。

Scala で直和型

ScalaMatsuriに参加したからというわけではありませんがScalaの話題を。

まず最初に直和型とは

例えばOptionみたいなやつです。Optionは、「必ずSome か Noneのどちらか」であるような型ですが、こういう「必ずAかB(かCか……)である」というような型を直和型と言います。

では、自分で直和型を定義するときのことを考えてみましょう。典型的かつ素直なアイデアとしては、 sealed trait を利用する方法があります。

sealed trait FirstPrecure
case object CureBlack extends FirstPrecure
case object CureWhite extends FirstPrecure

さて、これで FirstPrecure な値は CureBlack か CureWhite のどらかであるというのが表現できましたね。ポイントは trait を sealed で宣言している部分で、これによって外のファイルから勝手にFirstPrecureであるような型を追加することができなくなっています。

典型的なオブジェクト指向において、直和型というのは継承によって表現することが可能である、と言うことができるでしょう。

さて、ここまでは CureBlack も CureWhite も自分で定義した型なので単純な話なのですが、この方法だと「Int と String の直和型を作りたい」とか「自分が定義した型じゃないものの直和型を作りたい」みたいなときに困ってしまいます。自分が作ったわけではない型を勝手に自分が作った型の子クラスにすることはできませんからね。サブタイピングによる直和型表現の限界です。

型クラスによる表現を考えてみる

サブタイピングで表現できない、となったときの発想として、「じゃあそれ型クラスで表現できないかなぁ」という発想がありえそうです。素直にやるとこうかな?

sealed trait IntOrString[A]
implicit val i = new IntOrString[Int]{}
implicit val s = new IntOrString[String]{}

def nyan[T:IntOrString](x: T) = x

println(nyan(1)) // => 1
println(nyan("hoge")) // => hoge
println(nyan('c')) // => compile error

うーん、まあ、やりたいことが実現できていると言えばできていますね(implicit parameterが型クラスと同等であるという話に関しては 以下のエントリを参照してください Scala の implicit parameter は型クラスの一種とはどういうことなのか - 猫型の蓄音機は 1 分間に 45 回にゃあと鳴く)。しかしちょっと直感的ではない気もするし、val i とか val s とかが名前空間汚してる感じがある……。でもわたしのお脳ではこのアイディアまでが限界でした。

というわけで調べてみた

私よりも頭の良いひとが絶対になんか良い方法を考えついているはず、と思い調べてみたら、やっぱりというかなんというか、もっときれいな方法で直和型を表現する方法が Scalaz で提供されているようです。see 独習 Scalaz — 余積

type IntOrString = t[String]#t[Int] という構文で直和が表現できているのは直感的(というかHaskellっぽい)ですし、なるほどなるほど〜という感じです。詳しいメカニズムについてはまだ理解しきれてないので今度じっくり読んで考えてみることにして、脳が疲れたのでひとまず今日はここまで。

というか、TaPL 積んでるの完全にアカンという感じになってきたな……。

続・結局型クラスって何がうれしいのっていう話

今回は時間がないので簡単に見ます。

型クラスによってアドホック多相が実現されるとどんな柔軟なことができるのかという例を見ましょう。

たとえば、List#max について見て見ましょう。これ、要素が String だったら辞書順での最大値を返すし、Int だったら数的な意味での最大値を返しますよね。もし「そもそも比べられない型」が要素に入ってたらコンパイルエラーになります。

class Nyan(val value: String) //比べることのできない型

println(List(1, 2, 3).max) // => 3
println(List("a", "b", "c").max) // => c
println(List(new Nyan("a"), new Nyan("b"), new Nyan("c")).max) // => error:No implicit Ordering defined for this.Nyan.

おっこれはまさにアドホック多相ですね。そして、API ドキュメントを読むと、これのFull Signature は def max[B >: A](implicit cmp: Ordering[B]): A になっています。あっこれは完全に型クラスですね。

では例によって動きを詳しく見てみましょう。Aはどこから出てきたかというと List の定義 class List[+A] からです。となると、B は、[B >: A] より、Listの要素と同じクラスか、その親クラスに限定されることになりますね。ではこのとき List(1, 2, 3).max とすると、何が起こるでしょうか。まず A が Int に確定しますね。これによって、B は Int かその親クラスに確定します。すると implicit cmp の型は Ordering[B :> Int]となりますね。 APIドキュメントを引くとIntOrdering extends Ordering[Int] という trait が見つかりました。これで implicit cmp の型は IntOrdering に確定です。ここまでは今までさんざん見てきたのと同じ理屈ですね。

では新しく作った Nyan を Ordering 型クラスのインスタンスにしてみましょう。Ordering[Nyan] 型の implicit な値を定義してあげればよかったですね。

class Nyan(val value: String) {
  override def toString = s"Nyan(${value})" // 表示のため
}

implicit val nyanOrdering = new Ordering[Nyan] {
  def compare(a: Nyan, b:Nyan) = implicitly[Ordering[String]].compare(a.value, b.value)
}

println(List(1, 2, 3).max) // => 3
println(List("a", "b", "c").max) // => c
println(List(new Nyan("a"), new Nyan("b"), new Nyan("c")).max) // => Nyan(c)

おおー!!List#maxがNyanに対応した!!!!型クラス、めっちゃ便利なのでは?ということがわかってきたかと思います。

あっ、しれっと implicitly[Ordering[String]] とかいう今まで見たことのない新要素が登場していますが、これは「context bound」と呼ばれる記法で、「Ordering[String] 型の implicit な値を探してきてここに入れてください」という意味です。今回は Nyan の中の値が文字列なので、Ordering[String] の compare メソッドに処理を委譲したかったわけですが、特別なことはなにもせずに List("a", "b", "c").maxが呼べるということは、すでに Ordering[String] 型の implicit な値はこのスコープに存在しているわけで、だったらそれ使っちゃえばいいよね、ということでここで context bound を活用してみました。今APIリファレンス改めて引いたら Ordering.String ってのがあったので、Ordering.String.compare でもかまわないと思います。

で、結局型クラスって何がうれしいの、ということの説明

前回の記事では、Scala では implicit parameter を利用することで型クラスと同等のことが実現できることがわかりました。しかし前回の疑問として、「で、それの何がうれしいのよ」というのは残っていましたね。

今回はそのうれしみをなるべくわかりやすく説明したいと思います。

たとえば、色んなクラスに flipFlap メソッドを生やしたい

まず、前回と同じく「値をひっくり返す」という flipFlap について考えてみましょう。

前回は関数を作りましたが、Scalaらしく、こういう動きにしたいですね

1.flipFlap // => -1
true.flipFlap // => false
"string".flipFlap // => gnirts

Scala ではクラスを拡張するときには implicit conversion を使うのが一般的ですね。

class IntFlipFlapWrapper(x:Int) {
  def flipFlap(x: Int) = - x
}
implicit def toIntFlipFlapWrapper(x: Int) = new IntFlipFlapWrapper(x)

これで、1.flipFlap のようなステートメントを見つけると、コンパイラは「ん、Int には flipFlap なんてメソッドはないぞ?」となり、「implicit conversion でなんか別のクラスにならないかな?」と implicit conversion を探しにいきます。すると、toIntFlipFlapWrapper というメソッドが見つかりますね。引数に Int をとり、別のクラスに変換するメソッドが implicit で定義されているやつです。このとき、変換先である「IntFlipFlapWrapper」がflipFlapメソッドを持っているので、1 はコンパイラにより IntFlipFlapWrapper に変換されて、IntFlipFlapWrapperのflipFlapメソッドが呼び出される、という流れです。

さて、ここまでは問題ありませんが、flipFlap 可能な型すべてに対して implicit conversion を定義するのはちょっとエレガントじゃないですね。あと、「flipFlap 可能な型の集合」ってのがソースコード中のどこでも示されていなくて、整理されていない感じになってしまいそうです。

そこで型クラスですよ

ところで、型クラスというのは「ある計算が可能な型の集合を定義する」ためのものでした。これはめっちゃこの用途にマッチする気がしましね。では実際にやってみましょう。

// FlipFlap型クラスを定義
trait FlipFlap[T] {
  def flipFlap(x: T): T
} 

// Int をFlipFlap型クラスの型インスタンスとして定義
implicit val intFlipFlap = new FlipFlap[Int] {
  def flipFlap(x: Int): Int = - x
}

// String をFlipFlap型クラスのインスタンスとして定義
implicit val stringFlipFlap = new FlipFlap[String] {
  def flipFlap(x: String): String = x.reverse
}

ここまでは前回と一緒です。前回 implicit object で定義してた型インスタンスを implicit val で定義してますが、やってることは一緒です。flipFlap メソッドポリモーフィズムさせるためのオブジェクトたちを implicit で作ってるだけです。

さて、ではこいつを利用して、FlipFlap型クラスのインスタンスであるところのInt, String を一気に拡張してみましょう。

型クラスを利用してそのインスタンスを一気に拡張する

implicit conversion でクラスを拡張するためには、まずは変換先のクラスを作らないといけませんね。でも、今回は具体的な型ではなくて、たくさんの型に一気に対応しないとダメなので、具体的なクラスではなくて、型パラメータをともなった trait で作りましょう。

trait FlipFlapWrapper[A] {
  val f:FlipFlap[A] // ポリモーフィズムするためのオブジェクト
  val v:A // 拡張される側の値
  def flipFlap():A = f.flipFlap(v)
}

def flipFlap が、生やしたいメソッドです。こいつは f に対して処理を委譲しているだけですね。で、肝心の f は、もし [A] が [Int] の場合 f は FlipFlap[Int] となります。もし[A]が[String]なら、FlipFlap[String]になりますね、そんな感じで、[A]が変わるとflipFlap メソッドの移譲先の型も決定し、ポリモーフィズムする仕組みになっています。ではこいつを利用して implicit conversion のための implicit def を定義してやりましょう。

implicit def toFlipFlapWrapper[A](value:A)(implicit implicitFlipFlap:FlipFlap[A]) = new FlipFlapWrapper[A] {
  val v = value
  val f = implicitFlipFlap
}

こんな感じです。さて、この時点で、1.flipFlap するとどうなるでしょうか?Int には flipFlap が生えていないので、コンパイラは implicit conversion を探しに行きます。で、toFlipFlapWrapper が見つかります。引数に Int が渡されたので、型推論により toFlipFlapWrapperの[A]が[Int]に確定しますね。そうすると implicitFlipFlap の型も自動的に FlipFlap[Int] に確定し、intFlapFlap が暗黙的に束縛されます。これは前回見たのと同様の理屈ですね。さて、そうなると、このときもとの "1" という値は、FlipFlapWrapper の v に 1 が、 f に intFlapFlap が束縛されたオブジェクトに変換されることになります。結果、1.flipFlapintFlipFlap.flipFlap(1) を計算することになり、-1 を返します。

では"string".flipFlapするとどうなるでしょうか。同じ理屈で、"string" は f にはstringFlipFlapが、v に "string" が束縛された FlipFlapWrapperに変換されます。結果、"string".flipFlapstringFlipFlap.flipFlap("string") が計算され、"gnirts"が返ってきます。

では例によって、Boolean もFlipFlap型クラスのインスタンスにしてみましょう。

// Boolean をFlipFlap型クラスの型インスタンスとして定義
implicit val booleanFlipFlap = new FlipFlap[Boolean] {
  def flipFlap(x: Boolean): Boolean = ! x
}

これでいいですね。

さて、 true.flipFlap してみます。今までと同じ理屈で、implicit conversion が走り、true.flipFlap は FlipFlapWrapper の v が true に、f が booleanFlipFlap に束縛されたものに変換されます。結果、booleanFlipFlap.flipFlap(true)が計算されることになり、false が返ります。

こんな感じで、「flipFlap可能である」という同じ特徴を持つ型の集合を、FlipFlap型クラスのインスタンスにして、それらを利用した implicit conversion を定義することで、FlipFlap型クラスのインスタンスすべてを同時に拡張することができました。

まとめ

型クラスを利用することで、共通の性質を持つ型を型クラスにまとめた上で、その型すべてを拡張することができてうれしい、ということが言えそうです。おもしろいのは、今回で言えば Boolean も Int も String も、共通の祖先クラスを持っていない上、組み込みクラスであることですね。自分で作ったクラスに対して共通の性質を付与するならば、同じ trait を継承するという手段はありますが、組み込みクラスにあとから新しく祖先クラスを追加することはできません。伝統的なオブジェクト指向のタイプヒエラルキーよりも、型クラスと型インスタンスによるヒエラルキーのほうが柔軟であることが見て取れたかと思います。

追記

というわけなので、これで型クラスのうれしみがすべて説明されているわけではないのでまだまだ学習が必要ですね。道は険しい。

Scala の implicit parameter は型クラスの一種とはどういうことなのか

なんか型クラスとか言うと型の怖いひとたちが怖い話をワイワイしてるイメージがありますよね。わたしはあります。「で、それって何がうれしいのよ」とか、そういう話はあまりされていないような印象がありますね(あくまで印象です)。その上 "Scala の implicit parameter は型クラスの一種" とか言われると「暗黙的な引数がなんで型クラスの一種なんや!!!意味がわからん!!!!」となります。わたしはなりました。

というわけでそのへんについて勉強したので書きます。

そもそも型クラスってなんや

Haskellとかにあるやつですね。アドホック多相を実現するもの、らしいです。すごい、いきなり意味がわからない。

というわけで、まずは「アドホック多相ってなんなの」という話からして行きます。

さて、まずは「多相」から行きましょう。この文脈で言う多相とは、簡単に言えば「引数にいろんな型を取れる」ということです。

よく例に出される id 関数を見てみましょう。

def id[T](x: T) = x

引数をそのまま返すだけの関数です。この関数は、どんな型の引数でも受け取ることができますね。こんなふうに、「いろんな型を引数に取れる」ってのがこの文脈で言うところの「多相」だと思ってください。

ちなみに、引数に型パラメータを指定することで「どんな型でも受け入れるぜ」を実現するようなのを「パラメータ多相」と呼ぶそうです。

では、アドホック多相というのは?アドホックって、「その場で」とか「場当たり的に」とかそんなニュアンスの言葉ですよね。で、この文脈でアドホック多相関数ってのはどういう意味かっていうと、「この型のばあいはこういう計算をするよ」ってのを定義すると、その型を引数にとることができるようになる関数くらいの意味だと思ってください。なので、アドホック多相の場合、「ありとあらゆる型に対応」ではなくて、「振る舞いが定義されている型にのみ対応」することになります。例えば Int と String に対する振る舞いは定義されてるけど Boolean に対する振る舞いは定義されてないので Boolean は受け取れない、みたいな多相関数がありうるわけです。

具体的な例として、たとえば、「引数をひっくり返す」という振る舞いをする、IntとStringに対応したアドホック多相関数を考えましょう。

flipFlap(1) // => -1
flipFlap("string") // => gnirts
flipFlap(true) // => コンパイルエラー

こんな感じです。こういう動きをする関数を「アドホック多相関数」と言うみたいですね。

さて、では型クラスというのはなんなのでしょう?それは、「アドホック多相な関数がどんな型に対応してるか」を制限するための「インターフェイス」のようなもののようです。あるいは、「アドホック多相関数が受け取れる型の集合」みたいなものです。ちょっと具体的に見てみましょう。今、flipFlap は Int と String に対応していますね。このとき、便宜的にflipFlap関数の引数の型クラスを「flipFlap型クラス」と呼ぶと、Int と String は「flipFlap型クラスのインスタンスである」と言います。flipFlapが受け取れる型の集合を「flipFlap型クラス」と呼ぶとき、その要素ひとつひとつを「flipFlap型クラスのインスタンス」と呼ぶ訳です。

逆の言い方をすると、「flipFlap型クラスのインスタンス」である型でありさえすれば、flipFlap関数の引数として渡して上げることができる、ってことでもありますね。

インスタンスとかクラスとか言ってるけど、オブジェクト指向のいわゆる「クラス」とか「インスタンス」とは関係がないことに注意してください。型クラスと型インスタンスの関係はむしろ、型ができることを定義した抽象的なものであるインターフェイスと、その具体的な型であるクラス、という関係に近いかもしれませんね。

さて、話は変わって、たとえば、さらにあとからflipFlap 関数に Boolean に対する振る舞いを定義してあげたとします。すると、

flipFlap(1) // => -1
flipFlap("string") // => gnirts
flipFlap(true) // => false

となりますね。これは言って見れば、あとから Boolean を「flipFlap型クラス」に属するインスタンスにしてあげたことになるわけです。

実際にやってみる

では実際に Scalaアドホック多相を実現してみましょう。型パラメータ使ったらうまく実現できないかな〜、という発想がまず出てきそうです。

def flipFlap[T](x: T) = ...

で、パターンマッチで x の型に応じた処理を書く…?でもそうすると「対応してない型の場合はコンパイルエラー」にできないですよね。詰まってしまった……。しょうがないので、そこで、いきなり最終形をめざすのではなくて、いくつか段階を踏んでやっていきましょう。まずは、「型によって振る舞いが異なるなら、"ひっくり返す"という操作の実装を flipFlap の外に出しちゃえばいいんじゃないの?」という発想でこういうのはどうでしょう。

trait FlipFlapper[T] {
  def doFlipFlap(x:T):T
}
object IntFlipFlapper extends FlipFlapper[Int] {
  def doFlipFlap(x:Int) = - x
}
object StringFlipFlapper extends FlipFlapper[String] {
  def doFlipFlap(x:String) = x.reverse
}

def flipFlap[T](x:T, flipFlapper: FlipFlapper[T]) = flipFlapper.doFlipFlap(x) // ...(1)

flipFlap(1, IntFlipFlapper) // => -1
flipFlap("string", StringFlipFlapper) // => "gnirts"

(1)の行に注目してください。flipFlap 関数に、 FlipFlapper 型の引数を追加してみました。そして、その与えられたflipFlapper の doFlipFlap に、実際に値をひっくり返す処理を委譲しています。伝統的なオブジェクト指向ポリモーフィズムを活用した感じですね。ちょっと引数が増えちゃったけど、Tが変われば振る舞いが変わる、ということは実現できています。さらに、ここで例えばflipFlap(true, StringFlipFlapper)みたいなことをしても、StringFlipFlapper は FlipFlapper[Boolean] ではないのでコンパイルエラーとなります。

さて、それでは、flipFlap を Boolean に対応させてみましょう。

object BooleanFlipFlapper extends FlipFlapper[Boolean] {
  def doFlipFlap(x:Boolean) = ! x
}

flipFlap(true, BooleanFlipFlapper)

まあ、一応これでも動くっちゃ動くし、安全っちゃ安全ですね。うーん、でもこれ、やっぱり第二引数が邪魔ですね。第一引数の値の型はわかってるんだし型推論だってあるんだから、なんとかこの第二引数は暗黙的にこううまく解釈してやってくれないかなぁ……、となります。今「暗黙的に」という言葉が出てきましたね。implicit parameter を使えばよさそうです。

trait FlipFlapper[T] {
  def doFlipFlap(x:T):T
}
implicit object IntFlipFlapper extends FlipFlapper[Int] { // ...(1)
  def doFlipFlap(x:Int) = - x
}
implicit object StringFlipFlapper extends FlipFlapper[String] { // ...(2)
  def doFlipFlap(x:String) = x.reverse
}

def flipFlap[T](x:T)(implicit flipFlapper: FlipFlapper[T]) = flipFlapper.doFlipFlap(x) // ...(3)

flipFlap(1) // => -1
flipFlap("string") // => "gnirts"

さて、まずは(3)から見て行きましょう。

第二引数は implicit parameter として暗黙に取るように変更しました。この状態で例えば flipFlap(1) と渡したらどうなるでしょうか?

まず、T が 型推論によって Int に確定しますね。となると、自動的に flipFlapper の型も FlipFlapper[Int]に確定します。

さて、このスコープにおいて implicit で定義されている FlipFlapper[Int] 型の値は、(1)で定義されている IntFlipFlapper だけですので、これで無事 flipFlapper には IntFlipFlapper が暗黙に束縛されます。

もし flipFlap("string") とした場合はどうなるでしょう。同じ理屈で T が String に確定し、(2)で定義されている FlipFlapper[String] 型の implicit な値であるStringFlipFlapper が flipFlapper に束縛されます。

ではこのflipFlap 関数に Boolean を追加したい場合はどうすれば良いでしょう?

implicit object BooleanFlipFlapper extends FlipFlapper[Boolean] {
   def doFlipFlap(x: Boolena) = ! x
}

を追加するだけでいいですね。同じ理屈で、これさえ定義してあれば flipFlap(true) と渡すと T が Boolean に確定して、flipFlapper には BooleanFlipFlapper が暗黙的に束縛されます。

じゃあ、flipFlap('c')としたら?対応する FlipFlapper[Char] が implicit に定義されていないので、コンパイルエラーになりますね!やった!

さて、このとき、型クラスとの対応はどうなっているでしょうか。FlipFlapper[T] が型クラス、FlipFlapper[Int] や FlipFlapper[String] が型クラスのインスタンスの役割を果たしています。すこし丁寧に説明しましょう。def flipFlap[T](x:T) なら、Tにはどんな型も入って来れてしまいます。しかし、(implicit FlipFlapper[T])があると、FlipFlapper[具体的な型] が定義されている型以外を引数に渡したらコンパイルエラーになりますね。そして、例えば FlipFlapper[Int] を定義してやれば、flipFlap は Int を受け取ることができます。つまり、 「FlipFlapper[具体的な型]」を継承したオブジェクトがある場合、その具体的な型が「flipFlapが計算可能な型」になるわけですね。

そんな感じで、FlipFlapper[T] が型クラス、FlipFlapper[Int] や FlipFlapper[String] が型クラスのインスタンスの役割を果たしている、と言えるわけです。

これで、 implicit parameter が型クラスの一種である、ということが確認できました。

まとめ

  • アドホック多相な関数ってのは、「この型に対してはこういう振る舞いをするよ」と定義しておくことでいろんな型を受け取ることができるようになる関数。
  • アドホック多相が実現されてると、型によって柔軟な振る舞いができてうれしいし、コンパイル時に型チェックで守られてうれしい。
  • アドホック多相関数が受け入れることが可能な具体的な型(IntとかStringとか)」のことを「型クラスのインスタンス」と呼び、その集合を「型クラス」と呼ぶ。
  • 言い換えれば、型クラスは「そのアドホック多相関数がどんな具体的な型を受け入れることができるのか」を縛る役割をするものでもある。
  • Scalaにおいては implicit parameter で渡した値を多態させることでアドホック多相が実現できる。
  • 多態させるためのインターフェイスであるところの trait が型クラスに、その実装であるところの implicit な object が型インスタンスに対応する。
  • すなわち、implicit parameter は型クラスの一種である。

その他

単純に「引数の型によって振る舞いが変わる」という意味ではメソッドオーバーロードで実現できるじゃないか、という疑問は残るので、「こんな複雑なことをしてまで何がうれしいのか」という疑問は残る。ただ、オーバーロードだと「計算する側の型」つまりレシーバの側の型に処理が実装されるけど、型クラスと型インスタンスの関係だと「計算される側」、つまり「パラメータ」の側の型に処理が実装される。これは言い方を変えると、「新しい型に対応させたくなったときに、その新しい型の側に実装が書ける」ということであり、このほうがより変更に対して開かれているし、柔軟だし、責務の上でも適切かもしれないと感じる。

ただ、そのうれしみを享受するためにここまでのことをしなければならないことを考えると、「そういうことしたいなら言語仕様で型クラスがサポートされてる Haskell 使えばいいんじゃないの……」という気持ちがわき上がらないわけでもない……。「なにがうれしいのか」という部分に対してはまだそういう程度の理解度なので、もう少し考えてみたいと思う。

追記:

うれしいパターン一個あった。こういうのはどうでしょうか? https://gist.github.com/Shinpeim/76c687b3c9a8f8126da0

さらに:

2014.10.9 1:11 追記

ブコメより

id:matarillo (続く記事も読みました)C#/Java的に言うと、型クラスは型制約なんですよね。とある型をとある型クラスのインスタンスに「後から」できるかどうかは本質ではないという理解。

型制約というだけならば trait でいいじゃないかという話になってしまい、わざわざ型クラスを導入するメリットがないので、やはり既存の型に対する拡張が可能というのは型クラスの本質(の一部)ではないかなぁという理解をわたしはしています。実際型クラスの存在しない C#/Java でも、サブタイピングでの型制約は実現できているわけですし、「なぜわざわざ型クラスを導入する必要があるのか」という視点から見ると、サブタイピングでは実現できない柔軟さを実現する必要があるからかな、と思います。「そうじゃないんだ」「それはこういう理由で本質的には型制約と同等なのだ」という意見があれば是非参考にしたいので教えてください。

Scalaの勉強会、Scala入学式を新潟県長岡市でやってきました #nds33

Scala 一年生のわたしですが、僭越ながら「みんなで Scala に入門しようぜ!」というハンズオン形式のセミナーの講師を行ってきました。「Scala 入学式」という名前は papix 氏がやってる「Perl 入学式」のパクりです!

この勉強会に使った資料は以下にアップしておきます。 https://gist.github.com/Shinpeim/6740436 一部マークダウン壊れてるけど勘弁してもう燃え尽きた。

東京から jewelve 氏と moznion 氏が参戦してくれたりと、LTもえらい面白かったです。講師役がなんかいろいろ書くのもアレなんで、参加者のみなさんのブログを見ていただければなと思います!(参加者のひとはぜひブログ書いて!見つけ次第リンクしていきます)

と思ったけど、準備などで何を考えたかシェアして悪いことはないな、と思うので、一応少し書きます。

事前準備について

今回はハンズオンなので、参加者のみなさんには事前にいろいろと準備をしてもらう感じにしました。ただ、けっこうだるい作業とか多くて、「入学式の前に入学試験がある」という反応などもあり、もっとていねいにやるならば環境構築からまずはハンズオンでやるというのが優しいのかな、と思いました。

でも、今回は無料のセミナーなんで、そこまでは面倒みなくてもいいかな、というのもあり、簡単な事前準備のための資料だけを用意しておき、「それを読んで準備しといてね」って感じで参加者のみなさんの強い意思にお任せした結果、全員きちんと事前準備を行って来てくれるという感じでした。今までいろいろなイベントを見てきましたが、全員がちゃんと準備をしてドタキャンもないみたいなイベントは決してあたりまえではないし、NDS はいつもながら「なんとなく」な参加者のいないすばらしい勉強会だなと感じました。civicさん++。

資料作りについて

Scalaはなんか入門しにくい印象があるみたいなので、まず、入学式の目標を、「手を動かしてScalaの良さを実感してもらう」ということにしぼりました。そのため、楽しさを実感してもらうために必要であろう事柄はふんだんに、しっかり書くなら知っておくべきだけど、あとからしっかり学ぶときでもいいかな、という事柄は大胆にカットするという作業から始めました。その結果、本当に言語の基本的な部分と、Java を書いてるときに「だるい」ってなる部分を改善する機能、あるいは型付けの良さを実感してもらえる機能などは盛り込みつつも、implicit conversionなどの機能には全く触れない、という方針を決めました。

そこから、それぞれの機能に対して、「なぜこの機能があるとうれしいのか」を実感できるようなプログラム例をひねり出す作業を行いました。そこで、「型に守られる安心感」「型で設計する面白さ」を実感できる例として、あのゲームの一部分をめっちゃしょぼくしたみたいなプログラムを、パターンマッチや再帰の組み合わせの面白さを実感できる例として「仕事は終わらないわ、私が守るもの」のプログラムを、mapやflatMapでモナディックな操作を行うとうれしい例として List と List の要素の組み合わせを得るプログラムや Option モナドの例や Future モナドの例などを思いつき、最初に洗い出した「今回触れるべき機能」があることでなにが嬉しいのかがなるべく伝わるようにカリキュラムを組んで行きました。例を探しては実装してみて「うーん、これだとそんなに嬉しさが伝わらない気がする、没!」みたい感じでかなり試行錯誤したので、準備の中でこの部分が一番大変でした。もう二度とやりたくない(とか言いながら自分が好きな物をひとに薦めるの大好きマンだからまたなんか出て来たらやるんだと思う)。

当日について

と、いろいろと頑張って準備した割には結構グダグダな進行になってしまったところがたくさんあって、ユーザー体験最悪だったんじゃないかとすごい不安になってたんだけど、参加してくださったひとのブログを読むとそれなりに狙いがうまく行ってたようなので、安心しました。よかった。がんばり無駄じゃなかった感がある。

そんな感じです!