きくらげ観察日記

好きなことを、適当に。

Scalaのtrait

traitとは

具体的な実装を持たないようなクラスをScalaではtraitと呼びます。Javaでいうinterfaceのようなもの。
abstract classとの使い分けは、abstract classが「それが何であるか」を指定するのに対し、traitでは「それがどういった振る舞いをするか」を記述するためにあると思っています。

基本的な使い方

Javaのinterfaceと同じです。例えば辞書型、連想配列、マップ等と呼ばれるアレの振る舞いを定義するための「MyMap」というtraitを定義してみましょう。
(余談ですが、Perlがいわゆる連想配列に「ハッシュ変数」という実装部分の都合で名前を付けてしまったのは失敗だったと思います。Perlでプログラミングを始めた初心者がハッシュマップでないマップのこともハッシュと呼んだりする事例が散見されるので。)

import scala.collection.mutable._

trait MyMap[K, V] {
  def +=(kv: (K, V))
  def lookup(key: K): Option[V]
}

(mutableなのが気持ち悪いですが、traitの説明のために分かりやすさ優先で書いているため悪しからず。)

このtraitの実装として、恐らく連想配列の実装の中で最も簡単な(そして最も効率の悪い)ものであるalistを作ってみましょう。

class AList[K, V](keyValues: (K, V)*) extends MyMap[K, V] {
  val items: Buffer[(K, V)] = keyValues.toBuffer

  def +=(kv: (K, V)) {
    items += kv
  }

  def lookup(key: K): Option[V] = {
    for ((k, v) <- items) {
      if (k == key)
        return Some(v)
    }
    return None
  }
}

ここまではOKですね。

traitを型クラス的に使う

Scalaでは、型クラスCのインスタンスAにおける振る舞いの集合をまとめてC[A]のようなtraitを作り、そのtraitの実装のインスタンスを暗黙的に受け取ることによって、無理矢理型クラスのようなことを実現できてしまいます。


例として、モノイドを表す型クラスをtraitを使って書いてみました。

trait Monoid[T] {
  def zero: T
  def add(x: T, y: T): T
}

object Monoid {
  def sum[T](xs: Traversable[T])(implicit M: Monoid[T]) =
    xs.foldLeft(M.zero)(M.add)
}

object IntMonoid extends Monoid[Int] {
  val zero = 0
  def add(x: Int, y: Int): Int = x + y
}

object MonoidTest {
  implicit val intMonoid = IntMonoid
}

コンソール上で実行してみましょう。

scala> import Monoid._
scala> import MonoidTest._
scala> val numbers = 1 until 100
scala> sum(numbers)
res0: Int = 4950

このtraitの使いかた、便利ではあるけど正直初めて見たときはかなり気持ち悪いと思いました……。しかし、Scalaから入った人のQiitaのエントリーとかを読んでいると、むしろHaskellでの型クラスの実装のほうに違和感を感じている人が多いようなので(要出典)、ようは慣れなのかな…?

Haskellの型クラスとの違いは、暗黙的に受け取るオブジェクトを変更することによって、同一の型に対して複数インスタンス定義ができることです。例えば、(Z, +, 0)と(Z, *, 1)はどちらも共にモノイドなので、次のようにMonoid[Int]の実装を2つ作ることもできます。

object IntSumMonoid extends Monoid[Int] {
  val zero = 0
  def add(x: Int, y: Int): Int = x + y
}

object IntProductMonoid extends Monoid[Int] {
  val zero = 1
  def add(x: Int, y: Int): Int = x * y
}

object Add {
  implicit val intMonoid = IntSumMonoid
}

object Mul {
  implicit val intMonoid = IntProductMonoid
}

Intを(Z, +, 0)のモノイドとして扱いたい場合はAddを、(Z, *, 1)のモノイドとして扱いたい場合はMulをそれぞれimportすれば、期待通りの動作が得られます。

scala> import Monoid._
scala> import Add._
scala> sum(0 until 10) //=> 0 + 1 + 2 + ...  + 10
res1: Int = 45
scala> import Monoid._
scala> import Mul._
scala> sum(0 until 10) //=> 0 * 1 * 2 * ... * 10
res1: Int = 0

さらに、モノイド同士の加算のための新たな演算子「|+|」を定義した新たなラッパーのようなクラスを作り、そのクラスへの暗黙的型変換を用意することによって、こんなこともできてしまいます。

object Monoid {
  // sumの定義はさっきと同じ
  implicit class MonoidOps[T](val value: T) {
    def |+|(y: T)(implicit M: Monoid[T]) = M.add(value, y)
  }
}

実行結果

scala> import Monoid._
scala> import Mul._
scala> 3 |+| 5
res0: Int = 15

これはすごい、けど気持ち悪い(個人的に暗黙的型変換が嫌いなので)。
程々に利用すればとても便利だとは思います。

ちなみに、class MonoidOpsのimplicit修飾子を外して、かわりにobject Monoidの中で

implicit def toMonoid[T](x: T): MonoidOps[T] = new MonoidOps(x)

のようにすることでも同じ結果を得られますが、こちらはコンパイル時に警告が発生します。
暗黙的な型変換の濫用はよろしくないけど、implicit classで定義できる程度の単純なものならまあ許してやろうってことなのかな?

何にせよ、Haskellでのやりかたとは全く違うので、まだまだ慣れが必要なようです。

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

Scalaファンクショナルデザイン ―関数型プログラミングの設計と理解

Scalaファンクショナルデザイン ―関数型プログラミングの設計と理解