Back to Procedure / 手続きに帰れ (2)(U)
エンチャント
手続き的でない構文は,それらのコントローラーのアンタップ・ステップにアンタップしない。
TL; DR ~ 寺生まれの do さん ~
do 式はすごい,改めてそう思った。
前置き
むくむくと純粋関数型言語への興味が湧いてきた最近の私は,とりあえず PureScript を勉強することにした。この記事は一種の備忘録である。
ハローワールドレベルのごく小規模なプログラム, JavaScript (Node.js) による手続き的なそれを PureScript のモナドチェーンによる書き方に置き換え,それを構文糖である do 式を使用して書き換えることで手続き的な表現に戻せることを確かめよう。
元のプログラム
var readlineSync = require('readline-sync');
function main() {
console.log("Please tell me your first name?");
var firstName = readlineSync.question("> ");
console.log("Please tell me your last name?");
var lastName = readlineSync.question("> ");
console.log("Hello, " + firstName + " " + lastName + "!");
}
main();
readline-sync を使用して簡単な標準入力を実現していることをのぞけば,ごくごく普通の挨拶プログラムである。プログラミング教育の 2 時限目で取り上げられそうな題材だ。
PureScript で書く ~ モナドチェーンによる ~
readline-sync を FFI で PureScript から利用できるようにするが,先人の尻馬に乗ったばかりであまり大したこともしていないので省略する。こちらを参照。今回はquestion
という名前のString -> Effect String
型の函数として定義した。
詳細
"use strict";
// module Effect.ReadlineSync
var readlineSync = require('readline-sync');
exports.question = function (query) {
return function() {
return readlineSync.question(query);
}
}
module Effect.ReadlineSync (question) where
import Effect (Effect)
foreign import question :: String -> Effect String
Effect Unit
(IO ()
) とはなにであるか
エントリーポイントであるmain
の型はEffect Unit
であるという。
Unit
は具体型だ。これは直系の親である Haskell では()
と表現されているもので,unit
以外の値をもたない,単集合の型だ。これは C 直系言語で言うところのvoid
のような扱いを受けていると考えていいだろう。
Effect
は Haskell ではIO
にあたる多相型で,具体型を一つとって別の具体型に変わる。ある意味では,オブジェクト指向でよく出てくるArrayList<String>
のようなジェネリッククラスに近い。Effect Unit
なら,Unit
の属性を帯びたEffect
みたいな感じだ。そして,Effect
はかの悪名高いモナドでもあり,作用を意味している。
ところで,いまの表現は巷のメタファーとは主従が逆転している。たいてい,モナドm a
はm
の中にa
が入っているかのように説明される。
たしかにMaybe a
のようなモナドはa
型の値が入った箱のようなものと思ってもそう間違いではない。
が,Effect a
というモナドは,a
型の値が入っていると言うよりかはa
型の値を返す関数 (ルーチン) によって表現されると考えるべきだ。これをアクションという。
だから,それらの値を使って何かをしたいという欲求は,関数合成のような仕組みで実現される。モナドは文脈の合成を抽象化したものと言えるかもしれない。
純粋でありながら作用を扱えるというのは,このアクションとモナドの性質による。
たとえば,調理という行為は作用を起こすといえる。材料が消失し,料理ができあがる。これは副作用だ。あげく,レシピに “ホットケーキミックスが足りない場合,小麦粉で代用” とか書いてあったら目も当てられない。できあがる料理はホットケーキミックスの多寡によって変わってしまう。こんなの,純粋関数型言語で許せるわけがない。
しかし,レシピは作用を起こさない。当たり前の話である。レシピというただの情報が勝手に料理を生成し始めたらそんなもん収容案件だ。
そして,レシピは自由に組み合わせることができる。ご飯の炊き方,みそ汁の作り方,魚の焼き方。これらを合わせて朝食のレシピとなる。レシピを組み合わせても当然作用など起きようがないし,たとえレシピが条件で分岐していても,出来上がる合成レシピが変わることはない。合成されたレシピも条件で分岐するよう書かれるだけの話だ。つまり同じ入力に対しては同じ出力を返す,純粋な計算である。
この “組み合わせる” という操作を抽象化して,様々な文脈に応用するのがモナドという概念なのだ……と理解している。
モナドはいかにして合成するか
文脈の合成に便利な演算子をふたつ紹介。
まずは束縛演算子>>=
。左オペランドにm a
なモナド,右オペランドにa -> m b
なモナド函数をとってm b
なモナドを返す。手元のモナドを適切な函数の引数に束縛してやれば,まるで値をコンベアに乗せて加工していくようにつないでいける。
そして適用二次演算子*>
。モナドをふたつとって合成し,右オペランドの方を返す。一見無意味に思えるが,左オペランドがアクション系のモナドだった場合,宙ぶらりんだがそれが実行されるわけだ。
今回は使用しないが,以下のような演算子や函数も便利だ。
写像演算子<$>
。左オペランドにa -> b
な函数,右オペランドにm a
なモナドをとってm b
なモナドを返す。JavaScript 配列のmap
メソッドを使った経験はあるだろうか。この演算子は,まさしくそれだ。
適用演算子<*>
。左オペランドにm (a -> b)
な関数モナド,右オペランドにm a
なモナドをとってm b
なモナドを返す。カリー化された函数に写像演算子で引数を渡すと返る函数もモナドになるので,次の引数は適用演算子で渡す必要がある。add <$> someMonadA <*> someMonadB
ってな具合だ。
pure
。a
型の値を引数にとってm a
なモナドを返す。各モナドごとに実装されていて,例えばpure 1.0
とは純然たる1.0
…というように “最も単純なモナド” を作り出す函数といえる。ちなみにEffect
モナドの場合は “実際にはなんの副作用も起こさず (引数として与えた値) を返すだけのアクション” を作り出す。
こんなところだろうか。ちなみに,ここまでモナドモナドと言ってきたが,モナドよりもっと広い概念の演算子 / 函数だったり。Haskell では*>
のモナド専用バージョン>>
とpure
のモナド専用バージョンreturn
があるらしい。
私はいかにして失明したか
少々長く説明しすぎた。log
がString -> Effect Unit
型,question
がString -> Effect String
型であることを考慮しながら書いていくと,つまりこういうことになる。
まず,最初の文を表示する。
main = log "Please tell me your first name?"
つづいて,入力を促す。さっきのアクションは返り値を使わないので (っていうかunit
だし) 適用二次演算子で捨てる。
main = log "Please tell me your first name?" *> question "> "
入力された文字列は後で使うので,とりあえずラムダ式の引数に束縛する。question "> "
の型がEffect String
であるから,ラムダ式の型は自動的にString -> Effect a
となるわけで,引数firstName
の型はString
だ。
main = log "Please tell me your first name?" *> question "> " >>= (\firstName -> ...)
これをもう一度繰り返す。
main = log "Please tell me your first name?" *> question "> " >>= (\firstName -> log "Please tell me your last name?" *> question "> " >>= (\lastName -> ...))
最後に,ラムダ式の引数を使って挨拶を表示。先述の通り,引数は単にString
とみなせる。
main = log "Please tell me your first name?" *> question "> " >>= (\firstName -> log "Please tell me your last name?" *> question "> " >>= (\lastName -> log ("Hello, " <> firstName <> " " <> lastName <> "!")))
出来上がったのを改めて眺めてみる。
module Main where
import Prelude
import Effect (Effect)
import Effect.Console (log)
import Effect.ReadlineSync (question)
main :: Effect Unit
main = log "Please tell me your first name?" *> question "> " >>= (\firstName -> log "Please tell me your last name?" *> question "> " >>= (\lastName -> log ("Hello, " <> firstName <> " " <> lastName <> "!")))
うむ,長い。長過ぎる。そして一見して意図が掴みづらい。モナドを束縛するラムダ式は二つネストしているし,あまりにも読むのがきつい。こんなものレビューさせられたら PureScript ハラスメント略してピュアハラで訴えたくなるな。
改行してみる
失明したので少しでも読みやすくしよう。メソッドチェーンだって適宜改行するのだから,これも改行を入れたら見やすくなるかもしれない。
main =
log "Please tell me your first name?" *>
question "> " >>= (\firstName ->
log "Please tell me your last name?" *>
question "> " >>= (\lastName ->
log ("Hello, " <> firstName <> " " <> lastName <> "!")))
おや,こうすると意外と手続きとそう変わらない感じになってきた。
SYNTAX SUGAR do式-DOSHIKI-「待たせたな!」
ここで do 式の登場だ! do 式の中では,束縛演算子はラムダ式の仮引数を左に移動して<-
なんて代入っぽい見た目になり,適用二次演算子は単に消える。すると,
main = do
log "Please tell me your first name?"
firstName <- question "> "
log "Please tell me your last name?"
lastName <- question "> "
log ("Hello, " <> firstName <> " " <> lastName <> "!")
おお!
function main() {
console.log("Please tell me your first name?");
var firstName = readlineSync.question("> ");
console.log("Please tell me your last name?");
var lastName = readlineSync.question("> ");
console.log("Hello, " + firstName + " " + lastName + "!");
}
見比べてほしい。ほぼそのままじゃないか。最初のモナモナした感じが綺麗サッパリ消し去られた。これなら手続きで育ってきた人間も失明しない。さしずめこわくない PureScrtipt 入門といったところだ。
ここで,モナドがどのように合成されるか,つまり do 式でいえばそれぞれの行の間になにをするかは,それぞれのモナドごとに違う。もちろん,モナドの設計者が適切に実装しているはずだ。たとえばMaybe
なら失敗したら以降の行は全カット,List
なら後段のリストに再帰的に実行,Effect
なら先述したとおりある種の関数合成だ。
まるでこれはセミコロン;
という演算子をオーバーロードしているかのようである。これをもってモナドは「プログラム可能なセミコロン」と呼ばれるわけですね。
まとめ
モナドによるプログラミングをかじり do 式の正体を知った。
do 式の正体を知ることでややこしいとされる do 式のルールも丸暗記ではなく知識としてわかるようになる。この記事のルール 2 以降は正体を知った今なら説明できるはずだ。みんなもやってみよう。ルール 1 は単なる文法の決め事なので割愛。
しかしこんな長い素のモナドチェーンは二度と書きたくないと思いました。あらためて do 式に感謝。モナドチェーンだけでプログラミングなんてしたくないので命令型風味の構文考えたりした奴は万死に値するという意見には賛同できないけど確かに Haskell でreturn
って名前採用したのはアカンと思う。