F#とScalaの関数構成

単にこれについてのすべてを置く



数週間前、7歳の子供に数学関数とは何かを説明しようとした後、この記事を書くことを考え始めました。 私たちは非常に単純なものを見ることから始めました。 これはおかしく聞こえるかもしれませんが、おそらくばかげているように思えますが、機能の構成についての説明で入門説明を終了しました。 関数が何であるかを説明し、外界からの使用例を挙げて、構成について話すのはとても論理的に思えました。 この記事の目的は、関数の構成がどれほどシンプルで強力かを示すことです。 純粋な構図とありふれた説明の概念を検討することから始めます。その後、少しカレーを試してモナドを楽しみます。 楽しんでいただければ幸いです。







小さな箱のような機能



各ボックスが任意の正の数の引数を受け入れ、タスクを実行し、結果を返すことができる小さなボックス(ボックス)の形式の数学関数を想像してみましょう。 つまり、次のように追加機能を導入できます。













画像1、加算関数の英数字表現













図2、加算関数の記号表現







パンファクトリーを1つにまとめて立ち上げる必要がある状況を見てみましょう。 このファクトリは、要求の原則に基づいて構築されます。要求のたびに特定の操作のチェーンがアクティブになり、最終段階で結果が既製パンの形で提供されます。 最初は、これらの非常に具体的な操作を定義する必要があります。各操作を関数/ボックスとして表します。 必要になる可能性のある高次の操作のリストを次に示します。









以下に示すように、単一の生産チェーンをまとめて、パン工場をセットアップします。







w -> [Grind] -> [KneadDough] -> [DistributeDough] -> [Bake] -> b
      
      











図3、組み立てられた回路の表現







これですべてです。私たちのチェーンは準備ができており、小さなピースから組み立てられ、各ピースを別々のサブピースなどに分解できます。 関数の合成の概念を使用するだけで、私たちの周りの世界から膨大な数の物をモデル化できます。 実際には非常に簡単です。 ここで 、より理論的な側面に慣れることができます







構図表現



javascriptを使用して上記の生産チェーンを表示する方法を見てみましょう。







 var b = bake(distribureDough(kneadDough(grind(w))));
      
      





10〜15個の関数のチェーンがどのように見えるか想像してみてください。これは、発生する可能性のある問題の1つにすぎません。 また、完全な構成ではありません。なぜなら、 数学では、関数の合成とは、ある関数を別の関数の結果に個別に適用して、3番目の関数を取得することです。 これは次の方法で実現できます。







 function myChain1(w) { return bake(distribureDough(kneadDough(grind(w)))); } var b = myChain1(w);
      
      





ばかげているように見えますね。 関数型プログラミングの力を呼び出して、より消化しやすい形で実装しましょう。 私たちはより理解しやすい例を使って活動します。 まず、機能概念の構成とは何かを判断する必要があります。







Scalaバージョン


 implicit class Forward[TIn, TIntermediate](f: TIn => TIntermediate) { def >> [TOut](g: TIntermediate => TOut): TIn => TOut = source => g(f(source)) }
      
      





バージョンF


実際、F#には既にデフォルトの合成演算子があり、何も宣言する必要はありません。 ただし、それを再定義する必要がある場合は、次のようにできます。







 let (>>) fgx = g ( f(x) )
      
      





F#コンパイラーは、関数を処理していることを示唆するほどスマートなので、上記の関数のタイプ(>>)



は次のようになります。







 f:('a -> 'b) -> g:('b -> 'c) -> x:'a -> 'c
      
      





すべてを一緒につかむ


前のタスクのソリューションは、 Scalaでは次のようになります。







 object BreadFactory { case class Wheat() case class Flour() case class Dough() case class Bread() def grind: (Wheat => Flour) = w => {println("make the flour"); Flour()} def kneadDough: (Flour => Dough) = f => {println("make the dough"); Dough()} def distributeDough: (Dough => Seq[Dough]) = d => {println("distribute the dough"); Seq[Dough]()} def bake: (Seq[Dough] => Seq[Bread]) = sd => {println("bake the bread"); Seq[Bread]()} def main(args: Array[String]): Unit = { (grind >> kneadDough >> distributeDough >> bake) (Wheat()) } implicit class Forward[TIn, TIntermediate](f: TIn => TIntermediate) { def >> [TOut](g: TIntermediate => TOut): TIn => TOut = source => g(f(source)) } }
      
      





F#バージョンはより簡潔になります。







 type Wheat = {wheat:string} type Flour = {flour:string} type Dough = {dough:string} type Bread = {bread:string} let grind (w:Wheat) = printfn "make the flour"; {flour = ""} let kneadDough (f:Flour) = printfn "make the dough"; {dough = ""} let distributeDough (d:Dough) = printfn "distribute the dough"; seq { yield d} let bake (sd:seq<Dough>) = printfn "bake the bread"; seq { yield {bread = ""}} (grind >> kneadDough >> distributeDough >> bake) ({wheat = ""})
      
      





コンソールへの出力は次のようになります。







 make the flour make the dough distribute the dough bake the bread
      
      





キャリング



カレーの概念に慣れていない場合は、 こちらで詳細を確認できます。 このパートでは、関数型プログラミングの世界の2つの強力なメカニズムであるカリー化と合成を組み合わせます。 複数のパラメーターを持つ関数を使用する必要があり、これらのパラメーターのほとんどが関数自体が実行される前に既知である状況を見てみましょう。 たとえば、前の部分のbake



