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

Creatable a => a -> IO b

Haskellと数学とちょびっと音楽

Haskellerがふか〜いネストと戦う話

書きたいネタは色々あるのですが、どれもこれもやたら重くてなかなか筆が(キーボードが)進まないので、
今日は軽く、仕事で思いつきで作ってみたものの話をします。

モダンな言語を使ったシャレオツな設計に慣れてしまうと、なんというか、 下請けプログラマ特有のあのなんとも言えないコードを読むのが苦痛に感じてしまうものです。

ましてやHaskellerにとって、5000行のクラスの中の600行のメソッドだとか、その中のループと分岐と例外処理が入り乱れた11重ネストはもう、 苦行以外の何物でもなく、「なんなの?マジなんなのこれ?罰なの?Sガストの竜田揚げに備え付けのポン酢ではなく、 醤油と七味唐辛子をかけて食べた罪に対する天罰なの?」と、意味もなく天を仰いで祈りを捧げたり捧げなかったりとかするわけです。

えー・・・つまりですね、

こんなん読めるかボケェ(╯°□°)╯︵ ┻━┻

と、こういう事が言いたいわけです。はい。

プログラマがコードを読めないというのは色々とアレなアレがアレとは申しますが、とにかく辛いのでできれば読みたく無いのです、が、 そんな事言っているうちに障害管理表には項目が増えていく一方なので、とりあえずなんかこう、なんか、どうにかしようと思ったわけですよ、はい。

何がどーなれば良いのか

結局の所、いわゆるスパゲティだかラーメンだかと呼ばれているアレの問題は、全体の構造がまったくつかめない事にあるので、 とにかく、ネストだとか関数呼び出しだとかの階層の地図を描きたいわけです。 勿論、世の中にはそういうのを上手いことやってくれちゃう凄いソフトがあったりもするのかもしれませんが、 自分の周りに使ってる人とか居ないし、ちょっとググった感じだと見つけられなかったので、結局自力でなんとかする事にします。

感覚としては、自然言語混じりの擬似言語のようなもので、プログラムの何処で何をやっているか、 という事をメモ取りながら読み進めていくとわりと整理しやすいのですが、スケールや目的によって欲しい粒度だとかが全然違う事になったり、 同じコードのメモを何度も取るハメになったり、そもそも長期的な記録としては役に立たないとか色々と悩ましい問題があったりします。

そういったのを直感的に記述していって、かつ自在にスケールしたり、必要な情報だけ抽出できるようにしたいワケです。 プログラムでプログラムの情報を柔軟に扱うためには、構文木そのものをデータとして保持するのが良いのですが、 大抵の言語の場合、木のデータを作ってもそれを構築するための手段が無かったりするのですよね。

構文木と言えばFree

木のような代数的な構造を扱う問題に対して、Haskellはかなり優秀です。

今回のように、インタプリタコンパイラを作るわけではないけどとにかく「構文木」が欲しい場合、Freeモナドが便利です。 (Operationalというヤバイモナドもありますが、 構文木そのものを柔軟に扱うのが目的なので操作しやすいFreeを使います。)

早速基礎となるデータ構造を作り、モナドとして使えるようにしてやりましょう。

data PTreeData a
  = PSummary String a
  | PIf String (PTree ()) a
  | PLoop String (PTree ()) a
  deriving (Show, Read, Eq, Ord, Functor)

type PTree a = Free PTreeData a

psum :: String -> PTree ()
psum s = liftF $ PSummary s ()

pif :: String -> PTree () -> PTree ()
pif s t = liftF $ PIf s t ()

ploop :: String -> PTree () -> PTree ()
ploop s t = liftF $ PLoop s t ()

Freeモナドにすれば、後はdo記法で普通に手続きプログラムのように書いていけば良いだけなので、 レガシーを元に、メモを取る感覚で要約しながら写経すれば良いだけです。簡単簡単。

ためしに、次のようなデータを作って、ghciで構文木が組み立てられている事を確認してみましょう。
(プログラムに意味は無いです)

test :: PTree ()
test = do
  psum "初期化処理"
  pif "データAが0件でない場合" $ do
    ploop "データA全件走査" $ do
      pif "データBが0件でない場合" $ do
        ploop "データB全件走査" $ do
          pif "データAとデータBが等しい場合" $ do
            psum "データCにデータAとデータBのリンク情報を追加する"
      pif "データBが0件の場合" $ do
        psum "データAののループを抜ける"
  ploop "データC全件走査" $ do
    psum "データCの情報を画面に出力する"
  psum "終了処理"
