Skip to content

モナド怪物の体を完成させる

更新:このトピックに関する私の講演のスライドとビデオ

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

フランケンファンクター博士とモナド怪物の興味深い物語へようこそ。

前回の記事で、フランケンファンクター博士による「モナド怪物パーツ生成器」(略して「M」)の使用法を紹介しました。 これらの生成器は、死体の部品から生命を作り出すもので、生命力を供給すると生きた体の部位を返します。

また、怪物の脚と腕の作成方法や、 mapM(折れた腕用)とmap2M(2つの部分からなる腕用)を使ったM値の処理と組み合わせについても説明しました。

この第2回では、フランケンファンクター博士が頭、心臓、そして完全な体を作るために使用した他のテクニックを紹介します。

最初に頭の作成について説明します。

右腕と同じく、頭も脳と頭蓋骨の2つの部分で構成されています。

フランケンファンクター博士はまず、死んだ脳と頭蓋骨を次のように定義しました。

type DeadBrain = DeadBrain of Label
type Skull = Skull of Label

二つの部分からなる右腕とは異なり、生きている必要があるのは脳だけです。 頭蓋骨はそのまま使用でき、生きた頭に使用される前に変換する必要はありません。

type LiveBrain = LiveBrain of Label * VitalForce
type LiveHead = {
brain : LiveBrain
skull : Skull // 生きていない
}

生きた脳は頭蓋骨と組み合わされて生きた頭になります。これにはheadSurgery関数を使用します。これは以前のarmSurgeryと似ています。

let headSurgery brain skull =
{brain=brain; skull=skull}

これで生きた頭を作る準備が整いました。では、どのように進めればよいでしょうか。

map2Mを再利用できれば理想的ですが、問題があります。map2Mが機能するためには、頭蓋骨がMでラップされている必要があります。

head

しかし、頭蓋骨は生きる必要がなく、生命力も使用しないので、SkullM<Skull>に変換する特別な関数を作る必要があります。

以前と同じアプローチを使用できます。

  • vitalForceパラメーターを取る内部関数を作成する
  • この場合、vitalForceはそのまま残す
  • 内部関数から元の頭蓋骨と変更されていないvitalForceを返す
  • 内部関数を「M」でラップしてそれを返す

コードは次のようになります。

let wrapSkullInM skull =
let becomeAlive vitalForce =
skull, vitalForce
M becomeAlive

しかし、wrapSkullInMのシグネチャは非常に興味深いものです。

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

頭蓋骨への言及はどこにもありません!

完全に汎用的な関数を作成しました。これは何でもMに変換します。そこで、これの名前を変更しましょう。 returnMと呼ぶことにしますが、他の文脈ではpureunitと呼ばれることもあります。

let returnM x =
let becomeAlive vitalForce =
x, vitalForce
M becomeAlive

それでは、実際に試してみましょう。

まず、生きた脳を作成する方法を定義する必要があります。

let makeLiveBrain (DeadBrain label) =
let becomeAlive vitalForce =
let oneUnit, remainingVitalForce = getVitalForce vitalForce
let liveBrain = LiveBrain (label,oneUnit)
liveBrain, remainingVitalForce
M becomeAlive

次に、死んだ脳と頭蓋骨を用意します。

let deadBrain = DeadBrain "Abby Normal"
let skull = Skull "Yorick"

ちなみに、この変わった名前の脳の由来には、面白い話があるのですが、今はその詳細に立ち入る時間がありません。

異常な脳

次に、死んだ部品から「M」バージョンを構築します。

let liveBrainM = makeLiveBrain deadBrain
let skullM = returnM skull

そして、map2Mを使用して部品を組み合わせます。

let headSurgeryM = map2M headSurgery
let headM = headSurgeryM liveBrainM skullM

今回も、雷が落ちる前にこれらの準備をすべて整えておきます。

生命力が利用可能になったら、生命力を使ってheadMを呼び出せます。

let vf = {units = 10}
let liveHead, remainingFromHead = runM headM vf

そして、次のような結果が得られます。

val liveHead : LiveHead =
{brain = LiveBrain ("Abby normal",{units = 1;});
skull = Skull "Yorick";}
val remainingFromHead : VitalForce =
{units = 9;}

