2012年7月19日木曜日

クラウド温泉3.0 (3) / 代数的データ型 on Scala

クラウド温泉3.0@小樽のセッション「Monadicプログラミング・マニアックス」で使用するスライドのネタ検討その3です。

前回は代数的データ型について考えました。

ボクの非公式理解では、代数的データ型は:

  • 直積の直和の総和

です。

代数的データ型は以下のように、オブジェクト指向のインヘリタンスと対置する位置付けにある、関数型のかなり重要な構成要素であることが確認できました。

  • オブジェクト指向はクラスでインヘリタンス
  • 関数型は代数的データ型で選択

この代数的データ型ですが、Scalaではケースクラスで実装するのがイディオムになっています。

ケースクラス

以下のようにケースクラスPersonを定義します。これがそのまま代数的データ型となります。

scala> case class Person(name: String, age: Int)
defined class Person

代数的データ型は「直積の直和の総和」ですが、その単純形である「直積」そのものも代数的データ型です。ケースクラスはコンストラクタに並んだ要素の直積と見立てることが可能です。このためケースクラスを定義すれば、そのまま代数的データ型ということになります。

直積

Scalaでは直積を表すトレイトProductが定義されています。以下のように、ケースクラスPersonはProductでもあります。なんとケースクラスは意味上だけではなく、Scalaの型システム上も直積になっているというわけです。

scala> var a: Product = Person("Taro", 30)
a: Product = Person(Taro,30)

Scalaの代表的な直積の表現はタプルですが、このタプルも以下のようにProductとなっています。

scala> var b: Product = ("Taro", 30)
b: Product = (Taro,30)
選択

代数的データ型を「直積の直和の総和」として、直積そのものの実現はケースクラスで可能なことを説明しました。

それでは「直和の総和」はどう実現すればよいでしょうか。

これは、以下のようにsealed trait(またはsealed abstract class)とケースクラスの組み合わせがイディオムになっています。

sealed trait A
case class B() extends A
case class C() extends A
case class D() extends A

「直和の総和」の実現にインヘリタンスを使っていますが、オブジェクト指向的なインヘリタンスと違うのは、ベースクラスをsealedにすることで、サブクラスが後から追加されないことが保証されている点です。

このことで「直和」を表現しています。さらに複数のサブクラスを並べることで「直和の総和」を表現しています。

Scalaはオブジェクト指向の核の上に、関数型を載せている二段構成の言語システムになっているので、型システムはオブジェクト指向が核になります。このためオブジェクト指向の型システムを使って代数的データ型を実現すると全体の整合性が取れるということだと思います。

代数的データ型は選択と組み合わせて使用します。選択はScalaではmatch式で記述します。

先ほどの代数的データ型Aは以下のようにして選択を適用することができます。match式では「抜け漏れ」があるとコンパイル時に「match is not exhaustive!」の警告が出ます。

def f(a: A) = {
  a match {
    case b: B => xxx
    case c: C => xxx
    case d: D => xxx
  }
}
代数的データ型として気をつけること

前の説明で、「クラスのインヘリタンス」と「代数的データ型の選択」を対置しましたが、ケースクラスでは「代数的データ型の選択」の実現に「クラス(トレイト)のインヘリタンス」を使うというややねじれた構造になっています。しかし、これはScala言語の構成に適した実現方式ということであって、オブジェクト指向のクラスの機能を積極的に使うという意図ではない、という点は留意しておくとよさそうです。

ケースクラスは代数的データ型として使用するが想定されているクラスですが、文法的にそのことを強制する制約はないので、普通のクラスとして色々な機能を追加することができます。しかし、関数型プログラミングの中で代数的データ型としての用途を意識するケースでは、こういったオブジェクト指向的な機能の濫用は避けるのが賢明です。

列挙型

代数的データ型の応用として列挙型を表現する事ができます。この場合はケースクラスではなく、ケースオブジェクトを使うとよいでしょう。ケースオブジェクトを使うと、「Sunday()」ではなく「Sunday」という形で列挙型を使うことができるようになります。また、オブジェクトの生成を抑制する効果もあります。

ScalaではEnumというオブジェクトが用意されていますが、使い勝手が悪いので列挙型はケースオブジェクトを使うのを軸に考えるとよいと思います。ケースオブジェクトを使うことで「代数的データ型」的な効果(match式の抜け漏れチェックなど)も期待できます。

sealed trait DayWeek
case object Sunday extends DayWeek
case object Monday extends DayWeek
case object Tuesday extends DayWeek
case object Wednesday extends DayWeek
case object Thursday extends DayWeek
case object Friday extends DayWeek
case object Saturday extends DayWeek

列挙型の実現は、ケースオブジェクトではなく普通のオブジェクトを使うことも可能です。

sealed trait DayWeek
object Sunday extends DayWeek
object Monday extends DayWeek
object Tuesday extends DayWeek
object Wednesday extends DayWeek
object Thursday extends DayWeek
object Friday extends DayWeek
object Saturday extends DayWeek

ただ、ケースオブジェクトの方が以下のようなメリットがあるので、普通はケースオブジェクトにしておくとよいでしょう。

  • Productを継承して型システム上も直積つまり代数的データ型になる。
  • toStringで「Sunday」や「Monday」といった文字列が表示されるようになる。
  • Serializableになる。

スライド案

代数的データ型、ケースクラス、ケースオブジェクトについて考えてきましたが、Monadicプログラミングのテーマでセッション1時間に収めることを考えると、このあたりはほとんど触れることはできなさそうです。

「代数的データ型」のタイトルのスライドに以下のプログラムを載せておく感じかな。

case class Person(name: String, age: Int)

sealed trait A
case class B() extends A
case class C() extends A
case class D() extends A

sealed trait DayWeek
case object Sunday extends DayWeek
case object Monday extends DayWeek
case object Tuesday extends DayWeek
case object Wednesday extends DayWeek
case object Thursday extends DayWeek
case object Friday extends DayWeek
case object Saturday extends DayWeek

0 件のコメント:

コメントを投稿