Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
3
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

How to cook a Monad with Swift

TL;DR

モナドの作り方

  1. 値に付加するコンテキストを考える
  2. 1を元に、モナドインスタンス(M<T>)を定義する
  3. 基本関数(unit, flatMap)を実装する
  4. 置換と簡約を行い、モナド則を確認する
  5. 関数(アクション)は、モナドインスタンス型を返すようにする。

以下実際のモナドを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)

identifier_monad.swift
func unit<T>(_ value: T) -> T {
    return value
}

flatMap関数 (==>)

identifier_monad.swift
func flatMap<A,B>(_ monadInstance: A) -> (@escaping (A)->(B)) -> B {
    return { (f) in
        return f(monadInstance)
    }
}

モナド則の確認

置換による簡約で確認する。
計算式において``で囲んだ部分は、置換or簡約の対象を表す。

  • 右単位元則
右単位元則の確認.txt
  flatMap(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


  よって左式=右式なので、右単位元則が成立する
  • 左単位元則
左単位元則の確認.txt
  flatMap(unit(value))(f) == f(value)

  左式 = flatMap(`unit(value)`)(f)
    = `flatMap(value)`(f)
    = `{ (transform) in return transform(value) }(f)`
    = f(value)

  右式 = f(value)

  よって左式=右式なので、左単位元則が成立する
  • 結合法則
結合法則の確認.txt
  flatMap(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>

maybe_monad.swift
enum Maybe<T> where T: Equatable {
    case nothing
    case just (T)
}

基本関数の定義

unit関数 (return)

maybe_monad.swift
func unit<T>(_ value: T) -> Maybe<T> {
    return .just(value)
}

flatMap関数 (==>)

maybe_monad.swift
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)
        }
    }
}

モナド則の確認

  • 右単位元則
右単位元則の確認.txt
  flatMap(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)


  よって左式=右式なので、右単位元則が成立する
  • 左単位元則
左単位元則の確認.txt
  flatMap(unit(value))(f) == f(value)

  左式 = flatMap(`unit(value)`)(f)
    = `flatMap(.just(value))`(f)
    = `{ (transform) in return transform(value) }(f)`
    = f(value)

  右式 = f(value)

  よって左式=右式なので、左単位元則が成立する
  • 結合法則
結合法則の確認.txt
  flatMap(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は外界の型を表すが、コンピューターの世界では、外側の世界を表すすべがない
io_monad.swift
// 外界を取り敢えず、Anyにする。
typealias WORLD = Any

enum Pair<T, WORLD> {
    case cons (T, WORLD) //左側に値、右側に外界の情報を格納する
}

// IOモナドインスタンス = IOアクション
typealias IO<T> = (WORLD) -> Pair<T, WORLD>

基本関数の定義

unit関数

io_monad.swift
func unit<T>(_ value: T) -> IO<T> {
    return { (world) in
        return .cons(value, world)
    }
}

flatMap関数

io_monad.swift
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)を適用した簡約式(結果).txt
flatMap(m) = { (actionAB) in 
    return { (world) in
        return actionAB(value)(world)
    }
}

以下詳細

flatMap関数にモナドインスタンス(m)を適用した簡約式(詳細).txt
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)
    }
}

上式を用いて、モナド則を確認していく。

  • 右単位元則
右単位元則の確認.txt
  flatMap(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) }

  よって左式=右式なので成立する
  • 左単位元則
左単位元則の確認.txt
  flatMap(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) }

  よって左式=右式なので成立する
  • 結合法則
結合法則の確認.txt
  flatMap(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

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)
    モナド則の確認で使用した値を代入しての置換と簡約のテクニックは、この参考書から学びました。

  • すごいHaskellたのしく学ぼう(amazon)
    Haskellはこれで勉強しました。

  • 関数プログラミング実践入門(amazon)
    型を使ってのトップダウン方式によるプログラミング設計が解説されています。
    型の強い言語を使って開発をされている方は、ぜひ絶対に読んでほしい。

参考サイト

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
3
Help us understand the problem. What are the problem?