要求通りの、2つのサブコンポーネントで構成された生きた頭が完成しました。

また、頭蓋骨が生命力を使わなかったため、残りの生命力はちょうど9単位になっていることにも注目してください。

もう1つ必要なコンポーネントがあります。それは心臓です。

まず、死んだ心臓と生きた心臓を通常の方法で定義します。

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

しかし、怪物に必要なのは生きた心臓以上のもの ―― 鼓動する心臓です。 鼓動する心臓は、生きた心臓ともう少しの生命力から構築されます。以下のようになります。

type BeatingHeart = BeatingHeart of LiveHeart * VitalForce

生きた心臓を作成するコードは、前の例と非常によく似ています。

let makeLiveHeart (DeadHeart label) =
let becomeAlive vitalForce =
let oneUnit, remainingVitalForce = getVitalForce vitalForce
let liveHeart = LiveHeart (label,oneUnit)
liveHeart, remainingVitalForce
M becomeAlive

鼓動する心臓を作成するコードも非常によく似ています。生きた心臓をパラメータとして受け取り、さらに1単位の生命力を使用し、 鼓動する心臓と残りの生命力を返します。

let makeBeatingHeart liveHeart =
let becomeAlive vitalForce =
let oneUnit, remainingVitalForce = getVitalForce vitalForce
let beatingHeart = BeatingHeart (liveHeart, oneUnit)
beatingHeart, remainingVitalForce
M becomeAlive

これらの関数のシグネチャを見ると、非常に似ていることがわかります。どちらもSomething -> M<SomethingElse>の形式です。

val makeLiveHeart : DeadHeart -> M<LiveHeart>
val makeBeatingHeart : LiveHeart -> M<BeatingHeart>

死んだ心臓を出発点とし、鼓動する心臓を得る必要があります。

heart1

しかし、これを直接行うためのツールがありません。

DeadHeartM<LiveHeart>に変換する関数と、LiveHeartM<BeatingHeart>に変換する関数はあります。

ところが、最初の出力が2番目の入力と互換性がないため、それらを直接つなげることができません。

heart2

そこで、M<LiveHeart>を入力として受け取り、それをM<BeatingHeart>に変換する関数が必要になります。

さらに、この関数を既に持っているmakeBeatingHeart関数から構築したいと考えています。

heart2

以下は、これまで何度も使ってきたパターンを応用した第一案です。

let makeBeatingHeartFromLiveHeartM liveHeartM =
let becomeAlive vitalForce =
// liveHeartMからliveHeartを抽出
let liveHeart, remainingVitalForce = runM liveHeartM vitalForce
// liveHeartを使ってbeatingHeartMを作成
let beatingHeartM = makeBeatingHeart liveHeart
// ここに何が入るでしょうか?
// beatingHeartと残りの生命力を返す
beatingHeart, remainingVitalForce
M becomeAlive

しかし、真ん中には何が入るのでしょうか?beatingHeartMから鼓動する心臓を得るにはどうすればよいでしょうか?答えは、生命力を使ってbeatingHeartMを呼び出すことです (たまたまbecomeAlive関数の中にいるので、手元に生命力があります)。

どの生命力を使えばよいでしょうか?liveHeartを得た後に残った生命力を使用すべきです。

そこで、最終版は次のようになります。

let makeBeatingHeartFromLiveHeartM liveHeartM =
let becomeAlive vitalForce =
// liveHeartMからliveHeartを抽出
let liveHeart, remainingVitalForce = runM liveHeartM vitalForce
// liveHeartを使ってbeatingHeartMを作成
let beatingHeartM = makeBeatingHeart liveHeart
// beatingHeartMを実行してbeatingHeartを取得
let beatingHeart, remainingVitalForce2 = runM beatingHeartM remainingVitalForce
// beatingHeartと残りの生命力を返す
beatingHeart, remainingVitalForce2
// 内部関数をラップして返す
M becomeAlive

最後にremainingVitalForce2を返していることに注目してください。これは両方のステップを実行した後の残りです。

この関数のシグネチャを見ると、次のようになっています。

M<LiveHeart> -> M<BeatingHeart>

これこそ私たちが求めていたものです!