関数には、温度やベイク時間などのパラメータがありますが、これらは事前によく知られています。







Scala:







 def bake: (Int => Int => Seq[Dough] => Seq[Bread]) = temperature => duration => sd => { println(s"bake the bread, duration: $duration, temperature: $temperature") Seq[Bread]() }
      
      





F#:







 let bake temperature duration (sd:seq<Dough>) = printfn "bake the bread, duration: %d, temperature: %d" temperature duration seq { yield {bread = ""}}
      
      





カレーは私たちの友です。パンを焼くためのレシピを1つ定義しましょう。







Scala:







 def bakeRecipe1 = bake(350)(45) def main(args: Array[String]): Unit = { (grind >> kneadDough >> distributeDough >> bakeRecipe1) (Wheat()) }
      
      





F#:







 let bakeRecipe1: seq<Dough> -> seq<Bread> = bake 350 45 (grind >> kneadDough >> distributeDough >> bakeRecipe1) ({wheat = ""})
      
      





両方の場合の結論は次のとおりです。







 make the flour make the dough distribute the dough bake the bread, duration: 45, temperature: 350
      
      





モナド連鎖



チェーンの途中で何かがうまくいかない状況を想像できますか? たとえば、酵母または水を供給するためのワイヤが詰まって生地の生産が侵害される状況、またはオーブンが壊れて生地の半焼塊が得られる状況です。 機能の純粋な構成は、障害または障害のない対応物に耐えられる問題にとって興味深い場合があります。 しかし、上記の場合はどうしますか? 答えは明らかです-モナドを使用して、うーん。 モナドに関する基本的なことは、 ウィキペディアのページで見つけることができます。 この状況でモナドがどのように役立つかを見てみましょう。最初に、Etherと呼ばれる特別な型を定義(F#で)または使用(Scalaで)する必要があります。 F#の定義は、以下のマークされたユニオンのように見える場合があります。







 type Either<'a, 'b> = | Left of 'a | Right of 'b
      
      





これで、すべての要素を連結する準備が整いました。そのために、モナド演算bindと同等の値を作成する必要があります。







F#:







 let chainFunOrFail twoTrackInput switchFunction = match twoTrackInput with | Left s -> switchFunction s | Right f -> Right f let (>>=) = chainFunOrFail
      
      





Scala:







 implicit class MonadicForward[TLeft, TRight](twoTrackInput: Either[TLeft,TRight]) { def >>= [TIntermediate](switchFunction: TLeft => Either[TIntermediate, TRight]) = twoTrackInput match { case Left (s) => switchFunction(s) case Right (f) => Right(f) } }
      
      





最後に行う必要があるのは、上記のチェーンを、よりEither



やすい新しい形式に少し適応させることです。







F#:







 let grind (w:Wheat): Either<Flour, string> = printfn "make the flour"; Left {flour = ""} let kneadDough (f:Flour) = printfn "make the dough"; Left {dough = ""} let distributeDough (d:Dough) = printfn "distribute the dough"; Left(seq { yield d}) let bake temperature duration (sd:seq<Dough>) = printfn "bake the bread, duration: %d, temperature: %d" duration temperature Left (seq { yield {bread = ""}}) let bakeRecipe1: seq<Dough> -> Either<seq<Bread>, string> = bake 350 45 ({wheat = ""} |> grind) >>= kneadDough >>= distributeDough >>= bakeRecipe1
      
      





Scala:







 def grind: (Wheat => Either[Flour, String]) = w => { println("make the flour"); Left(Flour()) } def kneadDough: (Flour => Either[Dough, String]) = f => { println("make the dough"); Left(Dough()) } def distributeDough: (Dough => Either[Seq[Dough], String]) = d => { println("distribute the dough"); Left(Seq[Dough]()) } def bake: (Int => Int => Seq[Dough] => Either[Seq[Bread], String]) = temperature => duration => sd => { println(s"bake the bread, duration: $duration, temperature: $temperature") Left(Seq[Bread]()) } def bakeRecipe1 = bake(350)(45) def main(args: Array[String]): Unit = { grind(Wheat()) >>= kneadDough >>= distributeDough >>= bakeRecipe1 }
      
      





出力は次のようになります。







 make the flour make the dough distribute the dough bake the bread, duration: 45, temperature: 350
      
      





チェーンの要素の1つが対応するエラーインジケーターと共にRight



を返す場合、チェーンの後続の要素は単に無視され、ワー​​クフローはそれらのすべてをスキップし、スローされた例外を前のリンクから次のリンクに単純に伝播します。 エラーがあるシナリオを試してみることができます。







最後の部分



お気づきかもしれませんが、カテゴリ理論(モナドの起源)と関数の合成の間には、魔法のようなつながりがあります。 この記事の目的は、提示されたメカニズムを実際に管理する方法と、より機能的な方法でコードを編成する方法を示すことです。 自分が提示した資料のより基本的な側面に没頭できます。 この記事が、命令型プログラミングを放棄して機能的思考の方法を理解する方法を探している人、またはモナドと機能的構成の実用的な側面を発見したい人に役立つことを願っています。







参照資料






All Articles