C# から使いやすい F# コードの書き方
さて始まりました、F# Advent Calendar 2012 です。
今年は、「実用」がテーマと言うことで、F# で書いたコードを C# から使いたくなった時に気を付けるべきポイントなどをまとめました。
F# と C# で異なる名前を付ける
F# では、module に定義する関数や変数の名前は、lowerCamel で付けるのが一般的です (List.map など)。
しかし .NET の世界では、これらの名前は基本的に PascalCase で付けることになっています。
CompiledName 属性を使うことで、この差を埋め、F# からは lowerCamel に、C# からは PascalCase に見える名前を付けることができるようになります。
(* F# *) module Util = [<CompiledName "ToStr">] let toStr x = sprintf "%A" x
これで、F# からは Util.toStr という名前で、C# からは Util.ToStr という名前でアクセスできる関数が定義できます。
CompiledName 属性は、関数や変数だけでなく、型に付けることも可能です。
(* F# *) [<CompiledName "MyUtil">] module Util = [<CompiledName "ToStr">] let toStr x = sprintf "%A" x
こうすることで、Util ではなく、MyUtil というモジュールとして公開されることになります。
C# とは関係ないですが、型引数を持たない型名とモジュール名がかぶってしまうとコンパイルが出来ません。
(* F# *) type User = { Name: string; Age: int } (* Userが重複しているのでコンパイルエラー *) module User = let name user = user.Name
この場合、CompilationRepresentation 属性を使うことで回避できます。
(* F# *) type User = { Name: string; Age: int } [<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>] module User = let name user = user.Name
こうすると、C# からは User モジュールは UserModule という名前で見えるようになります。
C# からはオーバーロードされて見えるメソッド
CompiledName 属性について、少し前にこんなやり取りがありました。
F# と C# を併用して組んでると、関数名を camelCase にするか CamelCase にするかすごく迷う・・・。どうしたらいいのん? .
@u_1roh CompiledName属性を使うといいですよ
2012-10-25 15:53:14 via Tween to @u_1roh
@bleis おお!おおーっ!もしかして関数のオーバーロードもこれで勝つる!?
2012-10-25 16:06:30 via Tween to @bleis
@u_1roh 関数のオーバーロードは、クラスを使うとできます
2012-10-25 16:07:31 via Tween to @u_1roh
@bleis あ、それは分かってます。言葉足らずですいません。F# 側では違う名前でも(オーバーロードしていなくても)、CompiledName 属性で同じ名前を与えれば C# 側からはオーバーロードに見えるのかな?と思いまして・・・。
2012-10-25 16:09:34 via Tween to @bleis
@u_1roh あ、なるほど。F#上は関数fと関数gだけど、C#上からはどちらもFとして見せたい、ということですね。その発想はなかった!
2012-10-25 16:22:56 via Tween to @u_1roh
@bleis そーです、そーです!
2012-10-25 16:23:48 via Tween to @bleis
つまりこういうことができるわけです。
(* F# *) module StreamUtil = [<CompiledName "Write">] let writeByte (b: byte, stream: Stream) = ... [<CompiledName "Write">] let writeInt (i: int, stream: Stream) = ...
これで、C# 側からは Stream という名前でオーバーロードされて見え、F# 側からは別々の関数として見えるようになります。
ちなみに、関数名以外全く同一のシグネチャを持つ 2 つの関数に、CompiledName で同じ名前を指定すると、dll の書き込みに失敗するという、あまり見ないコンパイルエラーになります。
戻り値のみが異なる場合は後勝ちになるようです。
C# に暗黙の型変換を提供する
F# には暗黙の型変換は存在しませんが、C# 用に提供したい場合があります。
これは、op_Implicit を実装することで実現できます。
(* F# *) type Str = { Value: string } with static member op_Implicit(str) = str.Value
こうすることで、C# 上で以下のような記述が可能になります。
// C# var s = new Str("hoge"); string str = s;
しかし、この方法は型引数を持つ型に対してはなぜか使えません。
(* F# *) type Wrapper<'a> = { Value: 'a } with static member op_Implicit(wrapper) = wrapper.Value
// C# var w = new Wrapper<string>("hoge"); // 型 'Wrapper<string>' を型 'string' に暗黙的に変換できません。 string str = s;
これが出来たら、素敵なことができるんですが・・・
どうにかできる人がいたら連絡を!
拡張メソッド
F# には型拡張*1という仕組みがあり、これを用いることで型に関数などを後付けできるのですが、この方法で実装した拡張は C# 側からアクセスすることができません*2。
C# 側からもアクセスできる拡張メソッドを定義したい場合は、Extension 属性を使用します。
(* F# *) open System.Runtime.CompilerServices [<Extension>] module HogeExtensions = [<Extension>] [<CompiledName "Hoge">] let hoge(x) = ...
こっちでもいいです。
(* F# *) open System.Runtime.CompilerServices [<Extension>] type HogeExtensions = [<Extension>] static member Hoge(x) = ...
使い分けとしては、F# 側からはモジュールとして使いたいけど、C# 側からは拡張メソッドとして使いたい、という場合に前者を、どちらからも拡張メソッドとして使いたい(もしくは、F# 側からは使わない)、という場合は後者を使うといいでしょう。
前者の場合、モジュール名はもうちょっと考えたいところですが。
判別共用体を C# 側から使いたい
インターフェイスやクラスなど、C# 側にも対応する機能が存在するものは F# で定義しても C# 側からは簡単に使えます。
また、モジュールとレコードも、C# 側から自然に扱えます。
レコードにメソッドを定義できるのも、結構便利ですよね。
(* F# *) type User = { Name: string; Age: int } with override this.ToString() = sprintf "%A" this
しかし、判別共用体だけは簡単に C# から使うことができません*3。
例えば、成功と失敗を表す以下の判別共用体を定義したとします。
(* F# *) type Result<'TSuccess, 'TFailure> = Success of 'TSuccess | Failure of 'TFailure
これを C# 側から使おうとする場合・・・
// C# // 生成 var res = Result<int, string>.NewSuccess(42); // 分岐 switch (res.Tag) { case Result<int, string>.Tags.Success: int s = ((Result<int, string>)res).Item; // Successの時の処理 break; case Result<int, string>.Tags.Failure: string f = ((Result<int, string>)res).Item; // Failureの時の処理 break; }
これはひどい。
これでは使えたものではないので、これを操作するユーティリティを提供するといいでしょう。
F# からは関数をパイプライン演算子で繋いで使いたいので、モジュールを使うことにします。
しかし、C# 側からはそれでは面倒なので、拡張メソッドを使うことにします。
その前に、何にでも使える便利なメソッドを定義しておきましょう。
(* F# *) open System type Result<'TSuccess, 'TFailure> = Success of 'TSuccess | Failure of 'TFailure with member this.Match(ifSuccess: Func<_, _>, ifFailure: Func<_, _>) = match this with | Success x -> ifSuccess.Invoke(x) | Failure x -> ifFailure.Invoke(x) member this.Action(ifSuccess: Action<_>, ifFailure: Action<_>) = match this with | Success x -> ifSuccess.Invoke(x) | Failure x -> ifFailure.Invoke(x)
これだけでも、分岐処理を非常に簡単に記述することができるようになります。
// C# // 生成 var res = Result<int, string>.NewSuccess(42); // 分岐 var x = res.Match<string>( s => (s + 1).ToString(), f => "!" + f); // 分岐(副作用) res.Action( s => /* Successの時の処理 */, f => /* Failureの時の処理 */);
あとは、F# 用にモジュールを、C# 用に拡張メソッドを提供するだけです。
(* F# *) module Result = let fold f seed = function Success x -> f seed x | Failure _ -> seed let bind f = function Success x -> f x | Failure x -> Failure x let map f = function Success x -> Success(f x) | Failure x -> Failure x let count = function Success _ -> 1 | Failure _ -> 0 let exists f = function Success x -> f x | Failure _ -> false let forall f = function Success x -> f x | Failure _ -> true let iter f x = fold (fun _ x -> f x) () x let isFailure = function Success _ -> false | Failure _ -> true let isSuccess = function Success _ -> true | Failure _ -> false open System.Runtime.CompilerServices open System.ComponentModel [<EditorBrowsable(EditorBrowsableState.Never)>] [<Extension>] type public ResultExtensions = [<Extension>] static member Fold(x, seed, f: Func<_, _, _>) = Result.fold (fun a b -> f.Invoke(a, b)) seed x [<Extension>] static member Bind(x, f: Func<_, _>) = Result.bind f.Invoke x [<Extension>] static member Map(x, f: Func<_, _>) = Result.map f.Invoke x [<Extension>] static member Count(x) = Result.count x [<Extension>] static member Exists(x, f: Func<_, _>) = Result.exists f.Invoke x [<Extension>] static member Forall(x, f: Func<_, _>) = Result.forall f.Invoke x [<Extension>] static member Iter(x, f: Func<_, _>) = Result.iter f.Invoke x [<Extension>] static member IsFailure(x) = Result.isFailure x [<Extension>] static member IsSuccess(x) = Result.isSuccess x
EditorBrowsable 属性はなくてもいいんですが、付けておくと VS の IntelliSense を汚さないのでいい感じです。
コンピュテーション式に対応するクエリ式の提供
コンピュテーション式を提供する場合、C# 側にはクエリ式を提供することを考えましょう。
例えば先ほどの Result は、コンピュテーション式を提供すると便利です。
(* F# *) type ResultBuilder () = member this.Bind(x, f) = Result.bind f x member this.Return(x) = Success x member this.ReturnFrom(x: Result<_, _>) = x member this.Zero () = Failure() member this.Combine(e1, e2) = Result.bind (fun () -> e2) e1 let result = ResultBuilder()
result を提供することで、失敗の場合を気にせずに処理を書くことができるようになります。
(* F# *) let res = result { let! x = tryGetX() let! y = tryGetY() let! z = tryGetZ() let adjust = getAdjust() return adjust * double (x + y + z) }
C# 用には、Select と SelectMany を提供し、クエリ式が使えるようにします。
(* F# *) [<EditorBrowsable(EditorBrowsableState.Never)>] [<Extension>] type public ResultExtensions = (* 略 *) [<Extension>] static member Select(x, f: Func<_, _>) = Result.map f.Invoke x [<Extension>] static member SelectMany(x : Result<'a, 'TFailure>, f: Func<'a, Result<'b, 'TFailure>>, selector: Func<'a, 'b, 'c>) : Result<'c, 'TFailure> = x |> Result.bind (fun x -> f.Invoke x |> Result.bind (fun y -> Success (selector.Invoke(x, y))))
コンピュテーション式での let! が from in に、let が let に、return が select になったと思って使えます。
// C# var res = from x in TryGetX() from y in TryGetY() from z in TryGetZ() let adjust = GetAdjust() select adjust * (x + y + z);
ここまで考慮すれば、C# 側からもそれなりに使いやすいライブラリが提供できるようになります。
F# だけで閉じずに生きたいものですよね。
え? VB はどうしたって?
や、VB 分かりませんし・・・
次は・・・
次は、Gab_km さんの NaturalSpec なネタらしいです。楽しみですね!
あ、参加者の皆さんは、ネタかぶりを避けるためにも #FsAdventJP タグをつけて書く予定のネタを Twitter でつぶやいとくと良いかもです。
いげ太さんに reply を飛ばしておくとなおよし。