読者です 読者をやめる 読者になる 読者になる

Hash λ Bye

Haskell, Clojureなどの話

Haskellerの好きなところ

僕はHaskellが好き。

 

なんだけど、同じくらいHaskellもくもく会の常連の人達が好き。

 

ちょっとした型についての質問から、いつの間にかホワイトボード上で証明とか書き出して「あー、ほんとだー、そういうことかー」って言ってる感じが好き。

 

解らないことがあったら自分で検証するっていう、問題に対する誠実で真っ直ぐな態度を持っている人達が好き。

 

Haskellではそういう解らないところを形式的に検証する方法や知識がよく整備されていると思う。(とても難しいものもあるけど)

 

きっと他のコミュニティにもそういう人はたくさんいる。でもHaskellもくもく会にはそういう人達が間違いなくいるのを僕は知っている。

なのであのコミュニティが好きだし、そこにくる人達を尊敬している。

 

僕も真摯に問題と向き合って、reasonableなHaskellの良さを活かしてもの作りができるようになりたい。

Haskellの代数データ型をJava的なインタフェースと捉える

Haskellの代数データ型は僕にとってJavaのインタフェースに近い。

データ型がインタフェースでそのデータを受け取る関数がインタフェースのメソッドに相当する。 データをパターンマッチで分解して値コンストラクタ別の関数定義をするのは、 インタフェースに対する実装を与えているものだと考えている。

data Maybe a

mapMaybe :: (a -> b) -> Maybe a -> Maybe b

Maybe aというデータ型があるとする。これがインターフェース。

mapMaybeという関数があるとする。これがメソッド。

インタフェースに実装を与えていく。

まずはデータ構造としてのインタフェース実装を与える。

data Maybe a = Just a | Nothing

mapMaybe :: (a -> b) -> Maybe a -> Maybe b

ここでは Just aNothing という2種類の実装が Maybe a というインタフェースに対して与えられた。

次に関数としてのインタフェース実装を与える。

data Maybe a = Just a | Nothing

mapMaybe :: (a -> b) -> Maybe a -> Maybe b
mapMaybe f (Just x) = Just (f x)
mapMaybe f Nothing = Nothing

mapMaybe の具体的な定義が各インタフェース実装ごとに記述できた。

mapMaybe のユーザから見れば、 Maybe a の実態が Just a なのか Nothing なのかは意識する必要がない。 これが抽象化されているということ。

オブジェクト指向言語ではvirtualmethodなどを定義して、実行時に実装へと ディスパッチ する。 それと同じように上記の例ではパターンマッチを使って値コンストラクタを判定し、 ディスパッチ している。

この考え方で割りと上手くいく。ライブラリでも作らない限りはこの考え方で多くのことが抽象化できる。 しかしユーザが拡張可能なインタフェースを定義しようと思うと、問題もあったりする。

Expression Problem

Maybe a の値コンストラクタを増やした、つまり Maybe a インタフェースに新しい実装を与えたときに、 Maybe a にパターンマッチをかけている関数も修正しなければならくなるし、再コンパイルが必要になってしまう、という問題。

これを解くために型クラスを使ったインタフェースの定義をしていくことになる。

で、tagless finalってなんだろうというところが今。

Clojureでの副作用の表現について

自分が悩んでることを拙い語彙でぽつぽつ綴るので、誰にも伝わらないと思う。

DBアクセスする機能を実装したいとする。

java.jdbc を使ってDBアクセスレイヤを実装することになるのが普通かなと思う。 Clojure関数型プログラミング言語なので、組み立てに使う部品(関数)はできるだけ純粋な方がいい。 DBからのデータ取得やデータ更新を行う手続きもできるだけ副作用のない関数で組み立てたい。

DBアクセスする手続きなのにそんなことできるのか? と思う。

Haskellのように独自ASTとinterpreterのパターンならそれができる。 Clojureでも同じような構成になるのだろうか。

いくつかアプローチがありそうだ。

  1. 副作用を起こす関数は高階関数で受け取る
  2. 副作用を起こすコマンドを生成する純粋関数を合成し、interpreterで副作用を発行する

基本的には高階関数で解けるのが一番低コストでよいと思う。 高階関数を使ったDIだと捉えるとわかりやすい。

ただ適用する関数が色々な種類の副作用関数を要求するようになると突然複雑になる。DIで言えば注入する依存が肥大化している状態。 気軽に依存が満たせなくなるので、とても腰の重いインタフェースができる。

それを補うために関数の辞書を渡すことになってしまう。しかしインタフェースが抱える複雑さは変わらないので、実はあまり解決になってない。

2案

あるパラメータを渡すと「印字せよ」とか「INSERT文を発行せよ」という命令を返す関数を中心に組み合わせる。 命令は実行されるまで何の副作用も生まない。なので、その関数を呼び出しても、何か副作用が起きるわけではない。 実行すれば副作用が起こる命令が手元にあるだけだ。

これはDIとは完全に切れ離された、より純粋な設計になる。 しかし一方で、命令を実行する別の機能が必要になるため複雑さはそちらになお宿っている。

が、ビジネスドメインの記述に徹することができるのは保守上のメリットがある。

対して、命令と実行を受け持つ関数はなかなか軽くもシンプルにもならないだろうと思う。

それでもドメインを純粋に保てるのなら2案になるのかな。

設計プランは後ほど追記 - 4/23 まだできてない Clojureで遊んでいる場合ではない感じに。。。