.NETの標準ライブラリと仲良くする話
F# Advent Calendar 2013の9日目の記事です。
昨日の記事は、id:nenono さんの「F# でリフレクション/式木に触れてみる」でした。 リフレクション、扱いにくいですよねぇ・・・ リフレクションといえば、LangExtシリーズの一つとしてReflectionExtなんてのを作っているんですが、 時間がないうえにいろいろ問題もあって滞ってます・・・
さて、今回は.NETの標準ライブラリと仲良くする話(もしくはBasis.Coreの紹介)です。
はじめに
F#は.NET Frameworkの資産が使えるため、標準状態で色々なことができます。 これはF#の利点の一つですが、.NET Frameworkは関数型言語のために作られたわけではありません。 そのため、F#から.NET Frameworkの標準ライブラリを使うと、F#の標準ライブラリとは違った使い心地を体験することになります。
型推論との相性が悪い
例えば、System.String型のToUpperメソッドを呼び出すとしましょう。
let f (str: string) = str.ToUpper()
このように、メソッド(や、プロパティ)しか呼び出さない場合、型推論が働いてくれないため、strの型を明示してあげる必要があります。 C#などと違い、引数の位置以外でも型の指定は出来ますが、どこかで一回は明示する必要があります。
let f str = (str: string).ToUpper()
カリー化されていない
当然、.NET Frameworkのメソッドはカリー化されていません。 そのため、メソッドに対して部分適用できません。
(* let f (str: string) = str.Substring(1, _) *) (* 上のように書けないので、引数を追加する必要がある *) let f (str: string) n = str.Substring(1, n)
さらに、カリー化されていないので、パイプライン演算子との相性も良くないです。
str |> fun s -> s.ToUpper() |> fun s -> s.Substring(1)
こう書くくらいなら、メソッドチェインしますよね。
str.ToUpper().Substring(1)
高階関数との相性が悪い
一番厄介なのは、高階関数との相性が悪いことです。 例えば、文字列のリストすべてを大文字にした新たなリストを作りたいとします。 こういう場合にList.mapという高階関数を使うのですが・・・
["hoge"; "piyo"; "foo"; "bar"] |> List.map (fun s -> s.ToUpper())
このように、ラムダ式を書く必要があります。 レシーバは、先に固定しておきたいことよりも、最後に決めたいことの方が圧倒的に多いため、 インスタンスメソッドの場合はほぼ常にラムダ式を使うことになります。
もっと.NET Frameworkの関数と仲良くしたい!
いくら.NET Frameworkで用意されたものが使えるといっても、 このままではつらい場合が多いのも事実なのです。 特に、仕事で文字列操作やXMLの操作を行うことが多いので、 これらが楽に行えると(個人的に)とても嬉しいのです。
そのために、.NETの標準ライブラリとF#の橋渡しをするライブラリを作っています。 それが、Basis.Coreです。 「Basis.Core」というidでnuget.orgにも登録してありますので、簡単に使えます。
Basis.Coreとは何であって何でないか
Basis.Coreは、.NET(と、F#)の標準ライブラリで扱える範囲のものをより上手くF#で扱えるようにすることを最大の目標にしています。 そのため、ライブラリとしてはあまり面白味のないものになっています。
Basis.Coreは、以下のものではありません。
- 様々な型を提供するライブラリではありません
- 型プロバイダーを提供するライブラリではありません
- 型クラスを提供するライブラリではありません
- .NETの標準ライブラリの範囲を大きく超える機能を提供するライブラリではありません
これらのものではないため、このライブラリはあれもこれもと詰め込みすぎるようなことにはならないはずです。 まぁ、.NETの標準ライブラリが大きいので、それに合わせて肥大化してしまうことは考えられますが・・・
Basis.Coreを使うと何ができるか
型推論
文字列を大文字化するために、型の明示が必要でした。
let f (str: string) = str.ToUpper()
Basis.CoreのStrモジュールを使うと、型推論が効くようになります。
let f str = str |> Str.toUpper
基本的にはインスタンスメソッドの名前をlowerCamel
にした名前になっています*1。
部分適用
Basis.Coreの関数はカリー化されているため、部分適用可能です。
let f n (str: string) = str.Substring(1, n)
こう書いていたコードは、部分適用を使って書き直せます。
let f = Str.sub 1
パイプライン演算子
カリー化されており、レシーバに相当する引数が最後になっているので、 パイプライン演算子と組み合わせることができます。
str |> fun s -> s.ToUpper() |> fun s -> s.Substring(1)
こう書く必要があり、パイプライン演算子で書く意味が見いだせなかったコードも、
str |> Str.toUpper |> Str.subFrom 1
このとおりです。
高階関数に渡す
カリー化されており、レシーバに相当する引数が最後になっているので、 高階関数に渡す際もラムダ式を書く必要がありません。
["hoge"; "piyo"; "foo"; "bar"] |> List.map (fun s -> s.ToUpper())
こう書く必要があったコードは、
["hoge"; "piyo"; "foo"; "bar"] |> List.map Str.toUpper
こうです。
Basis.Coreで(まだ)できないこと
F#はメソッド以外ではオーバーロードが使えませんが、Basis.Coreはメソッドではなくモジュール関数で構築しています。 そのため、標準ライブラリで用意されているオーバーロードには、それぞれ別名を付ける必要があります。
例えば、System.StringのTrimには、char配列を取るバージョンがありますが、そちらにはtrimChars
という別名を付けています。
現状では対応していないメソッドもあります。 例えば、ToUpperはCultureInfoを取るバージョンがありますが、対応していません。 CultureInfoやIFormatProviderなどを取るメソッドは、 個人的には使用頻度も高くないため、正直何か別名を与えて補完の候補を増やすのは避けたいところです。 そのため、これらのメソッドには別名を与えるのではなく、別モジュールで定義するようにしようと考えています。
F#では同名のモジュールの同名の関数を後から定義したほうで「隠す」ことができるので、
例えばBasis.Core.Culture
名前空間にStr
モジュールを置き、それをopenすることでCultureInfoを取る版のtrim等を使えるようにするのです。
open Basis.Core open Basis.Core.Culture (* ここではStr.trimは、Basis.Core.Culture.Str.trim *) let f culture str = str |> Str.trim culture open Basis.Core (* もしくは、open Basis.Core.NonCultureとか *) (* ここではStr.trimは、Basis.Core.Str.trim *) let g str = str |> Str.trim (* 両方使いたい場合は、フル修飾するかモジュールに別名を付ける *) module CultureStr = Basis.Core.Culture.Str let h culture str = (str |> Str.trim) = (str |> CultureStr.trim culture)
このような感じで、Basis.Coreを発展させていこうと考えています。
その他雑多なこと
Nullable
Nullableって言うと、.NET的にはSystem.Nullable構造体のことを指すと思います。
でも、これってより正しくはNullableValueとかNullableValueTypeとかすべきだったものですよね。
だって、参照型はNullableなんてものに頼らずにnull
が突っ込めるわけですし。
Nullableモジュールを提供する場合にこれがすごく嫌な感じだったんです。
Nullableって言ってるのに、参照型のnullは扱えないの?的な。
これを解決するために、Basis.CoreではNullable.ValueTypeとNullable.ReferenceTypeという2つのモジュールを用意しました。
これにより、どちらも同じように扱えますし、RequireQualifiedAccess属性を付けているので、
null
を扱いにくくしつつサポートする、ということを実現しています。
アクティブパターンも用意していますが、これも値型用のアクティブパターン(NullValとNonNullVal)と参照型用のアクティブパターン(NullRefとNonNullRef)の2つを提供しています。
Result型
最初の方で、
Basis.Coreは、.NET(と、F#)の標準ライブラリで扱える範囲のものをより上手くF#で扱えるようにすることを最大の目標にしています。
と書きましたが、.NETの標準ライブラリにもF#の標準ライブラリにもない型を提供している、例外t的なものがあります。 それがResult型です。
Result型は、成功するかもしれないし失敗するかもしれない処理の結果として使える型です。 こう書くと、Optionと同じようなものに感じますが、実際やりたいことは同じことです。 しかし、Optionの場合、失敗側に原因を持たせることができません。 これを持たせるようにできるようにしたのがResult型です。
実は、F#にはChoiceという型があり、これに対するアクティブパターンや型の別名を与えることでResult型の代わりにすることもできます。 実際に、他のF#のライブラリではそうしていることがほとんどのようです。 ですが、Choiceではどの選択肢の重みも同じイメージを受けるのです。 それに対して、Resultという名前は何らかの結果、それも成功側に重点を置くという主張を型名によって行うことができます。 このメリットが大きいと感じるため、Choiceを使わずにResultという型を提供しています。
getOr/getOrElse/getOr'
OptionとResultに対して、getOr/getOrElse/getOr'という3つのよく似た関数を提供しています。 これらに与えられたCompiledNameはGetOr/GetOrElse/GetOrElseとなっており、 F#から見た場合はgetOr'はgetOrに似た感じを受けますが、 他の言語から見た場合はGetOrElseになります。
getOrは、値がなかった場合にデフォルト値を返しますが、 F#は正格評価言語のため、デフォルト値は常に計算されてしまいます。
let str = opt |> Option.getOr "empty"
これでは駄目な場合があるので、getOrElseというバリエーションがあります。 こちらは、デフォルト値そのものを受け取るのではなく、それを関数で包んで受け取ります。 そのため、値の計算を遅らせることができます。
let str = opt |> Option.getOrElse (fun () -> loadFromDb conStr)
VBのOrが短絡評価されず、OrElseが短絡評価されることを参考に語彙を選んでいます。
更にバリエーションとして、関数ではなくLazy<'T>を受け取るバージョンを用意しています。 これは、挙動としてはgetOrElseと同じであるため、getOrElse'にしようとおもったのですが、 F#ではLazyはlazyキーワードで作ることができます。 そして、getOr'に渡す場合はほぼその場で値を作るだろう、との判断から、getOr'という名前にしました。
let str = opt |> Option.getOr' (lazy "empty")
しかし、F#以外の.NET言語がlazyのようなものを持っているとは限らないため、 CompiledNameの方はGetOrElseとしました。
正直、この選択が正しかったのかという自信はありません。 そもそも、Lazy版を提供しない、という選択肢もあったわけですし・・・
コンピュテーション式
みんな大好きコンピュテーション式ですが、Basis.CoreでもOptionとResultに対するコンピュテーション式を提供しています。 独特なところを上げると、
return
やreturn!
で必ずコンピュテーション式から抜け出せる- Failure側用のコンピュテーション式も用意している
- "ゼロ値"を与えることで、通常はResultで用意できない
while
やfor
に対応している
ところです。
return
やreturn!
でコンピュテーション式から抜けるのを実現しているのは、
Combineです。
こんな感じのCombineの定義は他のライブラリでは見かけないですが、
こうしないとreturn
やreturn!
してもコンピュテーション式から抜け出さないという、
なんだかなぁ、なものになってしまいます。
実は今日までバグがあって、NoneやFailureをreturn!
してもコンピュテーション式から抜け出せませんでした。
これを解決するためにCombineやReturnFromなどのシグネチャをいじくって色々試していたんですが、
残念ながら時間切れでした。
現在の解決策は、コンピュテーション式のビルダーが状態を持つという非常に残念なことになっていますので、
誰か改善を・・・!
改善されました。ビルダーが状態を持つのではなく、状態を引数で引き回すことで解決しています。
あとの2つはあまり面白味はないですね。
次は・・・
ほぼF# Advent Calendar専用のブログと化している2つのアンコールを持つ、 @htid46 さんのアクティブパターンに関する記事のようです。
楽しみですね!
*1:一部例外アリ
オーバーロードって素晴らしいですよね!
オーバーロード
いやぁ、オーバーロードって素晴らしいものですよね。 例えばC#でintを取るメソッドと、stringを取る同じ名前のメソッドを書きたくなったとするじゃないですか。 そんな時でも、C#はメソッドのオーバーロードが出来るので、こう書けるわけですよ。
public Hoge Something(int x) { ... } public Hoge Something(string str) { ... }
素敵ですね!
関数(Funcデリゲート)
では、関数を考えてみましょう。 非ジェネリックなメソッドはreadonlyなフィールドとしても定義できますよね。
public static Func<int, Hoge> Something = ...
このSomethingは、他のメソッドと同じように呼び出せます。 SomethingがHogeクラスに定義されていたとしたら、
var res = Hoge.Something(10);
と出来るわけです。 メソッドの時と変わりませんから、簡単ですね。
ではこれに、stringを受け取ってHogeを返す関数も追加して・・・
public static Func<int, Hoge> Something = ... public static Func<string, Hoge> Something = ...
出来ない!!!
はい。 フィールドやプロパティはオーバーロード出来ないのですね・・・
ということで、関数をフィールド(やプロパティ)として定義出来るように見えたとしても、 C#ではフィールドやプロパティのオーバーロードが出来ないため、メソッドでできること全部は実現できません*1。
関数を返すメソッド
ここで、関数を返すメソッドを考えてみましょう。 例えば、intを受け取ると「stringを受け取ってHogeを返す関数」を返すようなメソッドです。
public static Func<string, Hoge> Something(int i) { ... }
この関数は、引数を1つ渡すと関数が返ってくるので、そこにさらに引数を渡すことでHoge型の値が返ってきます。
// Hoge.Something(10)で返ってきた関数に"hoge"を渡す var res = Hoge.Something(10)("hoge");
これ、2つ引数を取るメソッドと似てませんか?
// public static Hoge Something(int i, string str) { ... } があったとして、 var res = Hoge.Something(10, "hoge");
どちらも、引数を2つ渡すことでHogeが得られます*2。
再びオーバーロード
で、ですね。 引数を2つ取るメソッドはオーバーロード出来ます。
public static Hoge Something(int i, string str) { ... } public static Hoge Something(int i, int j) { ... }
でも、同じようなことが出来る関数を返すメソッドはオーバーロード出来ません。
public static Func<string, Hoge> Something(int i) { ... } public static Func<int, Hoge> Something(int i) { ... }
C#では、戻り値の型が異なるだけのメソッドがオーバーロード出来ないのです。 ここでも、戻り値の型のオーバーロードが出来ないため、メソッドで出来ること全部を実現することができません。
オーバーロード、確かに便利だけど、関数ではできないのがちょっと残念ですね。 最近だと関数を使う場面というのは多くなっているからなおのこと残念さが増します。
F#はどうか
F#では、関数ではオーバーロードを許していません。
何!?いまどきオーバーロードないだって!?!?そんな言語使えるかー!!!
判別共用体という解決策
メソッドではオーバーロードが使えるんですが、ここでは判別共用体を使いましょう。 判別共用体は、F#でユーザ定義型を提供する方法の一つで、「この中のどれか一つ」を表すことのできる型を定義できます。 これを使うと、something関数に渡せる型を次のように定義できます。
(* 文字列か整数を表す型 *) type SomethingArgType = | Str of string | Int of int
非常に簡単に型が作れることが分かります。 この型を使うと、somethingの実装はこう書けます。
let something = function | Str str -> ... | Int i -> ...
呼び出し側はこうです。
let res = something (Str "hoge")
オーバーロードするために型を作るの、ちょっとだるい気もしますけど、簡単に型が作れるので割とありな気はします。 他にも、別モジュールに格納するとか、そもそも関数名を分けるという方法も考えられ、まぁ一番適切だと思うものを選択すればいいです。
さて、F#にはオーバーロードがありませんので、関数を返す関数だろうが関係ありません。
(* 上のSomethingArgTypeとかsomethingとは別物 *) type SomethingArgType = | Str of string | Int of int (* 引数を2つ受け取るsomethingを定義 *) let something i = function | Str str -> ... | Int j -> ... (* 使う *) let res = something 10 (Str "hoge") let res = something 10 (Int 20)
あっ・・・オーバーロードいらない・・・
他にも、オーバーロードを削ったおかげで関数の型推論が出来るだとかのメリットもありますが、それはまた別の時にでも。
何が言いたいか
オーバーロードがいらないは完全に言い過ぎですけど、オーバーロードを入れてしまったがために(後から導入した)関数との統一性がなくなってしまっています。 「便利そうだから」という理由だけで言語に機能を盛り込むのではなく、 導入することによるデメリット(将来予定している機能との相性はどうか、とか)も考えたうえで盛り込んでほしいものですよね。 C#が登場した時期に関数型言語の機能を将来取り入れることを見越していたかは正直微妙*3ですが・・・
そろそろPower Assertについてひとこと言っておくか
タイトルはもちろん釣りで・・・はない!
ちょっと真面目に、Power Assertについて意見を述べたいのです。
そもそもPower Assertって何?
てきとーに説明すると、
普通の比較演算子で普通にassert書けば、失敗時に各部分式の値を表示してくれる
ようなものです。 Groovy製のテスティングフレームワークであるSpockがおそらく本家大本です((要出典。こういう系の発想は割と昔からあったし、Spock以前に実装例がありそうな気がする。そもそも、Spockは最初からPower Assert持ってたのかも調べないといけない。ちなみに、式木を弄ってAssertを組み立てる、というものであれば(PowerAssertよりも情報量は少なくなるものだけど)、自分の知る限りだと2009年6月にこんな記事があります。 http://themechanicalbride.blogspot.jp/2009/06/better-unit-tests-with-testassert-for.html まずはこの時点でのSpockの実装を確認せねば・・・))。
Groovyでこう書くと、
def xs = [0,1,2,3,4] assert 1 == xs.min()
こうなります。
Exception thrown 10 02, 2013 2:57:46 午後 org.codehaus.groovy.runtime.StackTraceUtils sanitize WARNING: Sanitizing stacktrace: Assertion failed: assert 1 == xs.min() | | | | | 0 | [0, 1, 2, 3, 4] false at org.codehaus.groovy.runtime.InvokerHelper.assertFailed(InvokerHelper.java:399) 以下略
おお!値がどうなったか一目瞭然ですね!
Power Assertをユニットテストに使う
どこがどうなったってアサーションに失敗したのかが分かりやすいため、 これをユニットテストのアサーションとして採用する流れがあります。
こんな感じですね。
import groovy.transform.Canonical @Canonical class User { def name def age } def a = new User("hoge", 10) assert a == new User("hoge", 20)
Exception thrown 10 02, 2013 3:05:37 午後 org.codehaus.groovy.runtime.StackTraceUtils sanitize WARNING: Sanitizing stacktrace: Assertion failed: assert a == new User("hoge", 20) | | | | | User(hoge, 20) | false User(hoge, 10) 略
Groovy知らなくても、何が起こっているのかはよくわかると思います。
何が起こっているかは、確かに一目瞭然なのですが・・・
俺たちが欲しかった情報はなんだ?
ユニットテストにおいて、最も欲しいのは「どこがどうなっているか」ではなく、 「どこがどう違っているか」じゃないですかね。
「どこがどうなっているか」だけ渡されても、「どこがどう違っているか」は目視で確認しなけりゃならんのです。 だるいのです。 先の例くらいならまだマシですけど、長い文字列とかだと探すの大変です。
import groovy.transform.Canonical @Canonical class SomeData { String str int i }
Assertion failed: assert a == new SomeData("very long long long string", 20) | | | | | SomeData(very long long long string, 20) | false SomeData(very long long long sting, 19)
19に釣られて、very long long long stringとvery long long long stingの違いを見抜けなくて(本来)無駄なRedになってしまっても、それは仕方がないことですよね。
本当に欲しい情報って、例えばこんなものじゃないですかね?
Assertion failed: equality check is failed. difference: - SomeData.str: ["...st(-)ing", "...st(r)ing"] - SomeData.i: ["(19)", "(20)"]
この下に、どこがどうなったか情報があったら重宝はすると思います。 が、それが最初じゃないでしょう、と言いたいのです。
じゃぁお前が実装しろよ
ここで、「なので実装しました!」とか言えたら超かっちょいいんですけど、
(社内用テスティングフレームワークとして)作りかけて止まっちゃってます・・・
ちょっと別の色々(LangExtとか)に時間が取られちゃってまして・・・
でも、自分が欲しいのは正直こういう形の情報なんですよね。 PowerAssert的な情報は、あると便利だけどそれだけあっても辛いのです。
なので、このエントリの意見に同意してくれて、時間ある人は是非作ってみてほしいんですよね。 Power Assertに「欲しかったのはお前じゃないんだ!」を突き付けたい!!!
ダブル・ディスパッチ~典型的な関数プログラミング・イディオム~
元ネタはダブル・ディスパッチ~典型的なオブジェクト指向プログラミング・イディオム~ です。 これをF#でやってみるとどうなるかやってみましょう。
レンタルショップの例(レベル1)
商品としてCDやDVDを取り扱うレンタルショップを想像・・・するのは面倒でしょうから、コードで示しますね。
type MemberKind = | Common | Gold type Member = { Kind: MemberKind } type ItemKind = | CD | DVD type Item = { Kind: ItemKind } module RentalShop = let calculateRentalFee (item: Item) (member_: Member) = (* 金額を計算する *)
レンタル料の計算が、商品種別と会員種別の組み合わせによって変わるようです。 一般会員(Common)の場合はどちらのそのままの値段で、ゴールド会員(Gold)の場合はCDが50円引き、DVDが100円引きとかになるんでしょうか。
RentalShop.calculateRentalFeeを何のひねりもなしに実装すると、こうなります(後で明らかにしますが、もっと別の方法があります)。
let calculateRentalFee (item: Item) (member_: Member) = match item.Kind with | CD -> match member_.Kind with | Common -> (* 一般会員がCDを借りる場合の料金計算 *) | Gold -> (* ゴールド会員がCDを借りる場合の料金計算 *) | DVD -> match member_.Kind with | Common -> (* 一般会員がDVDを借りる場合の料金計算 *) | Gold -> (* ゴールド会員がDVDを借りる場合の料金計算 *)
matchが入れ子になっていてなんだか「イヤな感じ」がしますね。
レンタル・ショップの例(レベル2)
レンタル・ショップのモデルを少し修正しましょう。 Itemが価格を持っていないわけがないので、Priceを追加します。 ついでに、レンタル料金の計算は商品オブジェクトにやってもらうようにします。
type MemberKind = | Common | Gold type Member = { Kind: MemberKind (* 多分こいつも名前とか色々持ってる *) } type ItemKind = | CD | DVD module CD = let calcRentalFee price = function | Common -> (* 一般会員がCDを借りる場合の料金計算 *) | Gold -> (* ゴールド会員がCDを借りる場合の料金計算 *) module DVD = let calcRentalFee price = function | Common -> (* 一般会員がDVDを借りる場合の料金計算 *) | Gold -> (* ゴールド会員がDVDを借りる場合の料金計算 *) type Item = { Kind: ItemKind Price: int } with member this.CalculateRentalFee(member_: Member) = match this.Kind with | CD -> CD.calcRentalFee this.Price member_.Kind | DVD -> DVD.calcRentalFee this.Price member_.Kind module RentalShop = let calculateRentalFee (item: Item) (member_: Member) = item.CalculateRentalFee(member_)
関数を分けたので、matchのネストがひとつ減りました。 これが「よい感じ」かどうかは置いておきましょう。 さぁさらに複雑にしていきますよ!
レンタル・ショップの例(レベル3)
モデルにさらに手を加え、MemberとItemを相互依存させちゃいましょう!
type MemberKind = | Common | Gold type ItemKind = | CD | DVD type Member = { Kind: MemberKind } with member this.CalculateRentalFeeForCD(item: Item) = match this.Kind with | Common -> (* 一般会員がCDを借りる場合の料金計算 *) | Gold -> (* ゴールド会員がCDを借りる場合の料金計算 *) member this.CalculateRentalFeeForDVD(item: Item) = match this.Kind with | Common -> (* 一般会員がDVDを借りる場合の料金計算 *) | Gold -> (* ゴールド会員がDVDを借りる場合の料金計算 *) and Item = { Kind: ItemKind Price: int } with member this.CalculateRentalFee(member_: Member) = match this.Kind with | CD -> member_.CalculateRentalFeeForCD(this) | DVD -> member_.CalculateRentalFeeForDVD(this) module RentalShop = let calculateRentalFee (item: Item) (member_: Member) = item.CalculateRentalFee(member_)
実際の計算ロジックはすべてMember内に集約されました! これで、会員の種類が増えたとしても手を入れる必要があるのはMemberのみで、Itemなどに手を入れる必要はありません。
しかし、RentalShop.calculateRentalFeeやItem.CalculateRentalFeeは処理を受けて流すだけの内容になってしまい、 これらはノイズとまでは言いませんが、処理を追うのが面倒になりました。
また、扱う商品の種類が増えた場合はItemとMember両方に手を入れなければならないので面倒です。
さらに、例えば新しい軸(曜日によって割引率が変わる、とか)が増えた場合も、どうすればいいんでしょうかこれ。
レンタル・ショップの例(レベル4)
いっそ、モデルの定義とロジックを分割してみましょう。
type MemberKind = | Common | Gold type Member = { Kind: MemberKind } type ItemKind = | CD | DVD type Item = { Kind: ItemKind Price: int } module RentalShop = let calculateRentalFee (item: Item) (member_: Member) = match item.Kind, member_.Kind with | CD, Common -> (* 一般会員がCDを借りる場合の料金計算 *) | CD, Gold -> (* ゴールド会員がCDを借りる場合の料金計算 *) | DVD, Common -> (* 一般会員がDVDを借りる場合の料金計算 *) | DVD, Gold -> (* ゴールド会員がDVDを借りる場合の料金計算 *)
実はこれ、最初のバージョンのmatchのネストを、タプルのパターンマッチに書き換えただけです。 これでよかったんやー。
共通処理の括りだし
常識的に考えて、一般会員に特別な値引きがあるとは思えません。 おそらく、一般会員は商品のレンタル料金そのままの値段でのレンタルになるでしょう。 レベル3のコードだと、その場合の処理の共通化をしようと思うと、関数に括りだすしかありません。
member private this.CalculateCommonMemberFee(item: Item) = item.Price member this.CalculateRentalFeeForCD(item: Item) = match this.Kind with | Common -> this.CalculateCommonMemberFee(item) | Gold -> (* ゴールド会員がCDを借りる場合の料金計算 *) member this.CalculateRentalFeeForDVD(item: Item) = match this.Kind with | Common -> this.CalculateCommonMemberFee(item) | Gold -> (* ゴールド会員がDVDを借りる場合の料金計算 *)
あまり見通しは良くないですね。 それに対して、レベル4のコードだと
let calculateRentalFee (item: Item) (member_: Member) = match item.Kind, member_.Kind with | _, Common -> item.Price (* 一般会員はそのままの値段 *) | CD, Gold -> (* ゴールド会員がCDを借りる場合の料金計算 *) | DVD, Gold -> (* ゴールド会員がDVDを借りる場合の料金計算 *)
と、見通しの良い記述が可能です。
曜日によって割引率が変わりました
という場合でも、レベル4のコードであればRentalShop.calculateRentalFeeに引数を増やすだけで対応可能です。
let calculateRentalFee (item: Item) (member_: Member) (dayOfWeek: DayOfWeek) = match item.Kind, member_.Kind, dayOfWeek with | CD, Common, Tuesday | CD, Gold, _ -> (* 火曜日は一般会員もCDがゴールド会員と同じ割引ですって *) | DVD, Common, Tuesday | DVD, Gold, _ -> (* 火曜日は一般会員もDVDがゴールド会員と同じ割引ですって *) | _, Common, _ -> item.Price
まぁ複雑になりすぎる場合は、コンビネータを合成する方向性の方がいいと思いますが、 オブジェクト指向プログラミングでのダブルディスパッチよりはかなり簡潔に書けていると思います。
ダブルディスパッチしたくなったら、言語を変更できないか考えてみてもいいかもしれませんね。
なごやまつりでF# Type Providerについて(?)話してきた
してきました。 あれだけの人数が集まって、F# 知らない人が全然いないとかすごい勉強会でしたね。
Excel方眼紙、どうにかしたいものですね。 今回作った(作りかけ)コードは、GitHubに置いてあります。
現実と戦うためのRealWorldsというorganizationを作ったので、「これも入れて!」というのがあれば考えます。 今のところ、みずぴーさんの作ったblockdiagcontrib-excelshapeもRealWorldsでホストしています。
飛ばしたところをちょっと補足つけて説明します。
TypeProviderのつくりかた
37枚目です。
TypeProviderは、 チュートリアル : 型プロバイダーの作成 (F#) をやれば作れるようになります。 このなかで、「ProviderTypes API」と呼ばれているものがありますが、 これはF#3.0 Sample Pack のSampleProviders/Sharedの中の「ProvidedTypes-xxx.fs(i)」です。 これをプロジェクトにコピーして使いましょう。 01~04とheadがありますが、headを使えばいいでしょう。
なぜコピーかというと、様々な型がinternalとなっているためです。 全部publicにしてもいいのですが、他の多くのプロジェクト(FSharpx, FSharp.Dataなどなど)がコピーして使っているため、 それに倣っておくのがいいでしょう。
基本的にはスライドでもある通り、ProvidedTypeDefinitionオブジェクトに対してメソッドやプロパティを追加していくことになります。 今回のコードだと、addCtorやaddMembersが該当箇所になるので、気になる方は追ってみましょう。
TypeProviderのデバッグ
38枚目です。
TypeProviderをVSでデバッグしながら開発する場合は、 TypeProvider用のソリューションとデバッグ用のソリューションを分割する必要があります。 これを同じにしてしまうと、VSがdllをつかみっぱなしになるためにビルドが出来なくなってしまいます。
今回スライドに書いた方法であれば、TypeProvider用のソリューションファイルと同じ場所にデバッグ用ソリューション(Sample.sln)を置くだけで誰でもVSでTypeProviderのデバッグが出来るようになります。 注意点を上げるとすると、デバッグ用ソリューションからはプロジェクト参照ではなく、dllを直接参照する必要がある、という点です。 プロジェクト参照してしまっては結局dll掴みっぱなしになってしまいますからね。
あとは、VSを2つ以上立ち上げても平気な環境がないとたぶん厳しいです。
小技
39枚目です。
HideObjectMethodsプロパティについては、上で紹介したチュートリアルに載ってますが、見落としていたので紹介です。 LangExtでは、EditorBrowsable属性を付けて回って頑張ってるわけですが、TypeProviderでProvideすればこのプロパティをtrueに設定するだけでいいんですね・・・便利。
ファイル存在チェックを導入する前は、GUID生成してファイル名にしてました。 これだと、一回コンパイルを走らせるだけで5ファイルくらい生成され、 リアルタイムでコンパイルが走っているためにその後もどんどんファイルが増えていく、という事態に陥りました。 そこで、コンパイルするファイルの中身をSHA-1ハッシュにかけ、ファイル名をそれにすることで存在チェックできるようにしました。
最後のはまぁ、えっと、その。
.NET基礎勉強会でラムダ計算の発表をしてきた
もう一か月以上も前の話ですが、.NET基礎勉強会で(型無し)ラムダ計算の話をしてきました。 .NETと言えばF#、F#の基礎と言えばラムダ計算!ですよね! 発表資料はこちらです。
当日は2 + 3が分からないと好評(?)でした。 当時の様子はこんな感じです。
2+3の計算が難しい @ラムダ計算 #dotNetbase
ぶれいすさんが人間簡約器になり下がっている #dotNetBase
bleis迷子中 #dotnetbase
人間簡約器がバグっている #dotNetBase
blaisさん、2+3で大苦戦中。 #dotNetBase
ぐるぐるさんも分からない #dotNetbase
料理番組よろしく「ここであらかじめ用意しておいたラムダ項を使って〜」とかやらないと厳しいですね #dotNetBase
2+3の計算についていけないなんて…物心ついて以来初めての経験です。 #dotNetBase
2013-07-20 15:00:01 via web
bleisさんとmzpさんがブツブツ言いながら板書する勉強会 #dotNetBase
結論: 2 + 3 is 難しい #dotnetbase
反省して、発表資料に計算過程を追加しておきました。
掛け算も追加してあるので、追ってみてください。 掛け算は、4から5に行くときに、3を表すラムダ項が増殖しているのがミソです。
実際にノートに書くのがおすすめですけど、カッコの対応が分からなくなる恐れがあるので、 色鉛筆使うのがおすすめです。
引き算や割り算、型付きラムダ計算など気になる方は、
- 作者: Benjamin C. Pierce,住井英二郎,遠藤侑介,酒井政裕,今井敬吾,黒木裕介,今井宜洋,才川隆文,今井健男
- 出版社/メーカー: オーム社
- 発売日: 2013/03/26
- メディア: 単行本(ソフトカバー)
- クリック: 68回
- この商品を含むブログ (8件) を見る
をどうぞ! 型付きラムダ計算以降も楽しいですよ!
NullableとOptionの違い
このエントリの最新版はGithubにあります。
Optionそのものについてのエントリは書く必要ない(世の中に有用なドキュメントが山ほどあるから)かな、 と思っていたのですが、Nullableとの違いについてはそれなりに需要がありそうなので書いておきます。
ちなみに、個人的な嗜好によりOptionを持ち上げ、Nullableを下に扱う感じになっていますが、Nullableも(仕方なく)使うことはあります。 特別な理由がなければNullable使わずにOptionを使う、ということでもありますが、そこは一つよろしくお願いします。
Nullableとは
C#ではnull
は参照型でしか使えませんでした。
Nullableは、この制限がない(ように見えるよう特別扱いされている)唯一の値型です。
ジェネリック型になっており、任意の値型を扱うことが出来ます。
// Nullable<int>はint?と書ける int? x = 42; // 値がある int? y = null; // 値がない // こちらは参照型なので、Nullable型ではない string a = "hoge"; // 値がある string b = null; // 値がない
Optionとは
Optionは、「null
よりも安全に値がないことを表せる」ものです。
ジェネリック型になっており、任意の型を扱うことが出来ます。
Option<int> x = Option.Some(42); // 値がある Option<int> y = Option.None; // 値がない Option<string> a = Option.Some("hoge"); // 値がある Option<string> b = Option.None; // 値がない
このエントリでは、OptionはLangExtのOptionを指すものとします。
NullableとOptionの類似点
NullableもOptionも、structで実装されており、値を持っているかどうかを表すフラグと、値を保持しているという点で、構造は似ています。 しかし、似ているのは構造くらいで、目的や使い方などはかなり異なります。
NullableとOptionの違い
パラメータとして渡せる型の違い
Nullableは値型でもnull
を扱えるようにするために作られたため、値型しか型パラメータに指定できません(参照型はそもそも最初からnull
を扱える)。
Nullable<int> i = null; // これはコンパイルエラー //Nullable<string> s = null; // そもそもこう書ける string s = null;
それに対してOptionは、null
を置き換えるために作られているため、値型であっても参照型であっても型パラメータに指定することができます。
Option<int> i = Option.None; Option<string> s = Option.None; // もちろんこれはコンパイルエラー //string s = Option.None;
また、Nullableはネストさせることができません(Nullable<Nullable<int>>
や、int??
はできない)が、Optionは出来る、という違いもあります。
表記上の違い
Nullableは言語組み込みの機能なので、表記はとてもシンプルです。 また、値がある場合はその値が直接書けます。
それに対してOptionは、値がある場合でもOption.Some
の呼び出しが必須になるため、その分面倒です。
任意の型からそのOption型への暗黙の型変換を提供すればこれは解決できるのですが、
これには問題もあるため、LangExtでは提供していません。
その理由については、 Optionの設計 で述べていますので、後で(NullableとOptionの違いを理解したうえで)読んでみてください。
設計方針の違い
Nullable型は値型でnull
を扱えるようにすることを目的としています。
単に値がない状態を表せればよく、それをどう扱うかには興味がありません。
そのため、Nullableが出来るのは基本的には
null
かどうかのチェック- 値の取り出し
の2つだけです。
それに対して、Option型はどうやって値がない状態を上手に扱うかが最大の興味です。 そのため、Optionに対しては様々な操作が可能です。 しかし、Nullableには用意されている「値の取り出し」は、Optionでは簡単にはできないようになっています。
値が取り出せるのは一見便利に見えます。 しかし、値がない場合に値を取り出そうとした場合にどうすればいいかを考える必要があります。 Nullableの場合、
- 値がない場合に例外(InvalidOperationException)を投げるValueプロパティ
- 値がない場合にデフォルト値を返すGetValueOrDefaultメソッド
- 値がない場合に引数で指定した値を返すGetValueOrDefaultメソッド
の3つの方法を用意しています。
Valueプロパティは一番使うのが簡単なので一番多用されがちな方法となりますが、null
チェックが強制されるわけではないため、気を抜くと簡単に例外が投げられてしまいます。
Nullableは「どうやって値がない状態をうまく扱うか」に興味がないため、
この設計選択は妥当と言えるでしょう(ただ、Nullableの場合と参照型の場合とで投げられる例外が違うのがいいことなのかどうかは場合によると思います)。
static string F(int? x) { return x.Value.ToString(); // xがnullならInvalidOperationExceptionが発生 }
しかし、「どうやって値がない状態をうまく扱うか」が最大の興味であるOptionでは、簡単に例外を投げるわけにはいきません。 そこで、Optionがとる方針はこうです。 「値の取得は禁止して、値を受け取った後の処理と、値がなかった(値が受け取れなかった)場合の処理を渡してもらいOptionの中でそれを振り分ければいい!」
static string F(Option<int> x) { // 値がない場合も考慮することを強制するAPI return x.Match( Some: i => i.ToString(), None: () => { throw new Exception(); }); // 今回は例外を投げるだけでいいので、Nullableより面倒 }
こうすることで、ついうっかり値がない考慮が漏れてしまうという失敗を防ぐことができるのが、OptionとNullableの最も大きな違いです。
値がないことの伝搬
T
と演算可能なU
があった時、T?
とU
も演算可能です。
そのため、以下のプログラムは「42.1」を表示します。
int? x = 42; double? y = x + 0.1; if (y != null) Console.WriteLine(y);
ここで、xがnull
だった場合はどうなるでしょうか。この場合、null
が伝搬することになります(比較演算子の場合はまた違います)。
つまり、yもnull
になります。
int? x = null; double? y = x + 0.1; if (y != null) Console.WriteLine(y); // yがnullなので表示されない
では、以下の例ではどうでしょう?
int? x = null; string y = x + "aaa"; if (y != null) Console.WriteLine(y);
この場合、なんと「aaa」が表示されます。
参照型の場合、(自分で定義しない限り)null
の伝搬は起こらなかったので、null
に文字列を加えたときの動作は特別なものではありませんでした。
しかし、Nullableにnull
の伝搬を導入したせいで、Nullableに関してはnull
に文字列を加えたときの動作が特別なものに見えるようになってしまっています。
Optionでどうなるかを見てみましょう。 Optionで同じことをするためには、Optionのままでは演算できないため、MapとIterを使います。 これらの関数は、内部でMatchを使って実装されています。
public static Option<U> Map<T, U>(this Option<T> self, Func<T, U> f) { // fが呼び出されるのは値がある(Someの時)だけ return self.Match( Some: v => Option.Some(f(v)), None: () => Option.None); // Noneは常に伝搬する(文字列が特別扱いされるなどはない) } public static void Iter<T>(this Option<T> self, Action<T> act) { // actが呼び出されるのは値がある(Someの時)だけ self.Match( Some: v => act(v), None: () => { }); }
これらを使うと、「値があるかどうかわからないものに0.1を加え、値があればその結果を表示する」コードはこう書けます。
Option<int> x = Option.Some(42); x.Map(_ => _ + 0.1) .Iter(y => Console.WriteLine(y)); // 「42.1」と表示される
Option<int> x = Option.None; x.Map(_ => _ + 0.1) .Iter(y => Console.WriteLine(y)); // 何も表示されない
値があるかどうかの判定をユーザーコード側で行っていない(MapとIter内部で行っている)という違いはありますが、ここまでの動作はNullableと同様です。 Nullableと違うのは、文字列の結合も他の場合と同じような動作をする点です。
Option<int> x = Option.None; x.Map(_ => _ + "aaa") .Iter(y => Console.WriteLine(y)); // 何も表示されない
Nullableでは、T
型でできることは、極力T?
型でもできるように考えられています。
しかし、そのせいで統一性が崩れてしまっています。
それに対して、OptionはT
型にできる操作をOption[T]
型に対して提供していません(提供できません)。
T
型とOption[T]
型は完全に別の型であり、Optionはあくまで「値があるかないかの表現」以上のことは行いません。
そのため、Optionが保持する値に対して処理を行いたい場合は、MapやIterのような高階関数を使って値を受け取る関数を渡すことになり、
すべての処理が統一性を保っています。
クエリ式の対象にできるかどうか
クエリ式でOptionを扱うと、「値がない場合」を意識せずに処理が記述でき、非常に便利です。
// Option<int> TryGet(string key); があったとする var res = from x in TryGet("x") from y in TryGet("y") select x + y;
この程度の例なら、Nullableでも問題なく扱えます。
// int? TryGet(string key); があったとする var x = TryGet("x"); var y = TryGet("y"); var res = x + y;
しかし、参照型が混じると途端に厳しくなります。
// Optionの場合は特に何も変わらない var res = from x in TryGetInt("x") from y in TryGetStr("y") select x + y; // x(の文字列表現)とyを連結
// Nullableと参照型との演算ではnullが伝搬しないので、nullチェックが必要になる var x = TryGetInt("x"); var y = TryGetStr("y"); var res = // うっ・・・ x == null || y == null ? null : x + y;
これを回避したくて、Nullableにもクエリ式を提供したくなります。
しかし、これらのケースすべてをカバーできるクエリ式を提供することは、C# のオーバーロード解決能力が低いため不可能です。
実際に実装してみると分かるのですが、T?
だけを扱うようなクエリ式は提供可能です。
例えば、
// int? TryGet(string key); があったとする var res = from x in TryGet("x") from y in TryGet("y") select x + y;
ここまでは可能です。
しかし、そこにT when T : class
where T : class
も扱えるようにしようとすると、オーバーロードが解決できないバージョンのSelectManyを書く必要が出てきてしまいます。
NullableとOptionの違い
Nullableは、null
チェックから逃れることを目的としていない時点で良くないアイディアです。
null参照の考案は10億ドル単位の過ちと、
考案者自らが認める過ちであるnull
の適用範囲を広げてしまうNullableを、言語仕様(それどころかCLRにまで)に組み込んでしまうというのは、
個人的にはなんてセンスのない設計をしたんだ、と思ってしまいます。
それに対してOptionは、OOPの側面からみるとNullObjectをジェネリックの力を借りて汎用化したものであり、 非常に好ましいものです。 シグネチャ上で「値がないかもしれない」ことを表すことができるのも、ありがたいです(Nullableは出来るが、参照型ではできない)。
構造は同じなのに、ここまで正反対の意見になるというのは、ちょっと面白いですね。 この違いは、主に提供するAPIから来ており、「値を(簡単には)取り出せない」という制限がOptionの良さの根源にあるというのは、心に留めておいてほしいです。 何でもかんでもできるAPIが常によいというわけではないのです。
(おまけ)Optionからの値の取り出し
・・・とは言うものの、Optionからさくっと値を取り出したい場合があるのも事実です。 例えば、「ここでは絶対値があることが分かりきっている」というような場合です。 そのような場合でも、通常は値がなかった場合の考慮が必要ですが、LangExt.Unsafe名前空間をusingすることで、Optionから直接値を取り出せる関数が使えるようになります。
LangExtのOptionに限らず、たいていのOptionの実装には、このような「直接値を取り出す」関数が提供されています。 そればかりか、場合によっては他のAPIと一緒にこの関数を紹介してしまっている場合もあります。
しかし、Optionの目的を「安全に値がないことを表す」と置くのであれば、この関数をそのように紹介するのはよくないことです。
これを先に教えてしまえば、null
を知っている人の多くは「使いにくいnull
だ」と思いながらOptionを使ってしまうことでしょう。
そういう事態を避けるためにも、Optionから直接値を取り出す関数は、Optionの目的や利点を語った後で教えるべきです。 場合によっては、教えないというのもアリでしょう。 それくらい、慎重に扱うべきものです。