鉄道指向でこころがぴょんぴょんする解説

前回鉄道指向でこころぴょんぴょんする記事を書いたところ、会社の同僚から「鉄道指向ってなんだい? わけがわからないよ。あと、この記事はどの辺がごちうさに関係あるんだい?」というありがたいコメントを頂きました。
そんなわけで、今回は前回書いたプログラムについて、鉄道指向と絡めて解説したいと思います。
なお、鉄道指向について詳しく知りたい場合は鉄道指向プログラミングの翻訳記事を読んでいただいたほうがわかりやすいかと思います。

さて、ざっくり鉄道指向について説明すると、「成功(Success)と失敗(Failure)の2種類の値を返すような関数を鉄道のスイッチに見立てて、この関数を組み合わせて広大な路線=プログラムを組み立てていく」というプログラミングスタイルです。
例えば何らかのWebアプリケーションを、以下のようなブラックボックスと考えてみましょう。

Request-Response

ApplicationからSuccessとFailureの2本の矢印が伸びていますが、これはSuccessとFailureの両方を返すわけではなく、状況に応じていずれか一方を返すという意味です。
鉄道の路線と同様、通るのはどちらか一方だけ、ということですね。
またSuccessとFailureはどちらの路線かという情報とは別に、更に詳細な情報を持つことが出来ます。
上を通る列車が、詳細な情報を荷物として載せているイメージです。

このような「SuccessとFailureのいずれかを返す型response」は、OCamlでは代数的データ型を用いて以下のように書くことが出来ます。

type ('a, 'b) response = Success of 'a | Failure of 'b

‘aがSuccessの時に保持している詳細情報の型を、’bがFailureの時に保持している詳細情報の型を、それぞれ表しています。
例えば成功した時に数値を、失敗した時に文字列を詳細情報として持つような型は

(int,string) response

という型になります。
Success(1)やFailure(“入力した数値が不正です”)などは(int,string) response型の値です。

さて、Webアプリケーションでよくある処理といえば、入力値の検証を行い、問題なければ情報を加工(場合によっては保存)、最終的に出力用に整形を施して出力を行う、といったあたりでしょう。
これを同じような図にしてみます。

multibox

相変わらず、出力の上の線はSuccess、下の線はFailureです。
先ほどの図でブラックボックスになっていたApplicationの中身なので、全体としての出力もSuccessかFailureになっています。
これらのブラックボックスを原文に習って、鉄道の分岐に見立てて「スイッチ」と呼ぶことにしましょう。
(鉄道の分岐に見えない、という方は鉄道指向プログラミングの翻訳記事を参照してください。鉄道のレールっぽい図を作るだけの気力がありませんでした…)

それぞれのスイッチの中身も覗いてみましょう。

multibox_inner

一番最初のスイッチである入力整形は、「入力値に問題がなかった場合はSuccessを、問題があった場合はFailureを」返す関数です。
明記してませんが、Successの場合はRequestがSuccessに包まれてそのまま返されるイメージで、Failureの場合には、おそらく失敗原因を表す文字列が入るのでしょう。

次のスイッチである情報加工では、Successの場合に送られてきた情報を元に何らかの処理を行います。
この過程で、何らかの例外(例えば、DB接続エラーなど)が出た場合にはFailureを返します。
注目すべきはFailureの場合で、この場合は特に処理を行わずFailureをバイパスします。
このように鉄道指向プログラミングでは「前の処理が失敗した」という情報を、Failureという型を使って伝播します。
これにより、前の関数に依存することなく、「失敗していた場合には何もしない」といった処理を書くことが出来ます。

最後のスイッチである整形では、成功と失敗のそれぞれについて適切な出力フォーマッティングを行います。
情報加工ではFailureをバイパスしていましたが、このようにSuccessとFailureのそれぞれに対して何かを行う、といった処理も書くことが出来ます。

これらのスイッチは非常に単純な方法で組み合わせられている、ということが重要です。
例えば、複数段の検証を行いたい場合や、加工した情報をデータベースに永続化したい、といった改修を行いたい場合も、適切な場所に関数を合成するだけです。
さらに、これらの関数は簡単に再利用することが出来る、という点もポイントです。

まとめると、鉄道指向プログラミングは「組み立てやすく再利用性の高いスイッチという単位に処理を切り出し、これらを組み立てて全体を作っていく」ような手法になります。

鉄道指向について解説しましたが、実例がないとよくわからないと思うので、試しにFizzBuzzを鉄道指向っぽく作ってみましょう。
この場合、「3で割り切れるか」「5で割り切れるか」をそれぞれ入力値の検証に見立てて、FizzとBuzzがエラーメッセージだと考えるとうまく組み立てることができます。

