それでenumとclassのどっちにすれば良いの?

この記事には

enumが便利なのは

  • ある状態では有効だけど、別の状態では有効ではない値を定義するとき

です。

と書きましたが、「それはサブクラスっていうやつではないか」という声があります(どこに?)。その通りで、enumとクラスはよく似ている側面があって、enumでできることはクラスでも(再コンパイルの量とか、網羅性の検査とか、isKindOf:とか、キャストとかの問題を見なかったことにすれば)できるという話でした。

ここで疑問になるのは、

  • なぜSwiftにはenumとclassという似たような機能が二つ用意されているのか(TMTOWTDI?)
  • 何を考えてenumとclassを使い分ければ良いのか

でしょう。

enumとclassの違い

Swift的には「classは状態を持てるがenumは持てない」という違いがあります。これはSwiftプログラミング言語の設計として導入した恣意的な制限で*1、もう少し抽象的にはenum (ADT) とclassには次のような違いがあります。

  • enumは個々のcaseを追加するのは大変だけど、値の操作を追加するのは楽
  • classは個々のサブクラスを追加するのは楽だけど、操作を追加するのは面倒

enumにcaseを追加すると、全体的にあなたのコードは全体的にコンパイルし直しで、多分switchがエラーになりまくります。黙って実行時エラーにされるよりはマシですが、一つ一つ確認して、丁寧に手作業で修正してまわらないといけません。めんどくさい。一方で、クラスにサブクラスを追加した場合はなにが起きるかというと、全体のコンパイルは必要ないし、既にスーパークラスを使っている部分もソースコードを修正する必要がありません。楽。

逆に、enumに操作を追加するのは簡単で、switchを書けばそれで終わりです。プログラミングしてて、「あ、ここはそれぞれのcaseで別々の操作が必要だな!」とか多分考えもせずに、無意識にswitchを書くことになるでしょう。classに操作を追加するのは、スーパークラスメソッドを追加して、サブクラスで全部実装してまわると言うことです。まずメソッド名を考えないといけませんし、中身が一行だけだったりするとそもそもfuncとか書くのも面倒ですし、サブクラスを全部探すのも面倒ですし、ファイルを開いてまわる時点で面倒です。ああなんて面倒。

継承とオブジェクトの仕組みはある種のプログラミングを楽にしましたが、一方でad-hocに操作を追加できないことが問題になることもあります。isKindOf:やキャストのような仕組みでこの問題は回避できますが、一方で安全性が失われます。実行時の型検査によって危険性は押さえられていますが、もっと型システムを活用したい場面はたくさんあります。このようなad-hocに操作を追加したいようなデータ型には、enumのようなアブローチが向いています。

さて、enumとclassのどちらを使えば良いかはかなり明らかになりました。

  • caseは増えなさそうだけど、操作が増えそうな場合はenumを使う
  • 操作は現時点でだいたいわかっているけど、caseが増えそうな場合はclassを使う

簡単ですね。

ちなみに個々の型についてenumとclassのどちらを選ぶかですが、僕の場合は、ADTの方が先に頭にあるのでまずはenumで書けないか考えることになります。それで

  1. 状態を変更する方が楽に実装できそうな予感がある
  2. 継承して実装を使い回したい
  3. 関連する操作が(直感的に)すでにわかっている

など、enumで書くのは微妙な感じがする場合には、classにします。

Expression Problem

enumとclassの違いは、関数型プログラミング言語とオブジェクト指向プログラミング言語の違いとして考えることができます。この問題はExpression Problemという名前で昔々に議論されていたりします。

WikipediaからリンクされているWadlerのメール(論文*2)には、次のような文章があります。

In a functional language, the rows are fixed (cases in a datatype declaration) but it is easy to add new columns (functions). In an object-oriented language, the columns are fixed (methods in a class declaration) but it is easy to add new rows (subclasses).

http://homepages.inf.ed.ac.uk/wadler/papers/expression/expression.txt

ここで行 (row) と列 (column) は、それぞれデータの種類と操作を指しています。和訳。

関数型ブログラミング言語では、行は固定されている(datatype宣言のcase)が列を足すのは簡単(関数)。 オブジェクト指向プログラミング言語では、列は固定されているが(クラス宣言に含まれるメソッド)行を足すのは簡単(サブクラス)。

さて、「それでは両方とも足せるようにするのはどうしたら良いでしょうか?」というのが、Expression Problemです。回答はいくつかあって、

  • OCamlではPolymorphic Variantという「すごいenum」を用意して解決した
  • Scalaもなんかうまいこと解決した(上のWadlerの論文は「俺の考えたGJならExpression Problemが良い感じに解ける」というものなので、きっと関連があるんだろうと思っていますが、Scalaはよく知らないのでわかりません)
  • Haskellもなんかできるらしい(よく知りません)

とかです。

ちなみに「既存のクラス階層にメソッドを追加しようとするとちょう大変」という側面を見れば、SwiftのExtensionやObjective-CのCategory、C#のExtension Methodsといった機能によって、ある程度は解決される問題でもあります*3GoFで言えばVisitorパターンがこの問題に取り組むものですね(キャストが必要なので型安全ではない)。

*1:というと、少しニュアンスが違うかもしれなくて、enumがimmutableなのは常識的な設計なんだけど、でも別にmutableであっていけないわけでもない。

*2:URLにpaperって入っている。

*3:でも再帰的なものを考えるとあまり上手くいかない。