Haskell と Expression problem とかのメモ
※ この記事はメモみたいなものなので, めっちゃ適当なことを書いているかもしれない上に話がまとまっていないので注意してください.
先日こういうツイートをしました.
Haskell で A | B みたいな data にあとから C を追加したりするとコードの既存箇所に修正が必要になると思うんだけど,コードの既存箇所に修正を加えることなくこういう感じの追加を実現するときってどうするんだろう.追加の虞がある箇所に data を使うのがよくない?
— バーチャル猫野詩梨 (@CyLomw) October 14, 2019
オブジェクト指向だと分岐 (switch) の代わりに実装を切り替えることで switch(つまり変更箇所)が散らばるのを防ぐみたいなことがあって,そういえば Haskell で data 使うときもパターンマッチが散らばることがあるなと思ったのですが,
— バーチャル猫野詩梨 (@CyLomw) October 14, 2019
インターフェースに対応するのは型クラスだから型クラス使えばいいのかなと思ったけどそもそも data ってなんだ……? みたいになってよくわからなくなりました(終わり
— バーチャル猫野詩梨 (@CyLomw) October 14, 2019
列挙型と似たところがあるよなと思って調べていたらこんなのを見つけました.列挙型を使うと switch が増えるみたいなのはやっぱり言われてる?らしいですねhttps://t.co/Tu82dEwOj9
— バーチャル猫野詩梨 (@CyLomw) October 14, 2019
余談ですがこの話はふと将棋のコマをクラスで書いたらどうなるのかなと思って,そういえば Haskell だと data でも書けるのかと思ったところから始まりました.書くべきなのかはわからない.
— バーチャル猫野詩梨 (@CyLomw) October 14, 2019
抽象化による switch の置き換え
最近「オブジェクト指向のこころ」とかを読んで,「抽象化によって switch を置き換える」という手法があることを知りました.
オブジェクト指向のこころ (SOFTWARE PATTERNS SERIES)
- 作者: アラン・シャロウェイ,ジェームズ・R・トロット,村上雅章
- 出版社/メーカー: 丸善出版
- 発売日: 2014/03/11
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (6件) を見る
例えば,A, B, C の3つの実装があり,実装によって処理を切り替えなければならない場合,条件分岐で
switch (IMPL) case A: ... case B: ... case C: ...
みたいに書けると思います.
ただ,プログラムの各所にこういった switch が現れてくるとだんだん保守が大変になっていきます.
例えば実装 D が増えた場合,こういった switch 全てを見つけ出して case D
を追加しなければならなくなります.
これを解決するためにオブジェクトを用いることができます.即ち処理部分を
Impl impl = ImplA() impl.process()
みたいにしておきます.すると,新たな実装 D が追加されたとしても,ImplA
を ImplD
に切り替えるだけでよくなります.
もちろん 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
みたいなのは列挙型と呼ばれるようです.
感じとしては「data に対するパターンマッチ」は「enum(例えば Java の Enum)に対する switch」に近いものなのかもしれません.
さて,上述のツイート中にもありますが, 以下のサイトでは「列挙型とswitch文を使ったソースコードは、ポリモーフィズムを使って書き直すべき典型的な悪い例」という記述が見受けられ ,またその上で列挙型を使用したほうが良い場合もあるとも述べられています.
ここで注目したいのは,インターフェースと実装を用いて書いたものを列挙型でも書くことができる場合があり(そしておそらくその逆も), それぞれにメリットデメリットがあるということ(,また Java の列挙型ではある程度ポリモーフィズムを達成することができるということ)です.
インターフェースと実装 | 列挙型 | |
---|---|---|
利点 | swtich が減る | クラス数が増えない |
欠点 | クラス数が増える | switch が増える (Java の Enum では回避可能) |
Haskell においても,先程の「型クラスを使用した抽象化によりパターンマッチを置き換える」の例を
data Impl = ImplA ... | ImplB ...
としてパターンマッチで書き換えることが出来そうに思われます.
しかし,この辺でちょっと混乱してきました. 今まで data と class は明らかに異質なもので,それぞれ使いどころがはっきりしていると思っていたからです. 特に class はインターフェースに似ているとはいえ,同じような使い方が出来るかどうかも疑問です.
data と class の両方で(うまく)書けるような場合が本当に出現するのでしょうか? そして,その場合はどちらを採用するべきなのでしょうか?
これに関して明確な答えはまだ出ていないのですが, data を class で書き換えるという方法はある程度うまく行くようです. これについては後述します.
意見とか
最初のツイートを再掲します.
Haskell で A | B みたいな data にあとから C を追加したりするとコードの既存箇所に修正が必要になると思うんだけど,コードの既存箇所に修正を加えることなくこういう感じの追加を実現するときってどうするんだろう.追加の虞がある箇所に data を使うのがよくない?
— バーチャル猫野詩梨 (@CyLomw) October 14, 2019
これに関していろいろなコメントがあったのでいくつか引用させて頂きます.
データに変更があるときに既存のコードも修正する必要があるのは仕方ないと思ってます。修正漏れさえ検出できるのなら問題ないのでは。どうしてもというなら型クラスを使う方法もありますが、将来の変更なんて予知できないので現実的ではなさそうです
— Cubbit (@cubbit2) October 14, 2019
既存のコードに修正が入ってしまうのはある程度仕方ないという意見です.
これは尤もだと思います. 結局の所真の目的は switch(パターンマッチ)を潰すことではなくて,コードのメンテナンスコストを下げることだからです.
型クラスを使う方法もあるということで,これは先述した data と class の書き換えの可能性を示唆していると思われます. また,いつ data を使うべきかについては,どのくらい流動的(でない)かにもよるという意見もありました.
ほか,次のようなコメントもありました.
Expression problemかな。既存箇所の修正が困難ならData Types a la CarteとかTagless finalが解決策に入ってくる感じかも https://t.co/arMllQo96a
— lotz△ (@lotz84_) October 14, 2019
The Expression Problem の一部なのでHaskell なら Data Types a la Carte ないし Open Union を使えば良いし、OCaml ならPolymorphic Variant 使えば良い気がする
— 益虫 (@e_gracilis) October 15, 2019
Expression problem (The Expression Problem) とは,既存のコードをいじらずに,データ型に新しいケースを追加するにはどうしたらいいかという問題らしいです. つまり,正に今考えている問題のことです.ちゃんと名前がついていたんですね.
Expression problem については以下の記事が詳しいみたいです. 名前がわかると検索ができるので,名前は大事ですね.
この中で(正確にはこの中で紹介されている動画の中で),データを追加するという問題を型クラスで解決する方法が述べられています. これはまさに先程の data を class で書き換えるということだと思います.
ということで,結論としては上の記事を読んでくださいということになってしまいました.
Data Types a la Carte や Tagless final 等については知らなかったので,また今度調べてみたいと思います.
機会があれば続く.
余談
ウェアハウス川崎が閉店するらしいです.悲しい.