F#で関数型プログラミングに関する一連の記事を続けます。 今日、非常に興味深いトピックがあります:関数の定義です。 含めて、匿名関数、パラメーターのない関数、再帰関数、コンビネーターなどについて話しましょう。 猫の下を見てください!
関数定義
「let」構文を使用して通常の関数を作成する方法は既に知っています。
let add xy = x + y
この記事では、関数を作成する他の方法と、それらを定義するためのヒントを見ていきます。
無名関数(ラムダ)
他の言語のラムダに精通している場合、以下の段落はおなじみのように思えます。 無名関数(または「ラムダ式」)は、次のように定義されます。
fun parameter1 parameter2 etc -> expression
C#のラムダと比較すると、2つの違いがあります。
- ラムダは
fun
キーワードで始まる必要がありますが、これはC#では必要ありません - C#のdouble
=>
代わりに、単一矢印->
使用されます。
加算関数のラムダ定義:
let add = fun xy -> x + y
従来の形式の同じ機能:
let add xy = x + y
ラムダは、小さな式の形で、または式に別の関数を定義する必要がない場合によく使用されます。 すでに見たように、リストを操作するとき、これは珍しいことではありません。
// let add1 i = i + 1 [1..10] |> List.map add1 // [1..10] |> List.map (fun i -> i + 1)
ラムダの周りに括弧を使用する必要があることに注意してください。
ラムダは、明らかに異なる機能が必要な場合にも使用されます。 たとえば、前に説明した「 adderGenerator
」は、ラムダを使用して書き換えることができます。
// let adderGenerator x = (+) x // let adderGenerator x = fun y -> x + y
ラムダバージョンは少し長くなりますが、中間関数が返されることがすぐにわかります。
ラムダはネストできます。 adderGenerator
定義の別の例、今回はラムダのみ。
let adderGenerator = fun x -> (fun y -> x + y)
3つの定義すべてが同等であることは明らかですか?
let adderGenerator1 xy = x + y let adderGenerator2 x = fun y -> x + y let adderGenerator3 = fun x -> (fun y -> x + y)
そうでない場合は、 カリー化に関する章を読み直してください。 これは理解するために非常に重要です!
パターンマッチング
関数が定義されると、上の例のように明示的にパラメーターを渡すことができますが、パラメーターセクションでテンプレートと直接比較することもできます。 つまり、パラメータセクションには、識別子だけでなく、パターン(一致するパターン)が含まれている可能性があります。
次の例は、関数定義でのパターンの使用を示しています。
type Name = {first:string; last:string} // let bob = {first="bob"; last="smith"} // // let f1 name = // let {first=f; last=l} = name // printfn "first=%s; last=%s" fl // let f2 {first=f; last=l} = // printfn "first=%s; last=%s" fl // f1 bob f2 bob
このタイプの一致は、一致が常に解決可能な場合にのみ発生します。 たとえば、場合によっては照合できないため、この方法ではユニオンタイプとリストを照合できません。
let f3 (x::xs) = // printfn "first element is=%A" x
コンパイラーは、不完全なマッチングに関する警告を出します(空のリストは、この関数の入り口でランタイムにエラーを引き起こします)。
よくある間違い:タプルvs. 多くのパラメータ
Cライクな言語から来た場合、関数の唯一の引数として使用されるタプルは、マルチパラメーター関数に非常に似ている可能性があります。 しかし、これは同じものではありません! 前述したように、コンマが表示される場合、これはおそらくタプルです。 パラメーターはスペースで区切られます。
混乱の例:
// let addTwoParams xy = x + y // - let addTuple aTuple = let (x,y) = aTuple x + y // // let addConfusingTuple (x,y) = x + y
- 最初の定義「
addTwoParams
」は、スペースで区切られた2つのパラメーターを取ります。 - 2番目の定義「
addTuple
」は、1つのパラメーターを取ります。 このパラメーターは、タプルから「x」と「y」をバインドし、それらを合計します。 - 3番目の定義「
addConfusingTuple
」は、「addConfusingTuple
」のような単一のパラメーターを取りますが、トリックは、このタプルが解凍され(パターンに一致)、パターンマッチングを使用してパラメーター定義の一部としてバインドされることです。 舞台裏では、すべてがaddTuple
とまったく同じようにaddTuple
ます。
署名を見てみましょう(何かわからない場合は、常に署名を見てください)。
val addTwoParams : int -> int -> int // val addTuple : int * int -> int // tuple->int val addConfusingTuple : int * int -> int // tuple->int
そして今ここに:
// addTwoParams 1 2 // ok -- addTwoParams (1,2) // error - // => error FS0001: This expression was expected to have type // int but here has type 'a * 'b
ここでは、2番目の呼び出しでエラーが表示されます。
最初に、コンパイラは(1,2)
を('a * 'b)
形式の一般化タプルとして扱います。これは、「 addTwoParams
」の最初のパラメータとして渡そうとします。 その後、期待される最初のパラメーターaddTwoParams
int
でaddTwoParams
ないが、タプルを渡す試みが行われたと文句を言います。
タプルを作成するには、コンマを使用してください!
addTuple (1,2) // ok addConfusingTuple (1,2) // ok let x = (1,2) addTuple x // ok let y = 1,2 // , // ! addTuple y // ok addConfusingTuple y // ok
逆に、タプルを待機している関数に複数の引数を渡すと、理解できないエラーが発生します。
addConfusingTuple 1 2 // error -- // => error FS0003: This value is not a function and // cannot be applied
今回、コンパイラは、2つの引数がaddConfusingTuple
れaddConfusingTuple
、 addConfusingTuple
をカリー化することを決定しました。 エントリ「 addConfusingTuple 1
」は部分的なアプリケーションであり、中間関数を返す必要があります。 パラメーター「2」でこの中間関数を呼び出そうとすると、エラーがスローされます。 中間機能はありません! カリー化の章と同じエラーが表示されます。カリー化では、パラメーターが多すぎる問題について説明しました。
タプルをパラメーターとして使用しないのはなぜですか?
上記のタプルの説明は、多くのパラメーターを持つ関数を定義する別の方法を示しています。それらを個別に渡す代わりに、すべてのパラメーターを1つの構造にアセンブルできます。 以下の例では、関数は1つのパラメーター(3つの要素のタプル)を取ります。
let f (x,y,z) = x + y * z // - int * int * int -> int // f (1,2,3)
署名は、3つのパラメーターを持つ関数の署名とは異なることに注意してください。 タプル(int*int*int)
指す矢印、パラメーター、アスタリスクは1つだけです。
別々のパラメーターで引数を送信する必要がある場合、およびタプルで送信する場合
- タプル自体が重要な場合。 たとえば、3次元空間での操作の場合、3組のタプルは3つの座標を別々に使用するよりも便利です。
- タプルを使用して、一緒に格納する必要のあるデータを単一の構造に結合する場合があります。 たとえば、.NETライブラリの
TryParse
メソッドは、結果とブール変数をタプルとして返します。 しかし、多くの関連データを保存するには、クラスまたはレコード( recordを定義する方が良いでしょう。
特別な場合:.NETライブラリのタプルと関数
.NETライブラリを呼び出すとき、コンマは非常に一般的です!
これらはすべてタプルを受け入れ、呼び出しはC#と同じように見えます。
// System.String.Compare("a","b") // System.String.Compare "a" "b"
その理由は、従来の.NETの機能はカリー化されておらず、部分的に適用できないためです。 すべてのパラメーターは常にすぐに送信する必要があり、最も明白な方法はタプルを使用することです。
これらの呼び出しはタプルを転送するように見えるだけですが、これは実際には特別な場合です。 そのような関数に実際のタプルを渡すことはできません。
let tuple = ("a","b") System.String.Compare tuple // error System.String.Compare "a","b" // error
.NET関数を部分的に適用する場合は、 以前に行ったように 、または以下に示すように、それらにラッパーを書くだけです。
// let strCompare xy = System.String.Compare(x,y) // let strCompareWithB = strCompare "B" // ["A";"B";"C"] |> List.map strCompareWithB
個別およびグループ化されたパラメーターの選択ガイド
タプルの説明は、より一般的なトピックにつながります。パラメータをいつ分離するか、グループ化するかです。
この点で、F#とC#の違いに注意する必要があります。 C#では、 すべてのパラメーターが常に渡されるため、この質問はそこでも発生しません! F#では、部分的に適用されるため、パラメーターの一部のみを表すことができるため、パラメーターを組み合わせる必要がある場合と、独立している場合を区別する必要があります。
独自の関数を設計する際のパラメーターの構成方法に関する一般的な推奨事項。
- 一般的なケースでは、タプルであれレコードであれ、1つの構造体を渡すのではなく、個別のパラメーターを使用することをお勧めします。 これにより、部分適用など、より柔軟な動作が可能になります。
- ただし、パラメータのグループを一度に渡す必要がある場合は、何らかのグループ化メカニズムを使用する必要があります。
つまり、関数を開発するとき、「このパラメーターを個別に提供できますか?」と自問してください。 答えがいいえの場合、パラメーターをグループ化する必要があります。
いくつかの例を見てみましょう。
// . // , let add xy = x + y // // , let locateOnMap (xCoord,yCoord) = // // // - type CustomerName = {First:string; Last:string} let setCustomerName aCustomerName = // let setCustomerName first last = // // // // , let setCustomerName myCredentials aName = //
最後に、パラメータの順序が部分的な適用に役立つことを確認してください(こちらのマニュアルを参照してください )。 たとえば、最後の関数でmyCredentials
前にaName
を配置したのはなぜですか?
パラメータなしの関数
パラメーターを受け入れない関数が必要になる場合があります。 たとえば、複数回呼び出すことができる「hello world」関数が必要です。 前のセクションで示したように、単純な定義は機能しません。
let sayHello = printfn "Hello World!" //
ただし、これは、ユニットパラメーターを関数に追加するか、ラムダを使用することで修正できます。
let sayHello() = printfn "Hello World!" // let sayHello = fun () -> printfn "Hello World!" //
その後、関数は常にunit
引数で呼び出される必要があります。
// sayHello()
.NETライブラリとやり取りするときに頻繁に起こること:
Console.ReadLine() System.Environment.GetCommandLineArgs() System.IO.Directory.GetCurrentDirectory()
覚えておいて、それらをunit
パラメータで呼び出します!
新しい演算子の定義
1つ以上の演算子文字を使用して関数を定義できます(文字のリストについてはドキュメントを参照してください)。
// let (.*%) xy = x + y + 1
関数を定義するには、文字を括弧で囲む必要があります。
*
始まる演算子には、括弧と*
間にスペースが必要です。 F# (*
はコメントの始まりとして機能します(C#の/*...*/
など):
let ( *+* ) xy = x + y + 1
定義後、角括弧で囲まれている場合、新しい関数は通常の方法で使用できます。
let result = (.*%) 2 3
関数が2つのパラメーターで使用される場合、かっこなしで中置演算子レコードを使用できます。
let result = 2 .*% 3
!
始まるプレフィックス演算子を定義することもできます または~
(いくつかの制限付き、 ドキュメントを参照)
let (~%%) (s:string) = s.ToCharArray() // let result = %% "hello"
F#では、ステートメントの定義はかなり一般的な操作であり、多くのライブラリは>=>
や<*>
ような名前のステートメントをエクスポートします。
ポイントフリースタイル
カオスのレベルを下げるための最新のパラメーターが欠けている関数の例は、すでに多く見ています。 このスタイルは、 ポイントフリースタイルまたは暗黙プログラミングと呼ばれます 。
以下に例を示します。
let add xy = x + y // let add x = (+) x // point free let add1Times2 x = (x + 1) * 2 // let add1Times2 = (+) 1 >> (*) 2 // point free let sum list = List.reduce (fun sum e -> sum+e) list // let sum = List.reduce (+) // point free
このスタイルには長所と短所があります。
利点の1つは、低レベルのオブジェクトに煩わされるのではなく、高次関数の構成に重点が置かれていることです。 たとえば、「 (+) 1 >> (*) 2
」は、明示的な加算とそれに続く乗算です。 また、「 List.reduce (+)
」は、リスト情報に関係なく、追加操作が重要であることを明確にします。
無意味なスタイルを使用すると、基本的なアルゴリズムに集中し、コード内の一般的な機能を特定できます。 上記で使用した「 reduce
」関数は良い例です。 このトピックについては、リスト処理に関する一連の計画で説明します。
一方、このスタイルを使いすぎると、コードが不明瞭になる可能性があります。 明示的なパラメータはドキュメントとして機能し、その名前(「リスト」など)は、関数が何をするのかを理解しやすくします。
プログラミングのすべてと同様に、最良の推奨事項は、最も明確なアプローチを選択することです。
コンビネーター
「 コンビネータ 」は関数と呼ばれ、その結果はパラメータのみに依存します。 これは、外の世界に依存していないことを意味し、特に、他の関数やグローバルな値が外の世界に影響を与えることはできません。
実際には、これは組み合わせ機能がさまざまな方法でパラメーターの組み合わせによって制限されることを意味します。
パイプと構成演算子といういくつかのコンビネーターを見てきました。 それらの定義を見ると、さまざまな方法でパラメーターを並べ替えているだけであることは明らかです。
let (|>) xf = fx // pipe let (<|) fx = fx // pipe let (>>) fgx = g (fx) // let (<<) gfx = g (fx) //
一方、「printf」のような関数は、プリミティブですが、外部世界(I / O)に依存しているため、コンビネーターではありません。
組み合わせ鳥
コンビネータは、コンピュータおよびプログラミング言語よりも何年も前に発明されたロジック(自然に「コンビナトリアルロジック」と呼ばれる)全体の基礎です。 組み合わせロジックは、関数型プログラミングに非常に大きな影響を及ぼします。
コンビネータと組み合わせロジックの詳細については、Raymond Smullyanの本「To Mock a Mockingbird」をお勧めします。 その中で、彼は他のコンビネーターを説明し、彼らに鳥の名前を空想的に与えます。 標準コンビネーターとその鳥の名前の例を次に示します。
let I x = x // , Idiot bird let K xy = x // the Kestrel let M x = x >> x // the Mockingbird let T xy = yx // the Thrush ( !) let Q xyz = y (xz) // the Queer bird ( !) let S xyz = xz (yz) // The Starling // ... let rec Y fx = f (Y f) x // Y-, Sage bird
リテラル名は非常に標準的なため、この用語に精通している人なら誰でもKコンビネーターを参照できます。
多くの一般的なプログラミングパターンは、これらの標準的なコンビネータを介して表現できることがわかります。 たとえば、Kestrelは、何かをするが元のオブジェクトを返す、流れるようなインターフェイスの一般的なパターンです。 ツグミはパイプであり、クィアは直接的な合成であり、Yコンビネーターは再帰関数を作成する優れた仕事をします。
実際、計算可能な関数は、KestrelとStarlingの2つの基本的な組み合わせのみを使用して構築できるという定理があります。
組み合わせライブラリ
組み合わせライブラリは、共有するように設計された多くの組み合わせ関数をエクスポートするライブラリです。 このようなライブラリのユーザーは、関数を簡単に組み合わせて、キューブなどのさらに大きく複雑な関数を簡単に取得できます。
適切に設計されたコンバイナライブラリにより、高レベルの機能に集中し、低レベルの「ノイズ」を隠すことができます。 「F#を使用する理由」シリーズのいくつかの例で既にその力を見てきました。また、 List
モジュールにはそのような機能がたくさんあり、「 fold
」と「 map
」も考えればコンビネーターです。
コンビネータのもう1つの利点は、最も安全なタイプの関数であることです。 なぜなら それらは外の世界に依存せず、グローバル環境が変化しても変化することはできません。 グローバル値を読み取る関数またはライブラリ関数を使用する関数は、コンテキストが変更されると、呼び出し間で中断または変更される場合があります。 これはコンビネーターには決して起こりません。
F#では、コンビネータライブラリを解析(FParsec)、HTMLの作成、フレームワークのテストなどに使用できます。 コンビネータについては、次のシリーズの後半で説明し、使用します。
再帰関数
多くの場合、関数はその本体から自身を参照する必要があります。 典型的な例は、フィボナッチ関数です。
let fib i = match i with | 1 -> 1 | 2 -> 1 | n -> fib(n-1) + fib(n-2)
残念ながら、この関数はコンパイルできません。
error FS0039: The value or constructor 'fib' is not defined
rec
キーワードを使用して、これが再帰関数であることをコンパイラーに伝える必要があります。
let rec fib i = match i with | 1 -> 1 | 2 -> 1 | n -> fib(n-1) + fib(n-2)
関数型プログラミングでは再帰的な関数とデータ構造が非常に一般的であり、このトピックについては後でシリーズ全体を取り上げたいと思います。
追加のリソース
F#には、C#またはJavaの経験がある人向けの資料など、多くのチュートリアルがあります。 次のリンクは、F#の詳細を説明するのに役立ちます。
F#の学習を開始する他のいくつかの方法についても説明します。
最後に、F#コミュニティは非常に初心者に優しいです。 Slackには、F#Software Foundationがサポートする非常に活発なチャットがあり、 自由に参加できる初心者ルームがあります。 これを行うことを強くお勧めします!
ロシア語を話すコミュニティF#のサイトを訪れることを忘れないでください! 言語の学習について質問がある場合は、チャットルームでお気軽にご相談ください。
- F#Software Foundation Slack Chatの部屋
#ru_general
- Telegramでチャット
- 雑談
- F#Software Foundation Slack Chatの部屋#en_general
翻訳著者について
@kleidemosによる翻訳
翻訳と編集上の変更は、F#開発者のロシア語コミュニティの努力によって行われました。 また、この記事を公開する準備をしてくれた@schvepsssと@shwarsにも感謝します。