Java 8を関数型っぽく使うためのおまじないをF#でやってみた
Java 8を関数型っぽく使うためのおまじない - きしだのはてな
Java 8を関数型っぽく使うためのおまじないをC#でやってみた - ぐるぐる~
Java も C# も大変ですね。 F# さんは、ラムダ式も関数型も最初から使えたので、似たようなことはすでにできます。 上記の記事のパクリなので、上記の記事をまずは読んでから読むことをおすすめします。
関数型(関数を表す型の方)
F# では FSharpFunc という型があります。名前空間や型パラメータまで含めると、Microsoft.FSharp.Core.FSharpFunc<'T, 'U>
です。
ただ、この型を直接使うことはありませんし、見ることもそうそうないです。
その代わりに、'T -> 'U
という表記が使えます。「'T
を受け取って 'U
を返す関数」と読みます。
ちなみに、型パラメータの最初に「'」が付いているのが割と大事なことなのですが、
それはまた機会があればということで今回は「型パラメータの一文字目は「'」が必要」くらいに思っておいてください。
こんな感じで使います。
(* F#では//形式の一行コメントも使えるけど、 はてなブログがF#のシンタックスハイライトに対応してないので、 この記事ではブロックコメントを使います(OCamlのシンタックスハイライトを使ってます)。 *) (* Java: Function<String, String> enclose = s -> "[" + s + "]"; *) (* C#: Func<string, string> enclose = s => "[" + s + "]"; *) let enclose = fun s -> "[" + s + "]"
型を全然書いてませんね。変数に型を明記すると、こうなります。
let enclose: string -> string = fun s -> "[" + s + "]"
これを呼び出そうとすると、こんな感じになります。
(* 引数を()で囲む必要はない *) System.Console.WriteLine(enclose "foo") (* System.Console.WriteLineの引数をカッコで囲っているように思えるけど、 どちらかというとenclose "foo"全体をカッコで囲っている感じ *)
こうすると、次のような表示になります。
[foo]
もうひとつ関数を定義してみましょう。最初の文字を大文字にする関数です。
(* sの型を明示しないと、メソッド呼出しが出来ないので明示している *) let capitalize = fun (s: string) -> s.Substring(0, 1).ToUpper() + s.Substring(1).ToLower()
2文字未満の文字列を与えると死にます。
呼び出してみます。
System.Console.WriteLine(capitalize "foo") (* => Foo *)
この2つを順に呼び出して、capitalize して enclose しようとすると、こんな感じになりますね。
System.Console.WriteLine(enclose (capitalize "foo")) (* => [Foo] *)
こういう場合、Java では andThen を使って連結し、C# では拡張メソッドを定義しましたが、
F# では >>
という関数が用意されています。
使ってみましょう。
System.Console.WriteLine((capitalize >> enclose) "foo");
これは、関数合成ですね。
let capEnc = capitalize >> enclose System.Console.WriteLine(capEnc "foo")
F# は関数型っぽいことが簡単にできます。べんり!
関数
ところで、F# では let f = fun x -> ...
は let f x = ...
と書くことが出来ます。
これを使うと、今までのコードはこう書けます。
let enclose s = "[" + s + "]" let capitalize (s: string) = s.Substring(0, 1).ToUpper() + s.Substring(1).ToLower() let capEnc = capitalize >> enclose System.Console.WriteLine(capEnc "foo")
そして、この let f x = ...
は関数定義です。
関数型は 'T -> 'U
でしたが、上で定義した enclose 関数のシグネチャも、string -> string
のように表します。
ラムダ式と関数がシームレスにつながっていて、とても良い感じです。
別名を使う?
'T -> 'U
がすでに Microsoft.FSharp.Core.FSharpFunc<'T, 'U>
の別名のようなものなので、必要ないですね。
さらに、型推論が効くので型を書く必要はほとんどの場合でありません。
unit
Java でも C# でも、戻り値を返さない関数は Function/Func とは別のものが必要でした。
これは、void というのが特殊なものなのが原因です。
しかし、F# で void に相当する unit は、他の型同様値を持つ(「()」という唯一の値を持つ)ので、特別扱いは不要です。
何の問題もなく、string -> unit
が使えます。
(* let writeLine (str: string) = System.Console.WriteLine(str) とかでも可 *) let writeLine: string -> unit = System.Console.WriteLine let enclose s = "[" + s + "]" writeLine (enclose "foo") let capitalize (s: string) = s.Substring(0, 1).ToUpper() + s.Substring(1).ToLower() writeLine (capitalize "foo")
ここまでで、1引数の関数型しか使っていませんが、2引数以上の関数は甘えなので、1引数の関数が定義できれば問題ありません。
あと F# の関数である FSharpFunc<'T, 'U>
には BiFunction
やら Func<TArg1, TArg2, TResult>
なんてものはありません。何の問題もありません。
関数合成
関数合成は次のように書けます。
writeLine ((capitalize >> enclose) "foo")
writeLine も特別扱いしなくていいので、さらに合成できますね。
(capitalize >> enclose >> writeLine) "foo"
さらにもうひとつ関数を用意して、次のように書いてみます。真ん中あたりを取り出す関数です。
let middle = (fun s: string) -> s.Substring(s.Length / 3, s.Length - s.Length * 2 / 3) (middle >> capitalize >> enclose >> writeLine) "yoofoobar"
関数合成を使わないと、次のようになりますね。
writeLine (enclose (capitalize (middle "foobaryah")))
このように、実際に呼び出す順と記述が逆になります。middle して capitalize して enclose して writeLine するのに、先に writeLine から書く必要があります。 また、カッコがたまっていくので、最後にまとめてカッコを閉じる必要があります。ちょっと関数呼び出しが増えると、閉じカッコの数が分からなくなってコンパイルエラーがなくなるところまで「)」を増やしたり減らしたりなんてこと、ありますよね?
F# ではさらに、パイプライン演算子というものがあるので、こう書くこともできます。
"yoofoobar" |> middle |> capitalize |> enclose |> writeLine
「"yoofoobar"をmiddleしてcapitalizeしてencloseしてwriteLineする」ことがコードで自然に表せていますね。 これを改行すると・・・
"yoofoobar" |> middle |> capitalize |> enclose |> writeLine
おぉ。
カリー化形式
さて、2引数以上の関数は甘えと書きましたが、実際2つ以上のパラメータを渡したいときはどうすればいいんでしょう? こういうときに使うのがカリー化です。カリー化は、ひとつの引数を取って関数を返すことで、複数のパラメータに対応します。
例えば、はさむ文字列とはさまれる文字列を指定して、文字列をはさむ関数は、C# の2引数関数であらわすならこうなるでしょう。
string Sandwich(string tag, string str) { return tag + str + tag; }
これをカリー化形式の関数で書くと、次のようになります。
(* 一部型指定をしてますが、気にしたら負けです *) let sandwich (tag: string) = fun str -> tag + str + tag
sandwich 自体は、『文字列を取って、「文字列を取って文字列を返す関数」を返す関数』になっています。
呼び出しは次のようになります。
writeLine (sandwich "***" "sanded!") (* => ***sanded!*** *)
3引数だとこんな感じですね。
let encloseC (openStr: string) = fun closeStr -> fun str -> openStr + str + closeStr
encloseC は、【文字列を取って、『文字列を取って、「文字列を取って文字列を返す関数」を返す関数』を返す関数】になっています。
呼び出しはこんな感じで。
writeLine (encloseC "{" "}" "enclosed!") (* => {enclosed!} *)
ところで、このカリー化形式の encloseC、引数を部分的に渡しておくことが出来ます。
let encloseCurly = encloseC "{" "}" writeLine (encloseCurly "囲まれた!")
こうやって部分適用することで、新しい関数が作れるわけです。 ちなみに Curly は、波カッコ=カーリーブラケット「{~}」のことで、カリー化とは関係ないのであしからず。
ところでところで、F# ではネストした fun をまとめることができます。 例えば、encloseC はこんな定義でした。
let encloseC (openStr: string) = fun closeStr -> fun str -> openStr + str + closeStr
2つも fun がありますね。まとめてしまいましょう。
let encloseC (openStr: string) = fun closeStr str -> openStr + str + closeStr
スッキリしましたね!
ところでところでところで、F# では let f = fun x -> ...
は let f x = fun x -> ...
と書けるのでした。
fun をまとめる前の定義に戻ると、
let encloseC (openStr: string) = fun closeStr -> fun str -> openStr + str + closeStr
でしたので、「f」の部分が「encloseC (openStr: string)」だと思ってみると・・・
let encloseC (openStr: string) closeStr = fun str -> openStr + str + closeStr
となって、さらに
let encloseC (openStr: string) closeStr str = openStr + str + closeStr
こうですね!
まとめ
Action 以降のソース、こんな感じです。
let writeLine (str: string) = System.Console.WriteLine(str) let enclose s = "[" + s + "]" writeLine (enclose "foo") let capitalize (s: string) = s.Substring(0, 1).ToUpper() + s.Substring(1).ToLower() writeLine (capitalize "foo") (* 関数合成 *) (capitalize >> enclose >> writeLine) "foo" let middle (s: string) = s.Substring(s.Length / 3, s.Length - s.Length * 2 / 3) (middle >> capitalize >> enclose >> writeLine) "yoofoobar" writeLine (enclose (capitalize (middle "foobaryah"))) (* カリー化形式 *) let sandwich (tag: string) str = tag + str + tag writeLine (sandwich "***" "sanded!") let encloseC (openStr: string) closeStr str = openStr + str + closeStr writeLine (encloseC "{" "}" "enclosed!") (* 部分適用 *) let encloseCurly = encloseC "{" "}" writeLine (encloseCurly "囲まれた!")
おまけ
Java や C# では、ラムダ式と通常のメソッドの間には大きな差がありました。 その一例として、ジェネリックな関数を定義できない、というものがありました。 しかし、F# ではラムダ式と通常の関数はシームレスにつながっています。 そのため、
(* 関数 *) let id x = x
はもちろんできるし、
(* ラムダ式 *) let id = fun x -> x