これまで、以下のような let 構文を使って一般的な関数を作る方法を見てきました。

let add x y = x + y

このセクションでは、関数を作るその他の方法と、関数を定義するためのヒントを見ていきます。

無名関数(別名:ラムダ式)

他の言語でラムダ式を使ったことがある人には、これは目新しくないでしょう。無名関数(または「ラムダ式」)は以下の形で定義します。

fun parameter1 parameter2 etc -> expression

C#のラムダ式に慣れている人は、いくつかの違いに気づくでしょう。

  • ラムダ式には特別なキーワード fun が必要です。これはC#版では必要ありません。
  • 矢印記号は単一の矢印 -> で、C#の二重矢印( => )とは違います。

以下は加算を定義するラムダ式です。

let add = fun x y -> x + y

これは、より一般的な関数定義と全く同じです。

let add x y = 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 x y = 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" f l

// パラメータ自体でマッチング
let f2 {first=f; last=l} =          // 直接パターンマッチング
   printfn "first=%s; last=%s" f l 

// テスト
f1 bob
f2 bob

このようなマッチングは、マッチングが常に可能な場合にのみ使えます。たとえば、和集合型やリストに対してはこの方法でマッチングできません。いくつかのケースがマッチしない可能性があるからです。

let f3 (x::xs) =            // リストに対するパターンマッチングを使う
   printfn "最初の要素は=%A" x

不完全なパターンマッチに関する警告が表示されます。

よくある間違い:タプルと複数のパラメータの混同

C言語系の言語を使ってきた人には、単一の関数パラメータとして使うタプルが、複数のパラメータとよく似て見えるかもしれません。しかし、これらは全く別物です! 先ほども言ったように、コンマが見えたら、それはおそらくタプルの一部です。パラメータはスペースで区切ります。

以下は、この混同の例です。

// 2つの別々のパラメータを取る関数
let addTwoParams x y = x + y

// 単一のタプルパラメータを取る関数
let addTuple aTuple = 
   let (x,y) = aTuple
   x + y

// 単一のタプルパラメータを取る別の関数
// しかし、2つのintを取るように見える
let addConfusingTuple (x,y) = x + y
  • 最初の定義 addTwoParams は、スペースで区切られた2つのパラメータを取ります。
  • 2つ目の定義 addTuple は、単一のパラメータを取ります。その後、タプルの中身を「x」と「y」に束縛し、加算をします。
  • 3つ目の定義 addConfusingTuple は、 addTuple と同じく単一のパラメータを取りますが、トリッキーなのは、パラメータ定義の一部としてパターンマッチングを使ってタプルが展開され、束縛されることです。裏側では、これは addTuple とまったく同じです。

シグネチャを見てみましょう(不安なときは、常にシグネチャを確認することをお勧めします)

val addTwoParams : int -> int -> int        // 2つのパラメータ
val addTuple : int * int -> int             // タプル->int
val addConfusingTuple : int * int -> int    // タプル->int

では、これらを使ってみましょう。

//テスト
addTwoParams 1 2      // OK - 引数の区切りにスペースを使う
addTwoParams (1,2)    // エラー - 単一のタプルを渡そうとしている
//   => error FS0001: この式に必要な型は 'int' ですが、
//                     ここでは次の型が指定されています ''a * 'b'

ここで、上記の2つ目のケースでエラーが起きていることがわかります。

まず、コンパイラは (1,2) を型 ('a * 'b) の一般的なタプルとして扱い、これを addTwoParams の最初のパラメータとして渡そうとします。 そして、 addTwoParams の最初のパラメータが int なのに、タプルを渡そうとしていると訴えます。

タプルを作るには、カンマを使います! 以下が正しい方法です。

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    // エラー - 2つの引数を渡そうとしている 
// => error FS0003: この値は関数ではないため、適用できません

この場合、コンパイラは2つの引数を渡していることから、 addConfusingTuple がカリー化できると考えます。そのため、 addConfusingTuple 1 は中間的な関数を返す部分適用となるはずです。その中間的な関数に 2 を適用しようとするとエラーが起きます。なぜなら、中間的な関数は存在しないからです! カリー化に関する投稿で、パラメータが多すぎる場合に起きる問題について議論したときに、まさにこの同じエラーを見ました。

なぜタプルをパラメータとして使わないのか?

タプルに関する上記の議論から、1つ以上のパラメータを持つ関数を作る別の方法があることがわかります。パラメータを個別に渡すのではなく、すべてのパラメータを単一の複合データ構造にまとめられます。以下の例では、関数は3つの項目を含むタプルである単一のパラメータを取ります。

let f (x,y,z) = x + y * z
// 型は int * int * int -> int

// テスト
f (1,2,3)

関数シグネチャが本当の3パラメータ関数とは違うことに注意してください。矢印は1つしかないので、パラメータは1つだけで、アスタリスクは (int*int*int) のタプルであることを示しています。