ここでも、makeBeatingHeartをハードコーディングする代わりに関数パラメーターを渡すことで、この関数を汎用化できます。

これをbindMと呼びましょう。以下がそのコードです。

let bindM f bodyPartM =
let becomeAlive vitalForce =
let bodyPart, remainingVitalForce = runM bodyPartM vitalForce
let newBodyPartM = f bodyPart
let newBodyPart, remainingVitalForce2 = runM newBodyPartM remainingVitalForce
newBodyPart, remainingVitalForce2
M becomeAlive

そして、このシグネチャは次のようになります。

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

つまり、Something -> M<SomethingElse>型の関数があれば、それを入力と出力の両方がMであるM<Something> -> M<SomethingElse>型の関数に変換できるということです。

ちなみに、Something -> M<SomethingElse>のようなシグネチャを持つ関数は、しばしばモナディック関数と呼ばれます。

bindMの仕組みを理解すれば、次のようなより簡潔な実装も可能です。

let bindM f bodyPartM =
let becomeAlive vitalForce =
let bodyPart, remainingVitalForce = runM bodyPartM vitalForce
runM (f bodyPart) remainingVitalForce
M becomeAlive

これで、DeadHeartを受け取り、M<BeatingHeart>を生成する関数を作成する方法がわかりました。

heart3

以下がそのコードです。

// 死んだ心臓を作成
let deadHeart = DeadHeart "Anne"
// 生きた心臓生成器(M<LiveHeart>)を作成
let liveHeartM = makeLiveHeart deadHeart
// liveHeartMとmakeBeatingHeart関数から
// 鼓動する心臓生成器(M<BeatingHeart>)を作成
let beatingHeartM = bindM makeBeatingHeart liveHeartM

このコードには複数の中間ステップがありますが、パイピングを使うとより簡潔になります。

let beatingHeartM =
DeadHeart "Anne"
|> makeLiveHeart
|> bindM makeBeatingHeart

bindMの捉え方の一つは、mapMと同様に「関数変換器」だということです。 つまり、任意の「Mを返す」関数を、入力と出力の両方がMである関数に変換するのです。

bindM

mapと同様に、bindも他の多くの文脈で登場します。

たとえば、Option.bindはオプションを生成する関数('a -> 'b option)を、入力と出力の両方がオプションである関数に変換します。 同様に、List.bindはリストを生成する関数('a -> 'b list)を、入力と出力の両方がリストである関数に変換します。

また、関数型エラーハンドリングに関する私の講演でも、bindの別のバージョンについて詳しく説明しています。

bindが重要な理由は、「Mを返す」関数がよく登場するからです。 これらの関数は、一つのステップの出力が次のステップの入力と一致しないため、簡単に連鎖させることができません。

bindMを使うことで、各ステップを入力と出力の両方がMである関数に変換できます。これにより、これらの関数を連結できるようになります。

bindM

いつものように、生命力が到着する前に手順を構築します。今回は、BeatingHeartを作る手順です。

let beatingHeartM =
DeadHeart "Anne"
|> makeLiveHeart
|> bindM makeBeatingHeart

生命力が利用可能になったら、beatingHeartMに生命力を与えて実行します…

let vf = {units = 10}
let beatingHeart, remainingFromHeart = runM beatingHeartM vf

…そして、次のような結果が得られます。

val beatingHeart : BeatingHeart =
BeatingHeart (LiveHeart ("Anne",{units = 1;}),{units = 1;})
val remainingFromHeart : VitalForce =
{units = 8;}

残りの生命力が8単位になっていることに注目してください。2つのステップを実行するのに2単位を使用したからです。

ついに、完全な体を組み立てるために必要な部品がすべて揃いました。

以下がフランケンファンクター博士による生きた体の定義です。

type LiveBody = {
leftLeg: LiveLeftLeg
rightLeg : LiveLeftLeg
leftArm : LiveLeftArm
rightArm : LiveRightArm
head : LiveHead
heart : BeatingHeart
}

ご覧の通り、これまでに開発したすべてのサブコンポーネントが使われています。

フランケンファンクター博士は右足を入手できませんでした。そこで近道を選び、体に二つの左足を使うことにしました。誰も気づかないことを願いながらです。

