更新 この話題に関する私の講演のスライドと動画

警告:この記事には、ぞっとするような話題、無理のある例え、モナドに関する議論が含まれています。

フランケンファンクター博士とモナド怪物の物語、第3回にようこそ!

第1回では、フランケンファンクター博士が死体のパーツから生命を作り出す過程を紹介しました。 博士は「モナド怪物パーツ生成器」(略して「M」)を使い、生命力を供給すると生きた体のパーツを返すようにしました。

また、生き物の脚と腕の作成方法、そしてこれらのM値がmapMmap2Mを使ってどのように処理され組み合わされたかについて説明しました。

第2回では、頭、心臓、胴体がreturnMbindMapplyMなどの強力な技術を使ってどのように作られたかを解説しました。

この最終回では、使用したすべての技術を振り返り、コードをリファクタリングし、フランケンファンクター博士の技術を現代のステートモナドと比較します。

シリーズの完全なリンク

使用した技術の振り返り

リファクタリングの前に、使用したすべての技術を振り返ってみましょう。

M

生命力が手に入るまで生きた体のパーツは作れませんでしたが、 雷が落ちる前にパーツを操作したり組み合わせたりする方法が必要でした。 そこで、各パーツの「生き返す」関数をラップするM型を作りました。 これにより、M<BodyPart>のことは、時が来たらBodyPartを作るためのレシピ、または指示書として考えることができました。

Mの定義は次のとおりです。

type M<'a> = M of (VitalForce -> 'a * VitalForce)

mapM

次に、生命力を使わずにMの内容を変換したいと思いました。具体的には、折れた腕のレシピ(M<BrokenLeftArm>)を折れていない腕のレシピ(M<LeftArm>)に変えたいと思いました。 解決策は、通常の関数'a -> 'bM<'a> -> M<'b>関数に変換するmapM関数を実装することでした。

mapMは次のようなシグネチャです。

val mapM : f:('a -> 'b) -> M<'a> -> M<'b>

map2M

また、2つのMレシピを組み合わせて新しいレシピを作りたいと思いました。 この場合、上腕(M<UpperRightArm>)と前腕(M<LowerRightArm>)を組み合わせて腕全体(M<RightArm>)を作ることでした。解決策はmap2Mでした。

map2Mは次のようなシグネチャです。

