#TL;DR
モナドの作り方
- 値に付加するコンテキストを考える
- 1を元に、モナドインスタンス(M<T>)を定義する
- 基本関数(unit, flatMap)を実装する
- 置換と簡約を行い、モナド則を確認する
- 関数(アクション)は、モナドインスタンス型を返すようにする。
以下実際のモナドをSwiftで実装して、上記を確認する。
※ IOモナドに関するコードは、「関数型プログラミングの基礎 JavaScriptを使って学ぶ(amazon)」で紹介されているJavaScriptのコードをSwift化したものです。
モナドとは
値にコンテキストを付加し、コンテキストを付加したまま処理の合成を行う仕組み。
モナドの関数
関数 | 型 | 概要 |
---|---|---|
unit (return) | T->M<T> | 値を受け取り、モナドインスタンスM<T>を返す関数 |
flatMap (>>=) | M<A>->(A->M<B>)->M<B> | モナドインスタンスM<A>と その値をわたして処理を行う関数(A->M<B>)を受け取り、 モナドインスタンスM<B>を返す関数 |
その他の関数(アクション) | A -> M<T> | モナドインスンタンスM<T>を返すようにする |
モナド則
-
右単位元則
flatMap(monadInstance)(unit) == unit(value)
-
左単位元則
flatMap(unit(value))(f) == f(value)
-
結合法則 = 先に2つの関数を合成してから実行した結果は、flatMapで入れ子で処理を実行していった結果と等しい
flatMap(flatMap(monadInstance)(f1))(f2) == flatMap(monadInstance)({ (value) in return flatMap(f1(value))(f2) })
各モナドとモナドインスタンスM<T>
モナド | モナドインスタンス M<T> |
---|---|
恒等モナド | T |
Maybeモナド | Maybe<T> |
リストモナド | [T] |
IOモナド | WORLD->(Pair<T, WORLD>) |
各モナド詳細
恒等モナド
値にコンテキストを付加せず、そのまま処理を行うモナド
モナドインスタンス M<T> = T
コンテキストを付加しないので値の型と同一
基本関数の定義
unit関数 (return)
func unit<T>(_ value: T) -> T {
return value
}
flatMap関数 (==>)
func flatMap<A,B>(_ monadInstance: A) -> (@escaping (A)->(B)) -> B {
return { (f) in
return f(monadInstance)
}
}
モナド則の確認
置換による簡約で確認する。
計算式において``で囲んだ部分は、置換or簡約の対象を表す。
-
右単位元則
右単位元則の確認.txtflatMap(monadInstance)(unit) == unit(value) はじめに、 monadInstance = unit(value) とする 左式 = flatMap(`monadInstance`)(unit) = flatMap(`unit(value)`)(unit) = `flatMap`(value)(unit) = `{ (a) in return (transform) in return transform(a) }(value)`(unit) = `{ (transform) in return transform(value) }(unit)` = `unit(value)` = value 右式 = `unit(value)` = value よって左式=右式なので、右単位元則が成立する
-
左単位元則
左単位元則の確認.txtflatMap(unit(value))(f) == f(value) 左式 = flatMap(`unit(value)`)(f) = `flatMap(value)`(f) = `{ (transform) in return transform(value) }(f)` = f(value) 右式 = f(value) よって左式=右式なので、左単位元則が成立する
-
結合法則
結合法則の確認.txtflatMap(flatMap(monadInstance)(f1))(f2) == flatMap(monadInstance)({ (value) in return flatMap(f1(value))(f2) }) 左式 = flatMap(flatMap(`monadInstance`)(f1))(f2) = flatMap(flatMap(`unit(value)`)(f1))(f2) = flatMap(`flatMap(value)(f1)`)(f2) = flatMap(`f1(value)`)(f2) = `flatMap(value2)`(f2) = `{(transform) in return transform(value2)}(f2)` = f2(`value2`) = f2(f1(value)) 右式 = flatMap(m)(I2) = flatMap(m){ (value) in return I1 } とした時、 I1 = flatMap(`f1(value)`)(f2) = `flatMap(value2)`(f2) = `{(transform) in return transform(value2)}(f2)` = f2(`value2`) = f2(f1(value)) I2 = { (value) in return I1 } 右式 = `flatMap(value)`(I2) = `{ (transform) in return transform(value) }(I2)` = `I2`(value) = `{ (value) in return I1 }(value)` = I1 //I1になるだなんて、不思議のモナドちゃん。。。天才だ! = f2(f1(value)) よって左式=右式なので、結合法則が成立する
NOTE:
結合法則から、flatMapを入れ子で処理していくと、関数合成になることが分かる。
またその他のモナドでも置換と簡約を行うと、右式はI1式に帰結する。
Maybeモナド
値に失敗の可能性があるコンテキストを付加するモナド
モナドインスタンス M<T> = Maybe<T>
enum Maybe<T> where T: Equatable {
case nothing
case just (T)
}
基本関数の定義
unit関数 (return)
func unit<T>(_ value: T) -> Maybe<T> {
return .just(value)
}
flatMap関数 (==>)
func flatMap<A,B>(_ monadInstance: Maybe<A>) -> ((A)->Maybe<B>) -> Maybe<B> {
return { (f: (A)->Maybe<B>) in
switch monadInstance {
case .nothing:
return .nothing
case .just (let value):
return f(value)
}
}
}
モナド則の確認
-
右単位元則
右単位元則の確認.txtflatMap(monadInstance)(unit) == unit(value) はじめに、 monadInstance = .unit(value) とする 左式 = flatMap(`monadInstance`)(unit) = flatMap(`.unit(value)`)(unit) = `flatMap(.just(value))`(unit) = `{ (transform) in return transform(value) }(unit)` = `unit(value)` = .just(value) 右式 = `unit(value)` = .just(value) よって左式=右式なので、右単位元則が成立する
-
左単位元則
左単位元則の確認.txtflatMap(unit(value))(f) == f(value) 左式 = flatMap(`unit(value)`)(f) = `flatMap(.just(value))`(f) = `{ (transform) in return transform(value) }(f)` = f(value) 右式 = f(value) よって左式=右式なので、左単位元則が成立する
-
結合法則
結合法則の確認.txtflatMap(flatMap(monadInstance)(f1))(f2) == flatMap(monadInstance)({ (value) in return flatMap(f1(value))(f2) }) はじめに、 monadInstance = .unit(value) とする 左式 = flatMap(flatMap(`monadInstance`)(f1))(f2) = flatMap(flatMap(`unit(value)`)(f1))(f2) = flatMap(`flatMap(.just(value))`(f1))(f2) = flatMap(`{(transform) in reutrn transform(value)}(f1)`)(f2) = flatMap(`f1(value)`)(f2) = `flatMap(value2)`(f2) = `{(transform) in return transform(value2)}(f2)` = f2(`value2`) = f2(f1(value)) 右式 = flatMap(m)(I2) = flatMap(m){ (value) in return I1 } とした時、 I1 = flatMap(`f1(value)`)(f2) = `flatMap(value2)`(f2) = `{(transform) in return transform(value2)}(f2)` = f2(`value2`) = f2(f1(value)) I2 = { (value) in return I1 } 右式 = `flatMap(value)`(I2) = `{ (transform) in return transform(value) }(I2)` = `I2`(value) = `{ (value) in return I1 }(value)` = I1 = f2(f1(value)) よって左式=右式なので、結合法則が成立する
IOモナド
入出力による副作用を受ける関数と純粋な関数を分離するためのモナド
IOモナドインスタンスや基本関数の定義はこちらの参考書のコードをSwift化したものです。
関数型プログラミングの基礎 JavaScriptを使って学ぶ(amazon)
モナドインスタンス M<T> = IO<T> = (WORLD) -> Pair<T, WORLD>
- IOモナドのモナドインスタンスは、IOアクション(IO<T>)と呼ばれる
- WORLDは外界の型を表すが、コンピューターの世界では、外側の世界を表すすべがない
// 外界を取り敢えず、Anyにする。
typealias WORLD = Any
enum Pair<T, WORLD> {
case cons (T, WORLD) //左側に値、右側に外界の情報を格納する
}
// IOモナドインスタンス = IOアクション
typealias IO<T> = (WORLD) -> Pair<T, WORLD>
基本関数の定義
unit関数
func unit<T>(_ value: T) -> IO<T> {
return { (world) in
return .cons(value, world)
}
}
flatMap関数
func flatMap<A, B>(_ monadInstance: @escaping IO<A>) -> (@escaping (A) -> IO<B>) -> IO<B> {
return { (actionAB: @escaping (A) -> IO<B>) in
// IOアクションを返す
return { (world) in
// 新たな外界を作る
let newPair = monadInstance(world)
// 値を取り出す
switch newPair {
case .cons(let value, let newWorld):
return actionAB(value)(newWorld)
}
}// as (WORLD) -> Pair<B, WORLD>
}
}
モナド則の確認
はじめにflatMap関数にモナドインスタンス(m)を適用した簡約式を記す
flatMap(m) = { (actionAB) in
return { (world) in
return actionAB(value)(world)
}
}
以下詳細
flatMapをクロージャー式で書き直すと
var flatMap =
{ (monadInstance) in
{ (actionAB: @escaping (A) -> IO<B>) in
return { (world) in
let newPair = monadInstance(world)
switch newPair {
case .cons(let value, let newWorld):
return actionAB(value)(newWorld)
}
}
}
}
従って、
m= { (world) in return .cons(value, world) }
として置換を行うと、
内部の関数にてnewPairの値が以下のように簡約できる。
let newPair = m(world)
= { (world) in return .cons(value, world) } (world)
= .cons(value, world)
従って、flatMapにmインスタンスを適用した簡約式は、
switch文も展開すると以下になる事が分かる。
flatMap(m) = { (actionAB) in
return { (world) in
return actionAB(value)(world)
}
}
上式を用いて、モナド則を確認していく。
-
右単位元則
右単位元則の確認.txtflatMap(m)(unit) == unit(value) 左式 = flatMap(m)(unit) = { actionAB: in return { (world) in return actionAB(value)(world) }}(unit) = { (world) in return unit(value)(world) } = { (world) in return .cons(value, world) } 右式 = unit(value) = { (world) in return .cons(value, world) } よって左式=右式なので成立する
- 左単位元則
左単位元則の確認.txtflatMap(unit(value))(f) == f(value) f = {(value) in return { (world) in return process(value) }} とすると 左式 = flatMap(unit(value))(f) = flatMap({(world) in return .cons(value, world)}(f) = { actionAB in return { (world) in return actionAB(value)(world) } }(f) = { (world) in return f(value)(world) } = { (world) in return process(value) } 右式 = f(value) = { (world) in return process(value) } よって左式=右式なので成立する
- 結合法則
結合法則の確認.txtflatMap(flatMap(monadInstance)(f1))(f2) == flatMap(monadInstance)({ (value) in return flatMap(f1(value))(f2) }) はじめに、 f1 = {(value) in return { (world) in return process1(value) }} f2 = {(value) in return { (world) in return process2(value) }} process1(value) = .cons(p1(value), ()) process2(value) = .cons(p2(value), ()) とすると 右式 = flatMap(m)(I2) = flatMap(m){ (value) in return I1 } とした時、 I1 = flatMap(f1(value))(f2) = flatMap({(world) in return process1(value)})(f2) = flatMap(m1)(f2) = { (actionAB) in return (world) in return actionAB(p1(value))(()) }(f2) = { (world) in return f2(p1(value))(()) } = { (world) in return .cons(p2(p1(value))) } I2 = { (value) in return I1 } 右式 = flatMpa(m)(I2) = { (actionAB) in return { (world) in return actionAB(value)(world) }}(I2) = { (world) in return I2(value)(world) } = { (world) in return I1(world) } = { (world) in return .cons(p2(p1(value))) } = I1 左式 = flatMap(flatMap(monadInstance)(f1))(f2) = flatMap({ (actionAB) in return { (world) in return actionAB(value)(world) }}(f1))(f2) = flatMap({ (world) in return f1(value)(world) })(f2) = flatMap({ (world) in return .cons(p1(value)) })(f2) = flatMap(m1)(f2) = I1 = { (world) in return .cons(p2(p1(value))) } よって左式=右式なので成立する
IOモナドサンプル
ファイルの中身を読み取り、ログを出力するサンプルを記す
[gist] io_monad_sample.swift
// MARK: -- Sample
func read(file: String) -> IO<String> {
return unit("Welcome to Monad World")
}
func write(file: String) -> ((String) -> IO<Void>) {
return { (message) in
print(message)
return unit(Void())
}
}
func copy(from: String, to: String) -> IO<Void> {
return flatMap(read(file: from))({ (message) in return write(file: to)(message)
})
}
// 新たに生成したIOアクション
let copyAction = copy(from: "hello.txt", to: "hello.txt.bk")
// 外界の情報を引き渡して初めてアクションが実行される
_ = copyAction(Void())
NOTE:
- モナドに組み合わせる関数(IOアクション)は、モナドインスタンスを返すようにする
- IOアクション同士をflatMap関数で接続して新たなIOアクションが生成できる
- IOアクションを実行する場合は、外界の情報を引き渡す(外界は、コンピューターでは表せないので、空タプルを引き渡している)
最後に
型を中心としたモナドの解説が殆どなかったため、
関数型プログラミングの勉教のまとめとして、モナドを型からアプローチしてみました。
IOモナドは、Googleで検索をかけてもHaskell以外での解説コードが見当たりませんでした。
「関数型プログラミングの基礎 JavaScriptを使って学ぶ(amazon)」では、IOモナドのモナドインスタンスをHaskellが型に包み込んでいるのに対して、直接関数で表してJavaScriptで解説されており、特段型に包み込む必要がないのだと勉強になりました。
(上記、Haskellは型クラスのインスタンスにするには型を定義する必要があるため、わざわざ関数を型で包み込む必要があるのだとも気付かされました。これだと2重に値を包み込むことになる。)
本当に本当に素晴らしい参考書です。
参考書
-
関数型プログラミングの基礎 JavaScriptを使って学ぶ (amazon)
モナド則の確認で使用した値を代入しての置換と簡約のテクニックは、この参考書から学びました。 -
関数プログラミング実践入門(amazon)
型を使ってのトップダウン方式によるプログラミング設計が解説されています。
型の強い言語を使って開発をされている方は、ぜひ絶対に読んでほしい。
参考サイト
- Haskell IOモナド 超入門 Qiita
- Haskellで大域変数が欲しい時はReaderモナドを使いましょう