機能的思考。 パート4

基本型に少し脱線した後、関数に再び戻ることができます。 特に、前述のなぞなぞに、数学関数がパラメーターを1つしか取得できない場合、F#でより多くのパラメーターを取得する関数はどうすればよいでしょうか? 詳細はこちら!















答えは非常に簡単です。複数のパラメーターを持つ関数は、それぞれが1つのパラメーターのみをとる一連の新しい関数として書き換えられます。 コンパイラーはこの操作を自動的に実行し、関数型プログラミングの開発に大きな影響を与えた数学者であるHaskell Curryを称えて「 カリー化 」と呼ばれます。







カレーが実際にどのように機能するかを見るために、2つの数字を出力する簡単なコード例を使用してみましょう。







//   let printTwoParameters xy = printfn "x=%iy=%i" xy
      
      





実際、コンパイラーはほぼ次の形式で書き換えます。







 //    let printTwoParameters x = //    let subFunction y = printfn "x=%iy=%i" xy //  ,    subFunction //  
      
      





このプロセスをより詳細に検討してください。







  1. printTwoParameters



    」という関数がprintTwoParameters



    いますが、 1つのパラメーター「x」のみを受け入れます。
  2. その内部にローカル関数が作成されますが、この関数も1つのパラメーター「y」のみを取ります。 ローカル関数はパラメーター "x"を使用しますが、xは引数として渡されないことに注意してください。 「x」は、ネストされた関数がそれを見て、渡す必要なしにそれを使用できるようなスコープ内にあります。
  3. 最後に、新しく作成されたローカル関数が返されます。
  4. 返された関数は、引数「y」に適用されます。 パラメータ「x」はその中で閉じられるため、返される関数は、そのロジックを完了するためにパラメータ「y」のみを必要とします。


このように関数を書き換えることにより、コンパイラは各関数が必要に応じて1つのパラメーターのみを受け入れるようにします。 したがって、「 printTwoParameters



」を使用すると、これは2つのパラメーターを持つ関数であると考えるかもしれませんが、実際には1つのパラメーターのみを持つ関数が使用されます。 これを確認するには、2つではなく1つの引数のみを渡します。







 //     printTwoParameters 1 //    val it : (int -> unit) = <fun:printTwoParameters@286-3>
      
      





1つの引数で計算すると、エラーは発生しません。関数が返されます。







したがって、 printTwoParameters



が2つの引数で呼び出されたときに実際に起こることはprintTwoParameters



です。









以下に、ステップバイステップおよび通常バージョンの例を示します。







 //   let x = 6 let y = 99 let intermediateFn = printTwoParameters x //  -  // x   let result = intermediateFn y //     let result = (printTwoParameters x) y //   let result = printTwoParameters xy
      
      





別の例を次に示します。







 //  let addTwoParameters xy = x + y //   let addTwoParameters x = //   ! let subFunction y = x + y //      subFunction //   //       let x = 6 let y = 99 let intermediateFn = addTwoParameters x //  -  // x   let result = intermediateFn y //   let result = addTwoParameters xy
      
      





繰り返しますが、「2つのパラメーターを持つ関数」は、実際には1つのパラメーターを持つ関数であり、中間関数を返します。







しかし、待ってください、 +



演算子はどうですか? これは2つのパラメーターを必要とするバイナリ操作ですか? いいえ、他の機能と同様にカレーもあります。 これは、上記のaddTwoParameters



と同様に、1つのパラメーターを取り、新しい中間関数を返す「 +



」と呼ばれる関数です。







x+y



を記述すると、コンパイラ 、中置記号を(+) xy



に変換するようにコードを並べ替え ます。これは、 +



という名前の関数で、2つのパラメーターを取ります。
「+」関数は、中置演算子としてではなく、通常の関数として使用されることを示すために括弧が必要であることに注意してください。







最後に、 +



という2つのパラメーターを持つ関数は、2つのパラメーターを持つ他の関数と同様に扱われます。







 //         let x = 6 let y = 99 let intermediateFn = (+) x //   ""  ""   let result = intermediateFn y //        let result = (+) xy //       let result = x + y
      
      





はい、これは他のすべての演算子とprintf



