読者です 読者をやめる 読者になる 読者になる

Java 8を関数型っぽく使うためのおまじないをF#でやってみた

Java 8を関数型っぽく使うためのおまじない - きしだのはてな
Java 8を関数型っぽく使うためのおまじないをC#でやってみた - ぐるぐる~

JavaC# も大変ですね。 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 "囲まれた!")

おまけ

JavaC# では、ラムダ式と通常のメソッドの間には大きな差がありました。 その一例として、ジェネリックな関数を定義できない、というものがありました。 しかし、F# ではラムダ式と通常の関数はシームレスにつながっています。 そのため、

(* 関数 *)
let id x = x

はもちろんできるし、

(* ラムダ式 *)
let id = fun x -> x

もできます。 表記上でも、普通の関数とラムダ式を用いた定義は JavaC# ほどの差はありません。