では、個別のパラメータの代わりにタプルパラメータを使いたい場合はいつでしょうか?

  • タプル自体に意味がある場合。たとえば、3次元座標を扱う場合、3つの次元を別々に扱うより、3タプルの方が便利かもしれません。
  • タプルは、一緒に保持すべきデータをまとめて単一の構造にするために時々使います。たとえば、.NETライブラリの TryParse 関数は、結果とブール値をタプルとして返します。しかし、まとめて保持すべきデータが多い場合は、おそらくレコードまたはクラス型を作ってそれを格納したいでしょう。

特殊なケース:タプルと.NETライブラリ関数

カンマがよく見られる領域の1つは、.NETライブラリ関数を呼び出すときです!

これらはすべてタプルのような引数を取るため、これらの呼び出しはC#の場合とまったく同じように見えます。

// 正しい
System.String.Compare("a","b")

// 間違い
System.String.Compare "a" "b"

理由は、.NETライブラリ関数がカリー化されておらず、部分適用できないからです。すべてのパラメータを常に渡す必要があり、タプルに似たアプローチを使うのは明らかな方法です。

ただし、これらの呼び出しはタプルのように見えますが、実際には特殊なケースだということに注意してください。本物のタプルは使えません。そのため、以下のコードは無効です。

let tuple = ("a","b")
System.String.Compare tuple   // エラー

System.String.Compare "a","b" // エラー

.NETライブラリ関数を部分適用したい場合は、通常、以前見たように、以下のようにラッパー関数を書くのは簡単です。

// ラッパー関数を作る
let strCompare x y = System.String.Compare(x,y)

// 部分適用する
let strCompareWithB = strCompare "B"

// 高階関数で使う
["A";"B";"C"]
|> List.map strCompareWithB

個別パラメータとグループ化されたパラメータのガイドライン

タプルに関する議論から、より一般的な話題に移ります。関数パラメータはいつ個別にすべきで、いつグループ化すべきでしょうか?

F#はこの点でC#とは違うことに注意してください。C#ではすべてのパラメータが常に提供されるため、この問題は起きません! F#では、部分適用のため、一部のパラメータのみが提供される可能性があるので、一緒にグループ化する必要があるパラメータと独立したパラメータを区別する必要があります。

以下は、自分で関数を設計するときにパラメータをどう構成するかの一般的なガイドラインです。

  • 基本的に、パラメータをタプルやレコードなどの単一の構造として渡すより、常に個別のパラメータを使う方が良いです。これにより、部分適用などのより柔軟な動作が可能になります。
  • ただし、パラメータのグループが必ず一緒に設定される必要がある場合は、何らかのグループ化の仕組みを使ってください。

言い換えれば、関数を設計するとき、「このパラメータを単独で提供できるだろうか?」と自問してください。答えがノーなら、パラメータはグループ化すべきです。

いくつか例を見てみましょう。

// 加算のために2つの数値を渡す。
// 数値は独立しているので、2つのパラメータを使う
let add x y = x + y

// 地理座標として2つの数値を渡す。
// 数値は依存しているので、タプルまたはレコードにグループ化する
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!"     // 望んでいるものではない