などの組み込み関数で機能します。







 //    let result = 3 * 5 //    - let intermediateFn = (*) 3 //  ""  3   let result = intermediateFn 5 //    printfn let result = printfn "x=%iy=%i" 3 5 // printfn   - let intermediateFn = printfn "x=%iy=%i" 3 // "3"   let result = intermediateFn 5
      
      





カリー化された関数シグネチャ



カリー化された関数がどのように機能するかがわかったので、その署名がどのように見えるかを知ることは興味深いです。







最初の例「 printTwoParameter



」に戻ると、関数が1つの引数を取り、中間関数を返すことがわかりました。 中間関数も1つの引数を取り、何も返しませんでした(つまりunit



)。 したがって、中間関数はint->unit



int->unit



。 つまり、ドメインprintTwoParameters



int



であり、範囲はint->unit



です。 これらすべてをまとめると、最終的な署名が表示されます。







 val printTwoParameters : int -> (int -> unit)
      
      





明示的にカリー化された実装を計算する場合、署名に角括弧が表示されますが、通常の暗黙的にカリー化された実装を計算する場合、角括弧はありません。







 val printTwoParameters : int -> int -> unit
      
      





ブラケットはオプションです。 しかし、それらは、関数シグネチャの認識を単純化するために心の中で表現することができます。







そして、中間関数を返す関数と2つのパラメーターを持つ通常の関数の違いは何ですか?







以下は、別の関数を返す1つのパラメーターを持つ関数です。







 let add1Param x = (+) x // signature is = int -> (int -> int)
      
      





そして、これは単純な値を返す2つのパラメーターを持つ関数です。







 let add2Params xy = (+) xy // signature is = int -> int -> int
      
      





それらの署名はわずかに異なりますが、実用的な意味では、2番目の関数が自動的にカリー化されるという事実を除いて、それらの間に大きな違いはありません。







3つ以上のパラメーターを持つ関数



3つ以上のパラメーターを持つ関数のカレーはどのように機能しますか? 同様に、最後のパラメーターを除く各パラメーターについて、関数は前のパラメーターを閉じる中間関数を返します。







この難しい例を考えてみましょう。 パラメーターの型を明示的に宣言しましたが、関数は何もしません。







 let multiParamFn (p1:int)(p2:bool)(p3:string)(p4:float)= () //   let intermediateFn1 = multiParamFn 42 // multoParamFn  int   (bool -> string -> float -> unit) // intermediateFn1  bool //   (string -> float -> unit) let intermediateFn2 = intermediateFn1 false // intermediateFn2  string //   (float -> unit) let intermediateFn3 = intermediateFn2 "hello" // intermediateFn3 float //     (unit) let finalResult = intermediateFn3 3.141
      
      





関数全体の署名:







 val multiParamFn : int -> bool -> string -> float -> unit
      
      





および中間関数の署名:







 val intermediateFn1 : (bool -> string -> float -> unit) val intermediateFn2 : (string -> float -> unit) val intermediateFn3 : (float -> unit) val finalResult : unit = ()
      
      





関数のシグネチャは、関数が受け取るパラメーターの数を示します。括弧の外側の矢印の数を数えるだけです。 関数が別の関数を受け入れるか返す場合、さらに矢印が表示されますが、括弧内に表示され、無視できます。 以下に例を示します。







 int->int->int // 2  int  int string->bool->int //   string,  - bool, //  int int->string->bool->unit //   (int,string,bool) //    (unit) (int->string)->int //   ,  // ( int  string) //   int (int->string)->(int->bool) //   (int  string) //   (int  bool)
      
      





複数のパラメーターの問題



カリー化の背後にあるロジックを理解するまで、予期しない結果が生じます。 予想よりも少ない引数で関数を実行してもエラーは発生しないことに注意してください。 代わりに、部分的に適用された関数を取得します。 その後、値が期待されるコンテキストで部分的に適用された関数を使用すると、コンパイラから不明瞭なエラーが発生する可能性があります。







一見無害な関数を考えてみましょう:







 //   let printHello() = printfn "hello"
      
      





以下に示すように呼び出すとどうなると思いますか? 「hello」はコンソールに出力されますか? 実行する前に推測してみてください。 ヒント:関数のシグネチャを見てください。







 //   printHello
      
      





期待に反して、電話はありません 。 元の関数では、渡されていない引数としてunit



が必要です。 したがって、部分的に適用された関数が取得されました(この場合、引数なし)。







