猫野詩梨帳

かわいいはかしこい

Haskell と Expression problem とかのメモ

※ この記事はメモみたいなものなので, めっちゃ適当なことを書いているかもしれない上に話がまとまっていないので注意してください.

先日こういうツイートをしました.

抽象化による switch の置き換え

最近「オブジェクト指向のこころ」とかを読んで,「抽象化によって switch を置き換える」という手法があることを知りました.

オブジェクト指向のこころ (SOFTWARE PATTERNS SERIES)

オブジェクト指向のこころ (SOFTWARE PATTERNS SERIES)

例えば,A, B, C の3つの実装があり,実装によって処理を切り替えなければならない場合,条件分岐で

switch (IMPL)
  case A:
     ...
  case B:
     ...
  case C:
     ...

みたいに書けると思います.

ただ,プログラムの各所にこういった switch が現れてくるとだんだん保守が大変になっていきます. 例えば実装 D が増えた場合,こういった switch 全てを見つけ出して case D を追加しなければならなくなります.

これを解決するためにオブジェクトを用いることができます.即ち処理部分を

Impl impl = ImplA()

impl.process()

みたいにしておきます.すると,新たな実装 D が追加されたとしても,ImplAImplD に切り替えるだけでよくなります. もちろん impl は持ち回すかグローバルに取得するか等しないといけないのですが,呼び出し側 (impl.process()) は変更せずに済みます. これにより修正がちょっと楽になります.

Haskell の data におけるパターンマッチ分散問題

ここで,Haskell のパターンマッチでも似たような状況になることを思い出しました.

例えば

data MyData = MyDataA | MyDataB

みたいな data に対してパターンマッチするような処理がいくつかあるとします.

このとき MyDataC が追加されたとしたら,パターンマッチを行っている処理全てを見つけ出して修正する必要が発生します.

もしかしてこれもなんとかして解決できるんじゃろか? というのが上述の一連のツイートの発端です.

型クラスを使用した抽象化によるパターンマッチの置き換え

オブジェクト指向のクラスやインターフェース(というか Java とかの interface)は Haskell の型クラスと比較されることがあります. 直感的にはこれらは「近そう」な感じはしますが,たぶんこれらはどちらも抽象化を実現するものなんだと思います.

どちらも抽象化を実現するものであるならば,例えば先程の「A, B, C の3つの実装」の話については, 「型クラスを使用した抽象化によりパターンマッチを置き換える」こともできそうに思えます.

実際に,

class Impl a where
  process :: a -> ...

data ImplA = ImplA ...

instance Impl ImplA where
  process (ImplA ...) = ...

とすることもできそうです.これはなんとなく「抽象化による switch の置き換え」に近い気がします.

data ふたたび

ここで,逆に Haskell の data はそれ以外の言語での何にあたるのか? という疑問も生じます.

Haskell の data はデータ型を定義するものですが,特に代数的データ型と呼ばれるデータ型を定義することができます.

例えば

data MyData = MyDataA | MyDataB

みたいなのは列挙型と呼ばれるようです.

qiita.com

感じとしては「data に対するパターンマッチ」は「enum(例えば JavaEnum)に対する switch」に近いものなのかもしれません.

さて,上述のツイート中にもありますが, 以下のサイトでは「列挙型とswitch文を使ったソースコードは、ポリモーフィズムを使って書き直すべき典型的な悪い例」という記述が見受けられ ,またその上で列挙型を使用したほうが良い場合もあるとも述べられています.

www.aerith.net

ここで注目したいのは,インターフェースと実装を用いて書いたものを列挙型でも書くことができる場合があり(そしておそらくその逆も), それぞれにメリットデメリットがあるということ(,また Java の列挙型ではある程度ポリモーフィズムを達成することができるということ)です.

インターフェースと実装 列挙型
利点 swtich が減る クラス数が増えない
欠点 クラス数が増える switch が増える (JavaEnum では回避可能)

Haskell においても,先程の「型クラスを使用した抽象化によりパターンマッチを置き換える」の例を

data Impl = ImplA ... | ImplB ...

としてパターンマッチで書き換えることが出来そうに思われます.

しかし,この辺でちょっと混乱してきました. 今まで data と class は明らかに異質なもので,それぞれ使いどころがはっきりしていると思っていたからです. 特に class はインターフェースに似ているとはいえ,同じような使い方が出来るかどうかも疑問です.

data と class の両方で(うまく)書けるような場合が本当に出現するのでしょうか? そして,その場合はどちらを採用するべきなのでしょうか?

これに関して明確な答えはまだ出ていないのですが, data を class で書き換えるという方法はある程度うまく行くようです. これについては後述します.

意見とか

最初のツイートを再掲します.

これに関していろいろなコメントがあったのでいくつか引用させて頂きます.

既存のコードに修正が入ってしまうのはある程度仕方ないという意見です.

これは尤もだと思います. 結局の所真の目的は switch(パターンマッチ)を潰すことではなくて,コードのメンテナンスコストを下げることだからです.

型クラスを使う方法もあるということで,これは先述した data と class の書き換えの可能性を示唆していると思われます. また,いつ data を使うべきかについては,どのくらい流動的(でない)かにもよるという意見もありました.

ほか,次のようなコメントもありました.

Expression problem (The Expression Problem) とは,既存のコードをいじらずに,データ型に新しいケースを追加するにはどうしたらいいかという問題らしいです. つまり,正に今考えている問題のことです.ちゃんと名前がついていたんですね.

Expression problem については以下の記事が詳しいみたいです. 名前がわかると検索ができるので,名前は大事ですね.

maoe.hatenadiary.jp

この中で(正確にはこの中で紹介されている動画の中で),データを追加するという問題を型クラスで解決する方法が述べられています. これはまさに先程の data を class で書き換えるということだと思います.

ということで,結論としては上の記事を読んでくださいということになってしまいました.

Data Types a la Carte や Tagless final 等については知らなかったので,また今度調べてみたいと思います.

機会があれば続く.

余談

ウェアハウス川崎が閉店するらしいです.悲しい.

nlab.itmedia.co.jp