この記事では、F#で使える様々な種類の式と、それらを使う際の一般的なヒントを紹介します。
本当にすべてが式なのか?
「すべてが式である」というのが実際にどう機能するのか、疑問に思うかもしれません。
まずは、おなじみのはずの基本的な式の例から見てみましょう。
1 // リテラル
[1;2;3] // リスト式
-2 // 前置演算子
2 + 2 // 中置演算子
"string".Length // ドットによるアクセス
printf "hello" // 関数適用
ここまでは問題ないですね。これらが式であることは明らかです。
しかし、以下のようなより複雑なものも式です。つまり、これらはそれぞれ値を返すので、他の目的に使えます。
fun () -> 1 // ラムダ式
match 1 with // マッチ式
| 1 -> "a"
| _ -> "b"
if true then "a" else "b" // if-then-else式
for i in [1..10] // forループ
do printf "%i" i
try // 例外処理
let result = 1 / 0
printfn "%i" result
with
| e ->
printfn "%s" e.Message
let n=1 in n+2 // let式
他の言語ではこれらが文として扱われることもありますが、F#では実際に値を返します。以下のように結果に値を束縛すれば、確認できます。
let x1 = fun () -> 1
let x2 = match 1 with
| 1 -> "a"
| _ -> "b"
let x3 = if true then "a" else "b"
let x4 = for i in [1..10]
do printf "%i" i
let x5 = try
let result = 1 / 0
printfn "%i" result
with
| e ->
printfn "%s" e.Message
let x6 = let n=1 in n+2
どのような種類の式があるのか?
F#には現在、約50種類のさまざまな式があります。そのほとんどは、リテラル、演算子、関数適用、「ドットによるアクセス」などのような、単純で自明なものです。
より興味深く高レベルなものは、以下のようにグループ分けできます。
- ラムダ式
- 「制御フロー」式。以下を含みます。
match..with
構文を使用したマッチ式- if-then-elseやループなど、命令型の制御フローに関連する式
- 例外関連の式
- "let" 式と "use" 式
async {..}
のようなコンピュテーション式- キャストやインターフェイスなど、オブジェクト指向コードに関連する式
ラムダについては「関数型思考」シリーズですでに説明しました。また、先に述べたように、コンピュテーション式とオブジェクト指向式は後のシリーズで扱います。
そのため、このシリーズの今後の記事では、「制御フロー」式と「let」式に焦点を当てます。
「制御フロー」式
命令型言語では、if-then-else、for-in-do、match-withなどの制御フロー式は通常、副作用を伴う文として実装されます。F#では、これらはすべて別の種類の式として実装されます。
実際、関数型言語において「制御フロー」を考えるのは役に立ちません。この概念は実際には存在しないのです。プログラムを、サブ式を含む巨大な式として考える方が良いでしょう。サブ式の一部は評価され、一部は評価されません。この考え方を理解できれば、関数型思考への良いスタートを切れたと言えるでしょう。
これらのさまざまな種類の制御フロー式については、今後いくつかの記事で取り上げます。
式としての「let」束縛
let x=something
はどうでしょうか?上記の例では以下のようなものがありました。
let x5 = let n=1 in n+2
「let」がどのように式になりうるのでしょうか?その理由は次の記事「let、use、doでの束縛」で説明します。
式を使う際の一般的なヒント
重要な式の種類を詳しく説明する前に、一般的に式を使う際のヒントをいくつか紹介します。
1行に複数の式
通常、各式は新しい行に置きます。しかし、必要な場合はセミコロンを使って1行に複数の式を区切ることができます。リストやレコードの要素を区切るときと並んで、F#でセミコロンを使う数少ない場面の一つです。
let f x = // 1行に1つの式
printfn "x=%i" x
x + 1
let f x = printfn "x=%i" x; x + 1 // セミコロンを使って1行にまとめる
もちろん、最後の式まではunit値が必要というルールは、引き続き適用されます。
let x = 1;2 // エラー: "1;"はunit式であるべき
let x = ignore 1;2 // OK
let x = printf "hello";2 // OK
式の評価順序を理解する
F#では、式は「内側から外側へ」評価されます。つまり、完全なサブ式が「見つかった」らすぐに評価されます。
以下のコードを見て、何が起こるか予想してから、実際にコードを評価してみてください。
// if-then-elseのクローンを作る
let test b t f = if b then t else f
// 2つの異なる選択肢で呼び出す
test true (printfn "true") (printfn "false")
実際には、test関数が「else」ブランチを評価することはないにもかかわらず、 "true" と "false" の両方が出力されます。なぜなら、 (printfn "false")
式は、test関数がどのように使用するかとは関係なく、即座に評価されるからです。
この評価スタイルは「正格(eager)」と呼ばれます。理解しやすいという利点がありますが、場合によっては非効率になることもあります。
もう一つの評価スタイルは「遅延(lazy)」と呼ばれ、式は必要になった時点で評価されます。Haskell言語はこのアプローチを採用しているため、Haskellで同様の例を書くと "true" だけが出力されます。
F#では、式を即座に評価しないようにするためのテクニックがいくつかあります。最も簡単なのは、必要に応じて評価される関数でラップすることです。
// 単純な値ではなく関数を受け取るif-then-elseのクローンを作る
let test b t f = if b then t() else f()
// 2つの異なる関数で呼び出す
test true (fun () -> printfn "true") (fun () -> printfn "false")
この方法の問題点は、"true" 関数が誤って2回評価される可能性があることです。1回だけ評価したいのに!
そこで、式を即座に評価しないようにするには、 Lazy<>
ラッパーを使うのが望ましい方法です。
// 制限のないif-then-elseのクローンを作る...
let test b t f = if b then t else f
// ...そして、遅延値で呼び出す
let f = test true (lazy (printfn "true")) (lazy (printfn "false"))
最終的な結果値 f
も遅延値であり、結果を取得する準備ができるまで評価されることなく渡すことができます。
f.Force() // Force()を使って遅延値の評価を強制する
結果が必要ない場合、 Force()
を呼ばなければ、ラップされた値は決して評価されません。
遅延については、パフォーマンスに関する今後のシリーズでさらに詳しく取り上げます。