解決策は、関数に unit パラメータを追加するか、ラムダ式を使うことです。

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つ以上使って名前を付けた関数を定義できます(使える記号の正確なリストについてはF#のドキュメントを参照してください)。

// 定義
let (.*%) x y = x + y + 1

定義するときは記号の周りにかっこを使う必要があります。

* で始まるカスタム演算子の場合、スペースが必要です。そうしないと、 (* がコメントの開始と解釈されてしまいます。

let ( *+* ) x y = x + y + 1

定義後、新しい関数は通常の方法で使えます。再び記号の周りにかっこを付けます。

let result = (.*%) 2 3

関数がちょうど2つのパラメータを持つ場合、かっこなしで中置演算子として使えます。

let result = 2 .*% 3

! または ~ で始まる前置演算子も定義できます(いくつか制限があります - 演算子のオーバーロードに関するF#のドキュメントを参照してください)

let (~%%) (s:string) = s.ToCharArray()

//使用
let result = %% "hello"

F#では、自分で演算子を作るのはとても一般的です。多くのライブラリが >=><*> などの名前の演算子を提供しています。

ポイントフリースタイル

関数の最後のパラメータを省略して簡潔にする例をすでに多く見てきました。このスタイルはポイントフリースタイルまたは暗黙的プログラミングと呼ばれます。

いくつか例を見てみましょう。

let add x y = x + y   // 明示的
let add x = (+) x     // ポイントフリー

let add1Times2 x = (x + 1) * 2    // 明示的
let add1Times2 = (+) 1 >> (*) 2   // ポイントフリー

let sum list = List.reduce (fun sum e -> sum+e) list // 明示的
let sum = List.reduce (+)                            // ポイントフリー

このスタイルには長所と短所があります。

長所としては、低レベルのオブジェクトではなく、高レベルの関数合成に注目できることです。たとえば、 (+) 1 >> (*) 2 は明らかに加算操作に続く乗算操作です。また、 List.reduce (+) は、実際に適用されるリストを知る必要なく、プラス操作が重要だということを明確にします。

ポイントフリースタイルは、基本的なアルゴリズムを明確にし、コード間の共通点を明らかにするのに役立ちます。上記で使った reduce 関数はその良い例です。これについては、リスト処理に関する予定のシリーズで話し合う予定です。

一方で、ポイントフリースタイルを使いすぎると、混乱を招くコードになる可能性があります。明示的なパラメータは一種のドキュメントとして機能し、その名前(「list」など)が関数が何に対して働いているかを明確にします。

プログラミングにおける他の多くのことと同様に、最も分かりやすさを提供するアプローチを使うのが最良のガイドラインです。

コンビネータ

コンビネータという言葉は、結果が引数だけに依存する関数を表すのに使います。つまり、外部世界への依存性がなく、特に他の関数やグローバルな値にまったくアクセスできません。

実際には、コンビネータ関数は引数を様々な方法で組み合わせることに限定されています。

すでにいくつかのコンビネータを見てきました。「パイプ」演算子と「合成」演算子です。これらの定義を見ると、それらがパラメータを様々な方法で並べ替えるだけだということがはっきりわかります。

let (|>) x f = f x             // 前方パイプ
let (<|) f x = f x             // 後方パイプ
let (>>) f g x = g (f x)       // 前方合成
let (<<) g f x = g (f x)       // 後方合成

一方、「printf」のような関数は原始的ですが、外の世界(I/O)への依存性があるため、コンビネータではありません。

コンビネータ鳥

コンビネータは、コンピュータやプログラミング言語が発明される遥か以前に作られた論理学の一分野(当然「コンビネータ論理」と呼ばれます)の基礎です。コンビネータ論理は関数型プログラミングに非常に大きな影響を与えてきました。

コンピネータとコンビネータ論理についてもっと学びたい方は、レイモンド・スマリヤン著の「ものまね鳥をまねる」という本がおすすめです。この本では、スマリヤンは様々なコンビネータを紹介し、ユーモアを交えて鳥の名前を付けています。以下に、代表的なコンビネータとその鳥の名前をいくつか紹介します。

let I x = x                // 恒等関数、または「アホウドリ」
let K x y = x              // 「ケストレル」
let M x = x >> x           // 「モッキングバード」
let T x y = y x            // 「ツグミ」(見覚えがありますね!)
let Q x y z = y (x z)      // 「クィアバード」(これも見覚えがあります!)
let S x y z = x z (y z)    // 「ホシムクドリ」
// そして悪名高い...
let rec Y f x = f (Y f) x  // Yコンビネータ、または「セージバード」

文字名はかなり標準的なので、「Kコンビネータ」と言えば、誰もがその用語を理解するでしょう。

興味深いことに、これらの標準的なコンビネータを用いることで、多くの一般的なプログラミングパターンを表現できます。たとえば、「ケストレル」は、操作を実行後に元のオブジェクトを返す、流暢なインターフェースにおける典型的なパターンです。「ツグミ」はパイプ操作を表し、「クィアバード」は前方合成、そしてYコンビネータは関数を再帰的にするために用いられることで有名です。

実は、よく知られている定理によると、「ケストレル」と「ホシムクドリ」という2つの基本的なコンビネータだけで、どんな計算可能な関数でも作れるとされています。

コンビネータライブラリ

コンビネータライブラリは、一緒に動くように設計された一連のコンビネータ関数を提供するコードライブラリです。ライブラリのユーザーは、単純な関数を組み合わせて、より大きく複雑な関数を簡単に作れます。まるでレゴブロックで組み立てるようです。

うまく設計されたコンビネータライブラリを使うと、高レベルの操作に集中でき、低レベルの「ノイズ」を背景に押しやれます。このような力の例をいくつか、「F# を使う理由」シリーズの例で既に見てきました。また、 List モジュールにはそのような例がたくさんあります。よく考えてみると、 foldmap 関数もコンビネータです。

コンビネータのもう一つの利点は、最も安全な種類の関数だということです。外部世界に依存しないため、グローバルな環境が変わっても変更されることはありません。グローバル値を読み取ったりライブラリ関数を使ったりする関数は、状況が違うと呼び出し間で壊れたり変化したりする可能性があります。コンビネータではこのようなことは決して起こりません。

F#では、パース(FParsecライブラリ)、HTML構築、テストフレームワークなどのためのコンビネータライブラリが使えます。後のシリーズでコンビネータについてさらに議論し、使っていく予定です。

再帰関数

しばしば、関数は本体で自分自身を参照する必要があります。古典的な例はフィボナッチ関数です。

let fib i = 
   match i with
   | 1 -> 1
   | 2 -> 1
   | n -> fib(n-1) + fib(n-2)

残念ながら、これはコンパイルできません。

error FS0039: 値またはコンストラクター 'fib' が定義されていません。

コンパイラに、これが再帰関数だと伝えるために、 rec キーワードを使う必要があります。

let rec fib i = 
   match i with
   | 1 -> 1
   | 2 -> 1
   | n -> fib(n-1) + fib(n-2)

再帰関数とデータ構造は関数型プログラミングではとてもよく使われます。後のシリーズでこのトピックに1つ全体を当てたいと思います。

results matching ""

    No results matching ""