(最近自作したライブラリ、Hashlogについて書きます。)
複雑な難しいプログラムを書くのはつらくとも面白いけど、煩雑なプログラムを書くのはつまらないです。
煩雑
事柄がこみいっていてまとまりがつかず,わずらわしいこと(さま)。
例えばこんなコードを読んだことが有ります(実物じゃないですよ)。
// 擬似コードです function calc($value1,$value2,$value3,$flag) { if($value1>0) { if($value1>=5) return 5; elseif($flag===true) return 4; } elseif($value2>0) return 3; elseif($value3>0) return 2; else return 1; }
ある計算をする関数なんですが、if-ifelse、ifのネストで返り値を決めています。
これコード自体は「まぁしゃーない」で済ませられると思います。
しかしこの関数が30箇所にベタ書きで書いてあって、微妙な違い($value5まである/$value2までしかない、$flag2がある)があったらどうでしょう。
さらに30のなかの5個ぐらいはまったく違う引数を取って、別々の数式で返り値を計算している。
ビルの屋上で「F****CK!」と叫びたくなってきます。
if-elseif、ifのネストの何がダメなのか
5個のまったく違うロジックは置いといて、25個の微妙に違うをロジックを共通化できないか、私は考えました。
私の好きな言語はClojureです。Clojureは関数型プログラミングの考え方を取り入れており、そのスタイルに慣れると小さな関数を組合せてコーディングする考え方が身につきます。
「関数的に書き直せば共通化できるんじゃないか」と思って試してみました。
- 入力値($value1、$flag等)を連想配列にまとめる
- 連想配列を受け取って連想配列を返す関数群を定義する。
- その関数を一列に並べて、連想配列をバケツリレー的に渡して更新していく。
こんなイメージでコードを書きました。
なんかしっくりこない。よくよくテストするとうまくいかないケースが出てくる。
上記の試作コードだと条件分岐を書くことが自然にできない。できるけど複雑になりすぎる感じがする。
最初のコードのif-elseif、ifのネストの何がダメなのかよく考えると、その処理フローのなかに状態を隠しているからではないかと思いつきました。
if(X) hoge(); elseif(Y) fuga();
elseif(Y)は条件Yだけでなく条件Xがfalseであるという結果も絡まっている。
試作コードのイメージで連想配列の受け渡しに変換しようとすると、hoge()、fuga()を単体の関数にすることが出来なくて、「状態」という別要素を組み込まないといけなくなる。
「状態かぁ。どんどんシンプルから離れていく感じがする」というのがこれに気づいた時の感想。
Rich Hickeyのスピーチ「シンプルさの必要性」
シンプルさの必要性 | eed3si9n
そんなときにこの記事のことを思い出しました。Rich HickeyさんはClojureの作者です。Clojure/Conjのキーノートスピーチを聞いてもシンプルさ・良いアイデアを考えることについて強い意志をもっていることが読み取れます。
Hammock Driven Development - Rich Hickey - YouTube
Hammock Driven Development Cheat Sheet | Data Sorcery with Clojure
僕たちは、今書いているプログラムと全く同一のものを、劇的にシンプルなものを使って作ることができます。
劇的にシンプルな言語、ツール、テクニック、方法論などです。もっと過激にシンプルなものです。例えば、Ruby と比べてより過激にシンプルなものを使うことができます。何故そうしないのでしょうか?
シンプルさの必要性 | eed3si9n
「最初の試作コード、なんだか難しく考えすぎてたんではないか」。
状態、順序、シンプルさ。いろんな要素が頭のなかでぐつぐつ煮える日々が過ぎました。
(ちなみに計算ロジックのリファクタリングは業務の片手間でやってたので時間はわりかしありました)
すごくシンプルに考えてみよう。入力値を連想配列にするのは良いことだ。けっきょくやってるのは連想配列を書き換えることだ。
私がプログラミングしてて難しいところを考えるときは図を描くのですが、そのとき頭に浮かんでたのはこんな図でした。
連想配列を見て「条件」に合致しているか判定して、指定した「キー」と「値」を更新するようなイメージです。
「これって宣言的に書けるんじゃないか、Prologっぽい感じで」
ちょうどClojureハッカソンであるTokyo.clj#17が11月30日にあったので、そのときに書いてみました。(ちなみに次回は来年1月)
それがHashlogです(HashMap + Prologの造語です)。
Github - deltam/hashlog
Hashlog DSLの書き方
core.cljにcommentでサンプルを書きましたが、Hashlog DSLはこんなかんじです。
マクロは使ってません。ただのvectorとHashMapの組合せです。
(def hashlog-dsl [{:cond (exists :key1) ; 入力HashMapの条件 :key :output ; 値を追加するキー :val (multiply :key1 10) ; キーに対応する値 } ... ])
実行する場合は、DSLと入力HashMapを渡して更新されていくHashMapのシーケンスを取得します。
更新された結果に変化が無かったらそれ以上のシーケンスはdropします(無限ループを避けるため)。
queryを使うとシーケンスのHashMapを検索して最初に追加されたキーの値を返します。
(use 'hashlog.core) (def hs (hash-seq {:key1 100, :key2 200} hashlog-dsl)) (query hs :outpu) ;=> 1000
core.cljのサンプルには、リンゴとオレンジしかない果物屋さんの料金計算の例を書いてみました。値引きクーポンを発行する場合も通常の料金計算に追加するかたちでロジック変更ができます。
(def fruits-price [ {:cond (exists :apple) :key :priceOfApple :val (multiply :apple 100) } {:cond (exists :orange) :key :priceOfOrange :val (multiply :orange 80) } {:cond (every [(exists :priceOfApple) (exists :priceOfOrange)]) :key :price :val (sum [:priceOfApple :priceOfOrange]) } ;; 値引きクーポン {:cond (every [(exists :price) (is :hasCoupon true)]) :key :discountPrice :val (multiply :price 0.9) } {:cond (every [(exists :price) (is :hasCoupon false)]) :key :discountPrice :val (value :price) } ] ) ;; 通常の料金計算 (query (hash-seq {:apple 1, :orange 2, :hasCoupon true} fruits-price) :price) ;=> 260 ;; 値引きクーポン有りの料金計算 (query (hash-seq {:apple 1, :orange 2, :hasCoupon true} fruits-price) :discountPrice) ;=> 234.0
Hashlogは使えるのか?
最初に手を付けたのは2週間ほど前ですが、Githubに上げたのは3時間ぐらい前です。
私は最初の書いた煩雑な計算ロジックをリファクタリングするために使うつもりです。
最初のプログラムがPHPだったのでPHP版も作ってます。実はPHP版を最初に書いたんですが、関数的書き方が使いにくかったので、まずClojureでプロトを作っていろいろ実験してる、という状態です。
Hashlogの仕様、DSLの書き方は今後どうなるか分かりません。でも基本的なアイデアは変わらないと思います。
「HashMapを宣言的に書き換えることで計算ロジックを組み立てる」というアイデアは言語を問わず使えます。
Hashlogはまだ煮詰まってません。なにかアイデアがあったら教えて下さい。
「これってXXXと同じものじゃないの?」みたいなのも教えてくれると助かります。作ってる間、なにかを再発明してるんじゃないかという思いは常にあったので。
お気軽に@deltamまで話しかけてくださーい。
次回、Clojure Advent Calendar 15日目はathosさんです。
0 件のコメント:
コメントを投稿