その結果、怪物は二つの左足を持つことになりました。これは必ずしも障害にはなりません。実際、 怪物はこの不利な条件を克服しただけでなく、かなりの腕前のダンサーになりました。以下の貴重な映像でそれを確認できます。

Gene Wilder - Young Frankenstein (1974) - Puttin’ on the Ritz

サブコンポーネントの組み立て

Section titled “サブコンポーネントの組み立て”

LiveBody型には6つのフィールドがあります。これらの様々なM<BodyPart>からどのようにして構築できるでしょうか。

一つの方法は、mapMmap2Mで使用した技術を繰り返すことです。map3Mmap4Mなどを作成できます。

たとえば、map3Mは次のように定義できます。

let map3M f m1 m2 m3 =
let becomeAlive vitalForce =
let v1,remainingVitalForce = runM m1 vitalForce
let v2,remainingVitalForce2 = runM m2 remainingVitalForce
let v3,remainingVitalForce3 = runM m3 remainingVitalForce2
let v4 = f v1 v2 v3
v4, remainingVitalForce3
M becomeAlive

しかし、こんな繰り返しはすぐに面倒になります。もっと良い方法はないでしょうか。

実は、あります!

ここで、重要なポイントを思い出してください。 LiveBodyのようなレコード型は、一度にすべてを構築する必要があります。一方、関数は違います。カリー化と部分適用という魔法のおかげで、関数は段階的に組み立てることができるのです。

この考え方を使って、LiveBodyを作成する6パラメータの関数を見てみましょう。

val createBody :
leftLeg:LiveLeftLeg ->
rightLeg:LiveLeftLeg ->
leftArm:LiveLeftArm ->
rightArm:LiveRightArm ->
head:LiveHead ->
beatingHeart:BeatingHeart ->
LiveBody

これを実際には5パラメータの関数を返す1パラメータの関数として考えることができます。

val createBody :
leftLeg:LiveLeftLeg -> (5パラメータの関数)

そして、最初のパラメータ(“leftLeg”)に関数を適用すると、5パラメータの関数が返ってきます。

(6パラメータの関数) apply (最初のパラメータ) returns (5パラメータの関数)

この5パラメータの関数は、次のようなシグネチャになります。

rightLeg:LiveLeftLeg ->
leftArm:LiveLeftArm ->
rightArm:LiveRightArm ->
head:LiveHead ->
beatingHeart:BeatingHeart ->
LiveBody

この5パラメータの関数も、4パラメータの関数を返す1パラメータの関数として考えることができます。

rightLeg:LiveLeftLeg -> (4パラメータの関数)

再び、最初のパラメータ(“rightLeg”)を適用すると、4パラメータの関数が返ってきます。

(5パラメータの関数) apply (最初のパラメータ) returns (4パラメータの関数)

4パラメータの関数は次のようなシグネチャになります。

leftArm:LiveLeftArm ->
rightArm:LiveRightArm ->
head:LiveHead ->
beatingHeart:BeatingHeart ->
LiveBody

このプロセスは続き、最終的に1パラメータの関数になります。この関数のシグネチャはBeatingHeart -> LiveBodyとなります。

最後のパラメータ(“beatingHeart”)を適用すると、完成したLiveBodyが返ってきます。

この巧妙な手法をM-関連のものにも使えます!

まず、Mでラップされた6パラメータの関数と、Mパラメータから始めます。

M-関数をM-パラメータに「適用」する方法があると仮定しましょう。すると、Mでラップされた5パラメータの関数が返ってくるはずです。

// 通常バージョン
(6パラメータの関数) apply (最初のパラメータ) returns (5パラメータの関数)
// Mの世界バージョン
M<6パラメータの関数> applyM M<最初のパラメータ> 返す M<5パラメータの関数>

そして、これを繰り返すと、次のM-パラメータを適用できます。

// 通常バージョン
(5パラメータの関数) apply (最初のパラメータ) returns (4パラメータの関数)
// Mの世界バージョン
M<5パラメータの関数> applyM M<最初のパラメータ> 返す M<4パラメータの関数>

このように、パラメータを一つずつ適用していき、最終結果を得ます。

このapplyM関数は、Mでラップされた関数とMでラップされたパラメータの2つのパラメータを持つことになります。 出力は、関数の結果をMでラップしたものになります。