*Main> test
Free (PSummary "\21021\26399\21270\20966\29702" (Free (PIf "\12487\12540\12479A\12364\&0\20214\12391\12394\12356\22580\21512" (Free (PLoop "\12487\12540\12479A\20840\20214\36208\26619" (Free (PIf "\12487\12540\12479B\12364\&0\20214\12391\12394\12356\22580\21512" (Free (PLoop "\12487\12540\12479B\20840\20214\36208\26619" (Free (PIf "\12487\12540\12479A\12392\12487\12540\12479B\12364\31561\12375\12356\22580\21512" (Free (PSummary "\12487\12540\12479C\12395\12487\12540\12479A\12392\12487\12540\12479B\12398\12522\12531\12463\24773\22577\12434\36861\21152\12377\12427" (Pure ()))) (Pure ()))) (Pure ()))) (Free (PIf "\12487\12540\12479B\12364\&0\20214\12398\22580\21512" (Free (PSummary "\12487\12540\12479A\12398\12398\12523\12540\12503\12434\25244\12369\12427" (Pure ()))) (Pure ()))))) (Pure ()))) (Free (PLoop "\12487\12540\12479C\20840\20214\36208\26619" (Free (PSummary "\12487\12540\12479C\12398\24773\22577\12434\30011\38754\12395\20986\21147\12377\12427" (Pure ()))) (Free (PSummary "\32066\20102\20966\29702" (Pure ()))))))))

一行にダラダラダラーと出力されてしまい、しかも文字列が残念な感じになってしまうのでとても解りづらいですが、 ちゃんとtest関数を元に擬似プログラムのデータができていそうです。

出力処理を作る

実際に使う際にはは、呼び出し範囲の広い変数の宣言や使用箇所を記述したり、例外処理を記述できるようにしたり、メソッド呼び出し等の構文を追加すると良いでしょう。

これで、例外を除いた単純の処理構造を抽出したり、ループだけ抜き出してオーダー計算に活用したりと、プログラムから好きに操作できるようになりました。
問題は、このままではせっかく整えたデータが恐ろしく読みづらい(というか読めない)という事です。

ので、綺麗に整えて表示する関数をちゃちゃっと作ってやります。

printPTree :: PTree () -> IO ()
printPTree = mapM_ putStrLn . layoutPTree 0
    
layoutPTree :: Int -> PTree () ->  [String]
layoutPTree c  (Pure ()) =  []
layoutPTree c (Free (PSummary s n)) = (makeIndent c ++ "// " ++ s) : layoutPTree c n 
layoutPTree c (Free (PIf s t n)) =  layoutPTree' c "If" s t n
layoutPTree c (Free (PLoop s t n)) =  layoutPTree' c "Loop" s t n

layoutPTree' :: Int -> String -> String -> PTree () -> PTree () -> [String]
layoutPTree' c s1 s2 t n 
  = concat [makeIndent c, s1, " `", s2, "'"] : layoutPTree (c + 1) t ++ layoutPTree c n

makeIndent :: Int -> String
makeIndent = flip replicate ' ' . (4*)

んで、出来上がった構文木printPTreeに渡してやれば、綺麗に整えられて出力される。とゆーわけ。ね、簡単でしょ?

*Main> printPTree test
// 初期化処理
If `データAが0件でない場合'
    Loop `データA全件走査'
        If `データBが0件でない場合'
            Loop `データB全件走査'
                If `データAとデータBが等しい場合'
                    // データCにデータAとデータBのリンク情報を追加する
        If `データBが0件の場合'
            // データAののループを抜ける
Loop `データC全件走査'
    // データCの情報を画面に出力する
// 終了処理

終わりに

これって、二重管理じゃね?って言われると返す言葉無いわけですよ(´・ω・`)
本当はこんな事しなくても良いように作り変えられれば良いのですが、既に稼働している大規模システムだとそーもいかず・・・

どうせ負の資産を増やすならばなるべく(自分が)扱いやすい形で、という事を考えたら結果こうなりました。

あ、もしレガシーコード(C#ダヨー)の中を自由に泳ぎ回れるようなすんごいツールあったら教えてくらはい。多分喜びのあまり泣きます。