この場合はどうですか? コンパイルされますか?







 let addXY xy = printfn "x=%iy=%i" x x + y
      
      





実行すると、コンパイラーはprintfn



の行について文句をprintfn



ます。







 printfn "x=%iy=%i" x //^^^^^^^^^^^^^^^^^^^^^ //warning FS0193: This expression is a function value, ie is missing //arguments. Its type is ^a -> unit.
      
      





カリー化について理解していない場合、このメッセージは非常にわかりにくい場合があります。 実際には、個別に評価される(つまり、戻り値として使用されない、または "let"によって何かにバインドされる)すべての式は、 unit



値で評価する必要があります。 この場合、 unit



値で計算され 、代わりに関数が返されます。 これは、 printfn



引数がないことをprintfn



長い言い方です。







ほとんどの場合、.NETの世界のライブラリを操作すると、このようなエラーが発生します。 たとえば、 TextReader



クラスのReadline



メソッドは、 unit



パラメーターを取る必要があります。 多くの場合、これを忘れて角括弧を入れないことがあります。この場合、「呼び出し」の時点でコンパイラエラーを取得できませんが、結果を文字列として解釈しようとすると表示されます。







 let reader = new System.IO.StringReader("hello"); let line1 = reader.ReadLine // ,    printfn "The line is %s" line1 //    // ==> error FS0001: This expression was expected to have // type string but here has type unit -> string let line2 = reader.ReadLine() // printfn "The line is %s" line2 //  
      
      





上記のコードでは、 line1



は単なる文字列ではなく、 Readline



メソッドへのポインターまたはデリゲートです。 reader.ReadLine()



()



()



を使用すると、実際に関数が呼び出されます。







オプションが多すぎます



関数に渡すパラメーターが多すぎる場合、同様に暗号化されたメッセージを受け取ることができます。 あまりにも多くのパラメーターをprintf



に渡す例:







 printfn "hello" 42 // ==> error FS0001: This expression was expected to have // type 'a -> 'b but here has type unit printfn "hello %i" 42 43 // ==> Error FS0001: Type mismatch. Expecting a 'a -> 'b -> 'c // but given a 'a -> unit printfn "hello %i %i" 42 43 44 // ==> Error FS0001: Type mismatch. Expecting a 'a->'b->'c->'d // but given a 'a -> 'b -> unit
      
      





たとえば、後者の場合、コンパイラは、3つのパラメーターを持つ書式文字列が期待されることを報告します(署名'a -> 'b -> 'c -> 'd



a- 'a -> 'b -> 'c -> 'd



は3つのパラメーターがあります)が、代わりに2つの文字列が受信されます(署名'a -> 'b -> unit



a- 'a -> 'b -> unit



2パラメーター)。







printf



が使用されない場合、多くのパラメーターを渡すことは、計算の特定の段階で、パラメーターが渡そうとしている単純な値が取得されたことを意味することがよくあります。 コンパイラは、単純な値が関数ではないことをresします。







 let add1 x = x + 1 let x = add1 2 3 // ==> error FS0003: This value is not a function // and cannot be applied
      
      





前に行ったように、一般的な呼び出しを一連の明示的な中間関数に分解すると、正確に何が間違っているのかがわかります。







 let add1 x = x + 1 let intermediateFn = add1 2 //   let x = intermediateFn 3 //intermediateFn  ! // ==> error FS0003: This value is not a function // and cannot be applied
      
      





追加のリソース



F#には、C#またはJavaの経験がある人向けの資料など、多くのチュートリアルがあります。 次のリンクは、F#の詳細を説明するのに役立ちます。









F#の学習を開始する他のいくつかの方法についても説明します。







最後に、F#コミュニティは非常に初心者に優しいです。 Slackには、F#Software Foundationがサポートする非常に活発なチャットがあり、 自由に参加できる初心者ルームがあります。 これを行うことを強くお勧めします!







ロシア語を話すコミュニティF#のサイトを訪れることを忘れないでください! 言語の学習について質問がある場合は、チャットルームでお気軽にご相談ください。









翻訳著者について



@kleidemosによる翻訳

翻訳と編集上の変更は、F#開発者のロシア語コミュニティの努力によって行われました。 また、この記事を公開する準備をしてくれた@schvepsss@shwarsにも感謝します。








All Articles