以下が実装です。

let applyM mf mx =
let becomeAlive vitalForce =
let f,remainingVitalForce = runM mf vitalForce
let x,remainingVitalForce2 = runM mx remainingVitalForce
let y = f x
y, remainingVitalForce2
M becomeAlive

ご覧の通り、これはmap2Mとよく似ていますが、「f」は最初のパラメータ自体をアンラップすることで得られる点が異なります。

では、試してみましょう!

まず、6パラメータの関数が必要です。

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

そして、左足をクローンして右足として使用する必要があります。

let rightLegM = leftLegM

次に、このcreateBody関数をMでラップする必要があります。どのようにすればいいでしょうか。

もちろん、先ほど頭蓋骨用に定義したreturnM関数を使えばいいのです!

これらを組み合わせると、次のようなコードになります。

// createBodyをM-世界に移動 -- Mでラップされた6パラメータの関数
let fSixParamM = returnM createBody
// 最初のM-パラメータを適用して、Mでラップされた5パラメータの関数を得る
let fFiveParamM = applyM fSixParamM leftLegM
// 2番目のM-パラメータを適用して、Mでラップされた4パラメータの関数を得る
let fFourParamM = applyM fFiveParamM rightLegM
// 以下同様
let fThreeParamM = applyM fFourParamM leftArmM
let fTwoParamM = applyM fThreeParamM rightArmM
let fOneParamM = applyM fTwoParamM headM
// 最後の適用後、結果はM<LiveBody>になる
let bodyM = applyM fOneParamM beatingHeartM

うまくいきました!結果は望み通りのM<LiveBody>になりました。

しかし、このコードは見栄えがよくありません。どうすれば良いのでしょうか。

一つの方法は、applyMを通常の関数適用のような中置演算子にすることです。この演算子は一般的に<*>と書かれます。

let (<*>) = applyM

これを使用すると、上記のコードを次のように書き直すことができます。

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

これでずっと見やすくなりました!

もう一つの工夫として、returnMapplyMを続けるのはmapMと同じであることに気づくことです。そこで、mapM用の中置演算子も作成すると…

let (<!>) = mapM

returnMも取り除くことができ、次のようにコードを書けます。

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

この書き方には大きな利点があります。記号に慣れてしまえば、まるで元の関数をそのまま呼び出しているかのように読めるのです。

いつものように、生命力が到着する前に事前に手順を構築しておきたいと思います。この場合、生命力が到着したときに完全なLiveBodyを提供するbodyMをすでに作成しました。

あとは稲妻が落ちて、生命力を生成する機械に電力が送られるのを待つだけです!

研究室の電気
出典:Misfit Robot Daydream

来ました ―― 生命力が利用可能になりました!急いで通常の方法でbodyMを実行します…

let vf = {units = 10}
let liveBody, remainingFromBody = runM bodyM vf

…すると、次のような結果が得られます。

val liveBody : LiveBody =
{leftLeg = LiveLeftLeg ("Boris",{units = 1;});
rightLeg = LiveLeftLeg ("Boris",{units = 1;});
leftArm = LiveLeftArm ("Victor",{units = 1;});
rightArm = {lowerArm = LiveRightLowerArm ("Tom",{units = 1;});
upperArm = LiveRightUpperArm ("Jerry",{units = 1;});};
head = {brain = LiveBrain ("Abby Normal",{units = 1;});
skull = Skull "Yorick";};
heart = BeatingHeart (LiveHeart ("Anne",{units = 1;}),{units = 1;});}
val remainingFromBody : VitalForce = {units = 2;}

生きています!フランケンファンクター博士の成果を見事に再現できました!

注目すべき点が2つあります。まず、体に正しいサブコンポーネントがすべて含まれていることです。 そして、残りの生命力が正しく2単位に減少していることです。これは体を作るのに8単位使用したからです。

この投稿では、以下の操作テクニックを追加で紹介しました。

  • 頭蓋骨用のreturnM
  • 鼓動する心臓用のbindM
  • 全身を組み立てるためのapplyM

この投稿で使用したコードサンプルはGitHubで入手可能です。

最終回では、コードをリファクタリングし、使用したすべてのテクニックを振り返ります。