val map2M : f:('a -> 'b -> 'c) -> M<'a> -> M<'b> -> M<'c>

returnM

もう1つの課題は、通常の値を生命力なしでMレシピの世界に直接持ち上げることでした。 この場合、SkullM<Skull>に変換して、map2Mで頭全体を作るのに使えるようにすることでした。

returnMは次のようなシグネチャです。

val returnM : 'a -> M<'a>

モナディック関数

似たような形の関数をたくさん作りました。これらはすべて何かを入力として受け取り、Mレシピを出力として返します。 つまり、これらの関数は次のようなシグネチャです。

val monadicFunction : 'a -> M<'b>

実際に使用したモナディック関数の例をいくつか示します。

val makeLiveLeftLeg : DeadLeftLeg -> M<LiveLeftLeg>
val makeLiveRightLowerArm : DeadRightLowerArm -> M<LiveRightLowerArm>
val makeLiveHeart : DeadHeart -> M<LiveHeart>
val makeBeatingHeart : LiveHeart -> M<BeatingHeart>
// そして
val returnM : 'a -> M<'a>

bindM

これまでの関数は生命力へのアクセスを必要としませんでしたが、次に2つのモナディック関数を連鎖させる必要が出てきました。 具体的には、makeLiveHeart(シグネチャ:DeadHeart -> M<LiveHeart>)の出力をmakeBeatingHeart(シグネチャ:LiveHeart -> M<BeatingHeart>)の入力につなげる必要がありました。 解決策はbindMでした。これは'a -> M<'b>形式のモナディック関数をM世界の関数(M<'a> -> M<'b>)に変換し、それらを組み合わせることができるようにします。

bindMのシグネチャは次のとおりです。

val bindM : f:('a -> M<'b>) -> M<'a> -> M<'b>

applyM

最後に、多くのMパラメータを組み合わせて生きた体を作る方法が求められました。 mapの特別なバージョン(map4Mmap5Mmap6Mなど)を作る代わりに、M関数をMパラメータに適用する汎用的なapplyM関数を実装しました。 これにより、部分適用を使って1つずつMパラメータを適用し、任意の大きさの関数を扱えるようになりました。

applyMのシグネチャは次のとおりです。

val applyM : M<('a -> 'b)> -> M<'a> -> M<'b>

bindとreturnを使って他の関数を定義する

これらの関数のうち、bindMだけが生命力へのアクセスを必要としたことに注目してください。

実際、以下で見るように、mapMmap2MapplyMbindMreturnMを使って定義できます!

コンピュテーション式へのリファクタリング

作成した関数の多くが非常に似た形をしており、結果として重複が多く生じています。以下は一例です。

let makeLiveLeftLegM deadLeftLeg  = 
    let becomeAlive vitalForce = 
        let (DeadLeftLeg label) = deadLeftLeg
        let oneUnit, remainingVitalForce = getVitalForce vitalForce 
        let liveLeftLeg = LiveLeftLeg (label,oneUnit)
        liveLeftLeg, remainingVitalForce    
    M becomeAlive  // 関数を単一ケースユニオンでラップ

特に、生命力を明示的に処理する箇所が多く含まれています。

ほとんどの関数型言語には、これを隠蔽してコードをより簡潔にする方法があります。

Haskellでは開発者が「do記法」を使用し、Scalaでは「for-yield」(for内包表記)を使用します。そしてF#では、コンピュテーション式を使用します。

F#でコンピュテーション式を作成するには、まず「bind」と「return」の2つが必要です。これらは既に持っています。

次に、特別な名前のメソッドBindReturnを持つクラスを定義します。

type MonsterBuilder()=
    member this.Return(x) = returnM x
    member this.Bind(xM,f) = bindM f xM

最後に、このクラスのインスタンスを作成します。

let monster = new MonsterBuilder()

これが完了すると、async{...}seq{...}などと同様に、特別な構文monster {...}が使用できるようになります。

  • let! x = xM構文では、右辺がM型、たとえばM<X>である必要があります。
    let!M<X>Xにアンラップし、左辺の「x」にバインドします。
  • return y構文では、戻り値が「通常の」型、たとえばYである必要があります。
    returnはそれをM<Y>にラップし(returnMを使用)、monster式の全体の値として返します。

したがって、例のコードは次のようになります。

monster {
    let! x = xM  // M<X>をXにアンラップし、「x」にバインド
    return y     // YをラップしてM<Y>を返す
    }

コンピュテーション式についてもっと詳しく知りたい場合は、私の詳細な連載をご覧ください。

mapMと仲間の再定義

monster式が使用可能になったので、mapMやその他の関数を書き直してみましょう。

mapM

mapMは関数とラップされたM値を受け取り、内部の値に関数を適用して返します。

monsterを使用した実装は次のとおりです。

let mapM f xM = 
    monster {
        let! x = xM  // M<X>をアンラップ
        return f x   // (f x)のMを返す
        }

この実装をコンパイルすると、以前の実装と同じシグネチャが得られます。

val mapM : f:('a -> 'b) -> M<'a> -> M<'b>

map2M

map2Mは関数と2つのラップされたM値を受け取り、両方の値に関数を適用して返します。

これもmonster式を使って簡単に書くことができます。

let map2M f xM yM = 
    monster {
        let! x = xM  // M<X>をアンラップ
        let! y = yM  // M<Y>をアンラップ
        return f x y // (f x y)のMを返す
        }

この実装をコンパイルすると、再び以前の実装と同じシグネチャが得られます。

val map2M : f:('a -> 'b -> 'c) -> M<'a> -> M<'b> -> M<'c>

applyM

applyMはラップされた関数とラップされた値を受け取り、値に関数を適用して返します。

これもmonster式を使って簡単に書くことができます。

let applyM fM xM = 
    monster {
        let! f = fM  // M<F>をアンラップ
        let! x = xM  // M<X>をアンラップ
        return f x   // (f x)のMを返す
        }

シグネチャは期待通りです。

val applyM : M<('a -> 'b)> -> M<'a> -> M<'b>

monsterコンテキスト内での生命力の操作

他のすべての関数もmonster式を使って書き直したいのですが、そこには課題があります。

多くの関数の本体は次のような形をしています。

// コンテキストから生命力の単位を抽出
let oneUnit, remainingVitalForce = getVitalForce vitalForce 

// 何かを行う

// 値と残りの生命力を返す
liveBodyPart, remainingVitalForce

つまり、生命力の一部を取得し、次のステップで使用する新しい生命力を設定しています。

オブジェクト指向プログラミングでは「ゲッター」と「セッター」に慣れているので、monsterコンテキストで動作する同様のものを書いてみましょう。

getMの導入

まずゲッターから始めましょう。どのように実装すべきでしょうか?

生命力は生きている状態のコンテキストでのみ利用可能なので、関数は一般的な形式に従う必要があります。

let getM = 
    let doSomethingWhileLive vitalForce = 
        // ここで何をする??
        何を返す??, vitalForce 
    M doSomethingWhileLive

vitalForceは取得しても使わないので、元の量をそのまま返します。

では、中間部分で何をすべきでしょうか?そしてタプルの最初の要素として何を返すべきでしょうか?

答えは簡単です。生命力自体を返すのです!

let getM = 
    let doSomethingWhileLive vitalForce = 
        // タプルの最初の要素として現在の生命力を返す
        vitalForce, vitalForce 
    M doSomethingWhileLive

getMM<VitalForce>値なので、monster式の中で次のようにアンラップできます。

monster {
    let! vitalForce = getM
    // 生命力で何かをする
    }

putMの導入

セッターについては、新しい生命力をパラメータとする関数として実装します。

let putM newVitalForce  = 
    let doSomethingWhileLive vitalForce = 
        ここで何をする??
    M doSomethingWhileLive

ここでも、中間部分で何をすべきでしょうか?

最も重要なのは、newVitalForceが次のステップに渡される値になることです。元の生命力は捨てなければなりません!

つまり、newVitalForceは返されるタプルの2番目の部分として使用しなければなりません

そして、返されるタプルの1番目の部分には何を入れるべきでしょうか?適切な値がないので、単にunitを使用します。

最終的な実装は以下のようになります。

let putM newVitalForce  = 
    let doSomethingWhileLive vitalForce = 
        // タプルの1番目の要素には何も返さない
        // タプルの2番目の要素にnewVitalForceを返す
        (), newVitalForce
    M doSomethingWhileLive

getMputMが用意できたので、次のような関数を作成できます。

  • コンテキストから現在の生命力を取得する
  • そこから1単位を抽出する
  • 現在の生命力を残りの生命力で置き換える
  • 1単位の生命力を呼び出し元に返す

以下がそのコードです。

let useUpOneUnitM = 
    monster {
        let! vitalForce = getM
        let oneUnit, remainingVitalForce = getVitalForce vitalForce 
        do! putM remainingVitalForce 
        return oneUnit
        }

monster式を使って他のすべての関数を書き直す

useUpOneUnitMを使って、他のすべての関数の書き直しを始めることができます。

たとえば、元のmakeLiveLeftLegM関数は次のようになっており、生命力の明示的な処理がたくさん含まれています。

let makeLiveLeftLegM deadLeftLeg  = 
    let becomeAlive vitalForce = 
        let (DeadLeftLeg label) = deadLeftLeg
        let oneUnit, remainingVitalForce = getVitalForce vitalForce 
        let liveLeftLeg = LiveLeftLeg (label,oneUnit)
        liveLeftLeg, remainingVitalForce    
    M becomeAlive  // 関数を単一ケースユニオンでラップ

monster式を使用した新しいバージョンでは、生命力の処理が暗黙的になり、結果としてずっとクリーンになります。

let makeLiveLeftLegM deadLeftLeg = 
    monster {
        let (DeadLeftLeg label) = deadLeftLeg
        let! oneUnit = useUpOneUnitM
        return LiveLeftLeg (label,oneUnit)
        }

同様に、すべての腕の手術コードを次のように書き直すことができます。

let makeLiveRightLowerArm (DeadRightLowerArm label) = 
    monster {
        let! oneUnit = useUpOneUnitM
        return LiveRightLowerArm (label,oneUnit)
        }

let makeLiveRightUpperArm (DeadRightUpperArm label) = 
    monster {
        let! oneUnit = useUpOneUnitM
        return LiveRightUpperArm (label,oneUnit)
        }

// M-パーツを作成
let lowerRightArmM = DeadRightLowerArm "Tom" |> makeLiveRightLowerArm 
let upperRightArmM = DeadRightUpperArm "Jerry" |> makeLiveRightUpperArm 

// armSurgeryをM-関数に変換 
let armSurgeryM  = map2M armSurgery 

// 手術を行って2つのM-パーツを新しいM-パーツに組み合わせる
let rightArmM = armSurgeryM lowerRightArmM upperRightArmM

このように続けていきます。この新しいコードはずっとクリーンになりました。

実は、コードをもっと整理することができます。 armSurgeryarmSurgeryMのような中間的な値を排除し、すべてを1つのmonster式にまとめることができます。

let rightArmM = monster {
    let! lowerArm = DeadRightLowerArm "Tom" |> makeLiveRightLowerArm 
    let! upperArm = DeadRightUpperArm "Jerry" |> makeLiveRightUpperArm 
    return {lowerArm=lowerArm; upperArm=upperArm}
    }

頭の場合も同様のアプローチを使用できます。headSurgeryreturnMはもう必要ありません。

let headM = monster {
    let! brain = makeLiveBrain deadBrain
    return {brain=brain; skull=skull}
    }

最後に、monster式を使って体全体を作ることもできます。

// M-パーツからM-体を作成する関数
let createBodyM leftLegM rightLegM leftArmM rightArmM headM beatingHeartM = 
    monster {
        let! leftLeg = leftLegM
        let! rightLeg = rightLegM
        let! leftArm = leftArmM
        let! rightArm = rightArmM
        let! head = headM 
        let! beatingHeart = beatingHeartM

        // レコードを作成
        return {
            leftLeg = leftLeg
            rightLeg = rightLeg
            leftArm = leftArm
            rightArm = rightArm
            head = head
            heart = beatingHeart 
            }
        }

// M-体を作成 
let bodyM = createBodyM leftLegM rightLegM leftArmM rightArmM headM beatingHeartM

注意:monster式を使用した完全なコードはGitHubで利用可能です。

monster式 vs applyM

以前、applyMを使用して体を作成する別の方法を紹介しました。

参考までに、applyMを使用した方法を以下に示します。

let createBody leftLeg rightLeg leftArm rightArm head beatingHeart =
    {
    leftLeg = leftLeg
    rightLeg = rightLeg
    leftArm = leftArm
    rightArm = rightArm
    head = head
    heart = beatingHeart 
    }

let bodyM = 
    createBody 
    <!> leftLegM
    <*> rightLegM
    <*> leftArmM
    <*> rightArmM
    <*> headM 
    <*> beatingHeartM

では、どこが違うのでしょうか?

見た目には少し違いがありますが、どちらの方法も、好みに応じて選択できる正当な手法です。

しかし、applyMアプローチとmonster式アプローチの間には、もっと重要な違いがあります。

applyMアプローチでは、パラメータを独立してまたは並行して実行できるのに対し、 monster式アプローチでは、パラメータを順序通りに実行する必要があり、一つの出力が次の入力に渡されます。

この違いは今回のシナリオでは重要ではありませんが、バリデーションや非同期処理など、他の場面では大きな意味を持つことがあります。 バリデーションを例に挙げると、最初に見つかったエラーだけを報告するのではなく、すべてのエラーを一度に集めて報告したい場合があります。

Stateモナドとの関係

フランケンファンクター博士は当時の先駆者でしたが、自身の発見を他の領域に一般化することはありませんでした。

現在では、一連の関数を通じて何らかの情報を受け渡すこのパターンは非常に一般的で、「Stateモナド」という標準的な名前が付けられています。

真のモナドであるためには、様々な性質(いわゆるモナド則)を満たす必要がありますが、 この記事はモナドのチュートリアルを意図したものではないので、ここでは議論しません。

代わりに、Stateモナドが実際にどのように定義され、使用されるかに焦点を当てます。

まず、真に再利用可能にするためには、VitalForce型を他の型に置き換える必要があります。そのため、関数をラップする型(ここではSと呼びます)には2つの型パラメータが必要です。 1つは状態の型用、もう1つは値の型用です。

type S<'State,'Value> = 
    S of ('State -> 'Value * 'State)

これを定義したら、通常のrunSreturnSbindSを作成できます。

// 状態を「実行する」関数呼び出しをカプセル化
let runS (S f) state = f state

// 値をS世界に持ち上げる 
let returnS x = 
    let run state = 
        x, state
    S run

// モナディック関数をS世界に持ち上げる 
let bindS f xS = 
    let run state = 
        let x, newState = runS xS state
        runS (f x) newState 
    S run

個人的には、Mコンテキストでのしくみをを理解した後で、完全に一般化したものを説明するという順序でよかったと思います。以下のようなシグネチャは、

val runS : S<'a,'b> -> 'a -> 'b * 'a
val bindS : f:('a -> S<'b,'c>) -> S<'b,'a> -> S<'b,'c>

前提知識なしでは非常に理解しづらいものです。

さて、これらの基本が整ったら、state式を作成できます。

type StateBuilder()=
    member this.Return(x) = returnS x
    member this.Bind(xS,f) = bindS f xS

let state = new StateBuilder()

getSputSは、monstergetMputMと同様の方法で定義されます。

let getS = 
    let run state = 
        // タプルの最初の要素に現在の状態を返す
        state, state
    S run
// val getS : S<State> 

let putS newState = 
    let run _ = 
        // タプルの最初の要素には何も返さない
        // タプルの2番目の要素に新しい状態を返す
        (), newState
    S run
// val putS : 'State -> S<unit>

state式のプロパティベースのテスト

先に進む前に、stateの実装が正しいことをどのように確認できるでしょうか?そもそも「正しい」とは何を意味するのでしょうか?

これは、実例ベースのテストをたくさん書くよりも、プロパティベースのテストのアプローチが適している場合の候補です。

満たすべきプロパティには以下のようなものがあります。

  • モナド則
  • 最後のputだけが有効。つまり、XをputしてからYをputするのは、単にYをputするのと同じであるべきです。
  • getは最後のputを返す。つまり、Xをputしてからgetを行うと、同じXが返されるべきです。

などです。

ここではこれ以上詳しく説明しません。より詳細な議論については、講演をご覧ください。

state式をmonster式の代わりに使用する

これで、state式をmonster式と全く同じように使用できます。以下に例を示します。

// getとputを組み合わせて1単位を抽出する
let useUpOneUnitS = state {
    let! vitalForce = getS
    let oneUnit, remainingVitalForce = getVitalForce vitalForce 
    do! putS remainingVitalForce 
    return oneUnit
    }

type DeadLeftLeg = DeadLeftLeg of Label 
type LiveLeftLeg = LiveLeftLeg of Label * VitalForce

// 生命力の暗黙的な処理を行う新バージョン
let makeLiveLeftLeg (DeadLeftLeg label) = state {
    let! oneUnit = useUpOneUnitS
    return LiveLeftLeg (label,oneUnit)
    }

もう一つの例として、BeatingHeartの作り方を示します。

type DeadHeart = DeadHeart of Label 
type LiveHeart = LiveHeart of Label * VitalForce
type BeatingHeart = BeatingHeart of LiveHeart * VitalForce

let makeLiveHeart (DeadHeart label) = state {
    let! oneUnit = useUpOneUnitS
    return LiveHeart (label,oneUnit)
    }

let makeBeatingHeart liveHeart = state {
    let! oneUnit = useUpOneUnitS
    return BeatingHeart (liveHeart,oneUnit)
    }

let beatingHeartS = state {
    let! liveHeart = DeadHeart "Anne" |> makeLiveHeart 
    return! makeBeatingHeart liveHeart
    }

let beatingHeart, remainingFromHeart = runS beatingHeartS vf

ご覧のように、state式は自動的にVitalForceが状態として使用されていることを認識しました。明示的に指定する必要はありませんでした。

したがって、state式の型が利用可能な場合、monsterのような独自の式を作成する必要はまったくありません!

F#でのStateモナドのより詳細で複雑な例については、FSharpxライブラリをチェックしてください。

注:state式を使用した完全なコードはGitHubで利用可能です。

その他のstate式の使用例

stateコンピュテーション式は、一度定義すれば様々な用途に使用できます。たとえば、stateを使ってスタックをモデル化することができます。

まず、Stack型と関連する関数を定義してみましょう。

// 状態として使用する型を定義
type Stack<'a> = Stack of 'a list

// state式の外でpopを定義
let popStack (Stack contents) = 
    match contents with
    | [] -> failwith "Stack underflow"
    | head::tail ->     
        head, (Stack tail)

// state式の外でpushを定義
let pushStack newTop (Stack contents) = 
    Stack (newTop::contents)

// 空のスタックを定義
let emptyStack = Stack []

// 空のスタックから開始して
// 実行した時のスタックの値を取得
let getValue stackM = 
    runS stackM emptyStack |> fst

これらのコードは、stateのコンピュテーション式について何も知らず、使用もしていないことに注意してください。

stateで動作させるには、state式のコンテキストで使用するためのカスタマイズされたゲッターとセッターを定義する必要があります。

let pop() = state {
    let! stack = getS
    let top, remainingStack = popStack stack
    do! putS remainingStack 
    return top
    }

let push newTop = state {
    let! stack = getS
    let newStack = pushStack newTop stack
    do! putS newStack 
    return ()
    }

これらが用意できたら、ドメインのコーディングを始めることができます!

スタックベースのHello World

簡単な例を見てみましょう。「world」をプッシュし、次に「hello」をプッシュし、その後スタックをポップして結果を組み合わせます。

let helloWorldS = state {
    do! push "world"
    do! push "hello" 
    let! top1 = pop()
    let! top2 = pop()
    let combined = top1 + " " + top2 
    return combined 
    }

let helloWorld = getValue helloWorldS // "hello world"

スタックベースの計算機

こちらは簡単なスタックベースの計算機です。

let one = state {do! push 1}
let two = state {do! push 2}

let add = state {
    let! top1 = pop()
    let! top2 = pop()
    do! push (top1 + top2)
    }

そして、これらの基本的なstate値を組み合わせて、より複雑なものを構築できます。

let three = state {
    do! one
    do! two
    do! add
    }

let five = state {
    do! two
    do! three
    do! add
    }

生命力の場合と同様に、今のところスタックを構築するレシピがあるだけです。レシピを実行して結果を得るには、まだ実行する必要があります。

すべての操作を実行してスタックのトップを返すヘルパーを追加しましょう。

let calculate stackOperations = state {
    do! stackOperations
    let! top = pop()
    return top 
    }

これで、次のように演算を評価できます。

let threeN = calculate three |> getValue // 3

let fiveN = calculate five |> getValue   // 5

はいはい、モナドの話ですね。少しだけ触れておきましょう

モナドについて知りたがる人はいつもいますが、これ以上モナドのチュートリアルを書くつもりはありません

ですので、これまでの内容とモナドの関係を簡単に説明します。

ファンクターは、(プログラミングの意味では)それに関連付けられたmap関数を持つデータ構造(OptionやList、Stateなど)です。 そして、map関数は満たすべきいくつかの性質(ファンクター則)があります。

アプリカティブファンクターは、(プログラミングの意味では)それに関連付けられた2つの関数 applypure(これはreturnと同じです)を持つデータ構造(OptionやList、Stateなど)です。 そして、これらの関数には満たすべきいくつかの性質(アプリカティブファンクター則)があります。

最後に、モナドは、(プログラミングの意味では)それに関連付けられた2つの関数 bind(しばしば>>=と書かれます)とreturn を持つデータ構造(OptionやList、Stateなど)です。 そして再び、これらの関数には満たすべきいくつかの性質(モナド則)があります。

これら3つのうち、モナドが最も強力です。なぜなら、bind関数によってMを生成する関数を連鎖させることができ、 見てきたように、mapapplybindreturnを使って書くことができるからです。

したがって、元のM型も、より一般的なState型のどちらも、サポート関数との組み合わせで、モナドになっていることがわかるでしょう (bindreturnの実装がモナド則を満たしていると仮定します)。

これらの定義の視覚的なバージョンについては、Functors, Applicatives, And Monads In Picturesという素晴らしい投稿があります。

補足文献

Web上にはStateモナドに関する多くの投稿がありますが、そのほとんどはHaskellに基づいています。しかし、この連載を読んだ後では、それらの説明がより理解しやすくなると思います。 そのため、ここではフォローアップリンクをいくつか紹介するだけにとどめます。

そして、「bind」のもう一つの重要な使用例については、関数型エラー処理に関する私の講演が役立つかもしれません。

F#での他のモナドの実装を見たい場合は、FSharpxプロジェクトを見てください。

まとめ

フランケンファンクター博士は画期的な実験者でした。彼女の仕事の方法について洞察を共有できて嬉しく思います。

彼女が原始的なモナドといえる型 M を発見したこと、 そして mapMmap2MreturnMbindMapplyM がいずれも特定の問題を解決するために開発された経緯を学びました。

また、同じ問題を解決する必要性が、現代のStateモナドとコンピュテーション式にどのようにつながったかも見てきました。

この連載が啓発的だったことを望んでいます。 そして密かに願っているのですが、モナドとそれに関連するコンビネータが、もはやあなたを怖がらせるようなものではなくなっていることを期待しています...

shocking

...また、それらをあなた自身のプロジェクトで上手く活用できるようになることも願っています。頑張ってください!

注:この投稿で使用されたコードサンプルはGitHubで利用可能です。

results matching ""

    No results matching ""