Clojureを使った関数型プログラミングデザインのメモ

2016-12-29 / [clojure]

はじめに

こまごましたツールを作るにもとても時間がかかってしまう。

デザインで悩み過ぎるからだ。 どうした方がいいのか悩みながら行きつ戻りつする。それで時間がかかってしまう。

主に解決したい問題をうまくエンコードしたプログラムの構造を発見するのに時間がかかっている。

下記はPythonで書いた時の失敗だが、概ねどれでも同じだろう。

  1. Classの責任がブレる
  2. 副作用の実行タイミングがコードに偏在する

こうした問題をどう解決すればよいだろうか。

Haskellであれば型によって問題を記述するところから始まる。 もう少し一般化したい。関数型プログラミングを念頭に置いて、プログラムをどうデザインするかを整理したい。

ここでは諸事情によりHaskellではなくLisp方言であるClojureを用いてデザインしていくことを検討したい。

なお、この検討の結果、良いデザインが出てこない可能性もあるが、とりあえず進めてみる

方針・考え方

関数型プログラミングとフィットしそうなプログラムの青写真というと、 下記のような考え方がポイントになってくると考えている。

  1. 副作用の実行は一箇所にまとめて、それ以外を純粋に保つ
  2. 状態の更新は状態を受け取り状態を返す ムーアマシン のような形式で表現する

やってみて

まるでコンパイラの各ステージみたいになった。面白い。

  1. parser
  2. AST transformer
  3. code generator

だいたいこの3ステージで構成される。

1. parser
low level data  +--------------------+ AST and state
--------------> | parse and make AST | ------------>
                +--------------------+

2. AST transformer
AST and state   +----------------------------+ operational commands
--------------> | process and translate      | ------------>
                | into operational semantics | 
                +----------------------------+

3. code generator
operational commands +------------------------+ low level data
-------------------> | execute operation      | ------------>
                     | and emit side-effect   | 
                     +------------------------+

Pythonと比べて

Pythonでは「あーこれは関数にするのかインスタンスメソッドにするのか」といった 悩みどころが発生して時間がかかるところが多かったがClojureではそれがなかった。

関数とデータというごくシンプルな概念だけの導入で済んだからだ。 この点だけとっても個人的に関数型プログラミングに慣れてきつつある自分には大きいメリットだった。

Clojure特有の良さ

全てのデータが不変でかつ大体シーケンスとみなせる。 そのためシーケンスに対する関数さえ覚えていればやりたいことができてしまう。

これは学習曲線初期の投資が後でとても活きてくることを意味している。

Clojure特有の辛さ

良さと表裏一体だけれども、静的型付けがないので データの更新方法を間違えてもその間違ったデータのまま 後続にパスされてしまってバグ発生箇所の切り分けに苦労した。

どこでデータが破綻したか追うのが難しい。

Clojureプログラマはこれをどうやって捉えているのだろう。

関数型プログラミングの良さ

関数型プログラミングでは状態のmutationが原則的に使えない。 そのため多くのインタフェースが 「状態を受け取って更新された状態を返す」 ようになる。

あとはこれらのインタフェースを持つ関数を合成して一本の大きな データフロー を作ることができる。

データフローの記述にはClojureではthreading macro ->->> があるので 流れ自体はぐっと読みやすくなる。

残った課題

  1. エラーハンドリングができていない
  2. テストコードを充分かけなかった(TDDにはできなかった)

エラーハンドリング

データフローを構成できるのはいいのだが、 失敗を伝播させたり途中でabortさせたり するような コードの一般的な記法を発見するまでには至らなかった。

HaskellであればMonadとしてエラーを文脈化して伝搬できるのだが、 Clojureではそうした文脈の抽象化方法がいまいち解らない。

テストコード

最初の方でスパイクしている時のみ少し書いたが、 ある程度プログラムの見通しが決まってくると不要になった。

つくづく自分はテストを 「デザインの探索」 方法の一部としか捉えていないことが解った。

なぜテストを書かなかったのか

テストを書くモチベーションの低下を起こす理由として思い当たるのが、 テスト対象のインタフェースが大きすぎる ということ。

インタフェースが大きいと、それに必要な入力データの構造が複雑になり準備が増える。 これが面倒になって書かなくなっていることがよくあったと感じる。 こういう場合どうしていくのがいいのだろうか?

  1. インタフェースを単純にする
  2. より小さい部分からテストする

より小さな部分からテストしていくのが良さそうだ、というのは理屈としては解る。