Help us understand the problem. What is going on with this article?

手続きに帰れ / Back to Procedure

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型の函数として定義した。

詳細
ReadlineSync.js
"use strict";

// module Effect.ReadlineSync

var readlineSync = require('readline-sync');

exports.question = function (query) {
  return function() {
      return readlineSync.question(query);
  }
}
ReadlineSync.purs
module Effect.ReadlineSync (question) where

import Effect (Effect)

foreign import question :: String -> Effect String

Effect Unit (IO ()) とはなにであるか

エントリーポイントであるmainの型はEffect Unitであるという。

Unitは具体型だ。これは直系の親である Haskell では()と表現されているもので,unit以外の値をもたない,単集合Unit Setの型だ。これは C 直系言語で言うところのvoidのような扱いを受けていると考えていいだろう。

Effectは Haskell ではIOにあたる多相型で,具体型を一つとって別の具体型に変わる。ある意味では,オブジェクト指向でよく出てくるArrayList<String>のようなジェネリッククラスに近い。Effect Unitなら,Unitの属性を帯びたEffectみたいな感じだ。そして,Effectはかの悪名高いモナドでもあり,作用を意味している。

ところで,いまの表現は巷のメタファーとは主従が逆転している。たいてい,モナドm amの中にaが入っているかのように説明される

たしかにMaybe aのようなモナドはa型の値が入った箱のようなものと思ってもそう間違いではない。

が,Effect aというモナドは,a型の値が入っていると言うよりかはa型の値を返す関数 (ルーチン) によって表現されると考えるべきだ。これをアクションという。

だから,それらの値を使って何かをしたいという欲求は,関数合成のような仕組みで実現される。モナドは文脈の合成を抽象化したものと言えるかもしれない。

純粋でありながら作用を扱えるというのは,このアクションとモナドの性質による。

たとえば,調理という行為は作用を起こすといえる。材料が消失し,料理ができあがる。これは副作用だ。あげく,レシピに “ホットケーキミックスが足りない場合,小麦粉で代用” とか書いてあったら目も当てられない。できあがる料理はホットケーキミックスの多寡によって変わってしまう。こんなの,純粋関数型言語で許せるわけがない。

しかし,レシピは作用を起こさない。当たり前の話である。レシピというただの情報が勝手に料理を生成し始めたらそんなもん収容案件だ。

そして,レシピは自由に組み合わせることができる。ご飯の炊き方,みそ汁の作り方,魚の焼き方。これらを合わせて朝食のレシピとなる。レシピを組み合わせても当然作用など起きようがないし,たとえレシピが条件で分岐していても,出来上がる合成レシピが変わることはない。合成されたレシピも条件で分岐するよう書かれるだけの話だ。つまり同じ入力に対しては同じ出力を返す,純粋な計算である。

この “組み合わせる” という操作を抽象化して,様々な文脈に応用するのがモナドという概念なのだ……と理解している。

モナドはいかにして合成するか

文脈の合成に便利な演算子をふたつ紹介。

まずは束縛Bind演算子>>=。左オペランドにm aなモナド,右オペランドにa -> m bなモナド函数をとってm bなモナドを返す。手元のモナドを適切な函数の引数に束縛してやれば,まるで値をコンベアに乗せて加工していくようにつないでいける。

そして適用二次Apply Second演算子*>。モナドをふたつとって合成し,右オペランドの方を返す。一見無意味に思えるが,左オペランドがアクション系のモナドだった場合,宙ぶらりんだがそれが実行されるわけだ。

今回は使用しないが,以下のような演算子や函数も便利だ。

写像Map演算子<$>。左オペランドにa -> bな函数,右オペランドにm aなモナドをとってm bなモナドを返す。JavaScript 配列のmapメソッドを使った経験はあるだろうか。この演算子は,まさしくそれだ。

適用Apply演算子<*>。左オペランドにm (a -> b)な関数モナド,右オペランドにm aなモナドをとってm bなモナドを返す。カリー化された函数に写像演算子で引数を渡すと返る函数もモナドになるので,次の引数は適用演算子で渡す必要がある。add <$> someMonadA <*> someMonadBってな具合だ。

purea型の値を引数にとってm aなモナドを返す。各モナドごとに実装されていて,例えばpure 1.0とは純然たるPure1.0…というように “最も単純なモナド” を作り出す函数といえる。ちなみにEffectモナドの場合は “実際にはなんの副作用も起こさず (引数として与えた値) を返すだけのアクション” を作り出す。

こんなところだろうか。ちなみに,ここまでモナドモナドと言ってきたが,モナドよりもっと広い概念の演算子 / 函数だったり。Haskell では*>のモナド専用バージョン>>pureのモナド専用バージョンreturnがあるらしい。

私はいかにして失明したか

少々長く説明しすぎた。logString -> Effect Unit型,questionString -> 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って名前採用したのはアカンと思う。

BlueRayi
アマグラマ。大学では化け学を専攻。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした