「型から始めるプログラミング」の編集履歴(バックアップ)一覧に戻る

型から始めるプログラミング - (2013/12/16 (月) 00:31:36) のソース

*型から始めるプログラミング

この記事は「[[F# Advent Calendar 2013>http://connpass.com/event/3935/]]」6日目の記事です。


**型に導かれて

 静的な強い型を持った関数型プログラミングにある程度馴染んでくると、次第に「コンパイルを通ったものは大抵動く」という不思議な感覚を持つようになります。この感じは手続き型プログラミングしかやったことがない人にはなかなか伝わらないもので、そんな話をしても大体「そんなわけあるわけないwww」というような反応を受け、こちらもうまく伝えられずモゾモゾすることになりがちです。

 もちろん「コンパイル通ればできてる」というのが正しくない、というのは分かりきったことなのですが、「コンパイルを通った時点で動いたものに何の問題もなかった」という事象の確率がかなり高いのもまた統計的なデータこそないものの皆様肌で感じているのではないかと思います。

 こういう「コンパイル通ったと思ったら動いた」というプログラムは、僕の経験では「大がかりな型」を想定することからスタートすることで持たされることが多い気がします。
 
 今回は課題として、CSVを読みこみいくつかのチェックと変換をして別のファイルに書きだすプログラムを書きながら、「大がかりな型」からプログラムを作っていく過程を見ていただきましょう。

**作るもの
 作ってみるプログラムの機能はこんなものです。

+ CSVから一行を読みこみ、カンマで区切って前後のダブルクォーテーションや空白を削除し、文字列の配列を作る
+ 配列の特定の項目について、
-空白ならば0とする
-特定の項目間で何らかの計算をする
-条件を満たしていない場合は例外を出す(本当はログを出した方がいいのですが、今回の主旨からはずれるので)
+結果として出来上がったデータをCSVとして保存する。

 手続き型プログラミングでも普通に作れそうなものですが、ここはF#なりに、「大がかりな型」をベースにプログラミングをしていくことにします。

*ファイルの読み込みとCSV展開

 F#では、行データのシーケンスとしてファイルを扱うことが一般的です。
 
 let getCsvDataFromFile (filename : string) = 
   seq {
     use sr = new System.IO.StreamReader(filename, System.Text.Encoding.GetEncoding("shift_jis"))
     sr.ReadLine() |> ignore
     while not sr.EndOfStream do
       let oneLine = sr.ReadLine()
       yield oneLine.Split(',') |> Array.map (fun (v : string) -> v.Trim(' ').Trim('"'))
   }

 ファイルを開き、先頭行を読み飛ばしたら後はファイルの終端まで行ごとに読みこみ、それをカンマで区切って文字列の配列とし、要素ごとに前後のスペースとダブルクォーテーションを除いたものを各要素とするシーケンスが作成されました。ここまでは一般的な実装だと思います。細かいことを言うと、要素の中にカンマが入っていることを想定する必要がある場合にはこの実装はNGです。その場合はもっとまじめにパーサを書く必要があります。

*チェック関数を書く

 次に、実際にチェックを行う関数を書いていきましょう。そんな末端から・・・という気もするかもしれませんが、F#は定義が先に書かれていない関数を呼び出すことができません。中身はなくとも関数の引数と戻り値の型まではここで決めて大枠だけでも書いておく必要があります。

 まずは、ある項目の中身がなかった場合は、それを"0"として与える関数を想定します。
 チェック対象の項目の値が渡されたら、空白かどうか調べてその場合はゼロを返します。

 let null2Zero value = 
   if value = "" then "0" else value

 簡単ですね。とりあえず先を急ぎましょう。

 次には、二つの項目の値を足した値を自分の項目とする関数を考えます。足し合わせる項目は二つのインデックス番号で与えられるものとします。数値でなければ例外を投げるようにしましょう(手抜きですが)。
 let adder i1 i2 (ar : string []) =
   let val1 = Int32.TryParse(ar.[i1])
   let val2 = Int32.TryParse(ar.[i2])
   match val1, val2 with
   | (false, _), _ -> failwithf "数値に変換できない値があります(%s)" (ar.[i1])
   | _, (false, _) -> failwithf "数値に変換できない値があります(%s)" (ar.[i2])
   | (_, v1), (_,v2) -> string (v1 + v2)

 こういう関数は通常テストファーストで書きます。先にテストを書いてから関数を書くのです。末端の関数はテストが書きやすいこともありますが、何よりこういうプログラムではバグが出るとすれば大抵このような末端の関数だからなのです。ちなみにインデックスの範囲チェックなども本当は必要です(割愛しますが)。

 最後に条件を満たしていない場合は例外を出す関数を書いてみましょう。
 
 let checker (f : string -> string[] -> string option) value (ar : string []) =
   match f value ar with
   | Some err -> failwith err
   | None -> value

 関数を引数にとって、その関数に項目の値と行全体を渡したら、エラーの場合Some にエラーを包んで返してくれる関数です。

と、ここまで作ってきて、これらの関数は行を処理する関数から共通インターフェースで呼ばれる必要があるので、引数をそろえましょう。とは言っても揃えるのは戻り値の型と、引数の最後のいくつかだけです。項目の値と行全体の値の療法がないと処理できない関数があるため、すべての関数でそれを引数とするように調整します。不要なものは(_)アンダーバーで受けるようにするのです。

 let null2Zero value _ = 
   if value = "" then "0" else value
 
 let adder i1 i2 _ (ar : string []) =
   let val1 = Int32.TryParse(ar.[i1])
   let val2 = Int32.TryParse(ar.[i2])
   match val1, val2 with
   | (false, _), _ -> failwithf "数値に変換できない値があります(%s)" (ar.[i1])
   | _, (false, _) -> failwithf "数値に変換できない値があります(%s)" (ar.[i2])
   | (_, v1), (_,v2) -> string (v1 + v2)
 
 let checker (f : string -> string[] -> string option) value (ar : string []) =
   match f value ar with
   | Some err -> failwith err
   | None -> value

 これでチェック関数はとりあえず揃いました。実際に仕事で使う場合は、このような関数を必要なだけ追加していくことになります。
 
*チェック内容をリストとして記述する