まず基本となるスイッチは、

  • 整数値Xを受け取り、3で割り切れる場合はFailure(“Fizz”)を、割り切れない場合はSuccess(X)を返すスイッチ関数
  • 整数値Xを受け取り、5で割り切れる場合はFailure(“Buzz”)を、割り切れない場合はSuccess(X)を返すスイッチ関数

の2つです。

FizzBuzz

コードは以下のようになります。

let failOnMod n' msg n = if n mod n' =  then Failure msg else Success n
let fizz = failOnMod 3 "Fizz"
let buzz = failOnMod 5 "Buzz"

fizzとbuzzは剰余演算で用いる数字とエラーメッセージが違うだけなので、基本的な部分はfailOnModという関数に切り出しています。

ところでこの2つのスイッチは、いずれかがFailureになったとしても両方を通ってもらう必要があります。
フォームの入力値チェックなどでも、複数の検証を並列に行い全てのエラーを一度にユーザーに教えてあげる、というシチュエーションはありがちですね。
先ほどは直列に繋ぐ方法しか解説しませんでしたが、もちろんこれらのチェックを以下のように並列に合成することが可能です。

join_parallel

今回のFizzBuzzやフォームの検証では、いずれかの関数がFailureになった場合は全体としてもFailureになってもらう必要があります。
このブロックA、ブロックBにFizzBuzzのスイッチをそれぞれはめこんだものが以下の図です。

FizzBuzzMiz

動作を書き込んだのでちょっとごっちゃっとしていますが、要は

  • 両方Success(X)ならSuccess(X)にする(同じ値なので片方を捨てる)
  • 片方だけFailure(Y)ならFailure(Y)にする(Successを捨てる)
  • Failure(Y1)とFailure(Y2)ならFailure(Y1+Y2)にする(中身を連結する)

といった合成を行います。

コードに書くと以下のようになります。

let plus addS addF f1 f2 r = match f1 r, f2 r with
  | Success s1, Success s2 -> succeed @@ addS s1 s2
  | Failure f1, Success _  -> fail f1
  | Success _ , Failure f2 -> fail f2
  | Failure f1, Failure f2 -> fail @@ addF f1 f2
let (&&&) = plus const (^)
let fizzbuzz = fizz &&& buzz

plusは「両方Successのときの連結関数」と「両方Failureのときの連結関数」の2つを受け取り、並列に合成する関数を返す関数です。
(&&&)がplusを用いて定義した実際の連結関数で、前述のとおり両方Successの場合は片方を捨てるため第一引数にconst、両方Failureの場合は文字列連結を行うので(^)を渡しています。
このように定義した(&&&)を使うと、fizzbuzzは非常にシンプルになりますね。

ちなみに、このままだとSuccessとFailureに入ったままなので、表示などをしたい場合、これを文字列にしてやる必要があります。
が、ひとまずこれでFizzBuzzが出来たといっても過言ではないでしょう。

ところで。
FizzBuzzと同様、

  • 3で割り切れる数で「あぁ^~」を
  • 5で割り切れる数で「心が」を
  • 7で割り切れる数で「ぴょんぴょんするんじゃぁ^~」を

出力する関数を考えてみましょう。
もうわかりますね?

let failOnMod n' msg n = if n mod n' =  then Failure msg else Success n
let aa     = failOnMod 3 "あぁ^~"
let kokoro = failOnMod 5 "心が"
let pyon   = failOnMod 7 "ぴょんぴょんするんじゃぁ^~"
let (&&&) = plus const (^)
let gochiusa = aa &&& kokoro &&& pyon >> either string_of_int id
 
let () = List.iter (gochiusa >> Printf.printf "%s\n") (1--105)

関数aa, kokoro, pyonがそれぞれfizz, buzzのようなスイッチ関数になっています。
そして、関数gochiusaはそれらを&&&で繋げたものです。(>> either string_of_int idは文字列に変換しているだけです)
最後のlet ()は単に1から105までの数にgochiusaを適用した結果を出力しているだけです。
ね、簡単でしょ?

ぶっちゃけFizzBuzzはそこまで鉄道指向に適した題材ではなかったのですが、それでも便利そうだな―というのは伝わったのではないかと思います。
元の翻訳記事では今回使わなかったような様々な合成方法などについても触れられているので、興味を持たれたらぜひご一読をお勧めします。

以上、鉄道指向でこころぴょんぴょんする解説でした。

> gochiusa 105
あぁ^~心がぴょんぴょんするんじゃぁ^~