JavaでTupleってみる

可変長な型変数の表現より、タプルの話です。

.NETのタプル

.NETでは、型引数の数が違うクラスを定義できるので、Tupleという名前で7要素まで対応しています。

public class Tuple<T1> { ... }
public class Tuple<T1, T2> { ... }
public class Tuple<T1, T2, T3> { ... }
public class Tuple<T1, T2, T3, T4> { ... }
public class Tuple<T1, T2, T3, T4, T5> { ... }
public class Tuple<T1, T2, T3, T4, T5, T6> { ... }
public class Tuple<T1, T2, T3, T4, T5, T6, T7> { ... }

そして、各要素にアクセスするために、Item1, Item2のようなプロパティを使います。

さて、では.NETのタプルは8要素以上は対応していないかと言うと、そうではありません。 タプルのインスタンスを作るためには、Tuple.Createというメソッドを使って作るのが楽です。 そしてこのTuple.Createは、なんと8引数版まで用意されているのです。

var t = Tuple.Create(true, 2, 3.0, "4", new[] { 5 }, 6M, 7L, 8);

このtの型は、こうなります。

Tuple<bool, int, double, string, int[], decimal, long, Tuple<int>>

実は、Tupleは8番目の型引数として、「残りの要素を保持するタプルの型」が受け取れるようになっています。 ちなみにTuple<T1>という謎のクラスはこの時のみ使われるクラスです。

public class Tuple<T1, T2, T3, T4, T5, T6, T7, TRest> { ... }

「残りの要素を保持するタプル」にアクセスするためには、Item8ではなく、Restという名前のプロパティを使います。 そのため、8要素タプルの8番目の要素にアクセスしたい場合は、

var value = t.Rest.Item1;

となります。 さてさて、ここからが.NETの残念なところなのですが、実は9要素以上のタプルを作る簡単な方法は用意されていません*1。 ので、コンストラクタを使うことになります。

var t = new Tuple<int, int, int, int, int, int, int, Tuple<int, int>>(
    1, 2, 3, 4, 5, 6, 7, Tuple.Create(8, 9));

コンストラクタの型パラメータは省略できないので、悲惨なことになっていますね・・・ これは、Tuple.Createの8要素版が8要素タプルを作るためではなく、ネストしたタプルを作るためのヘルパーになっていればもっと楽が出来たのです。

var t = Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create(8, 9)); // こうはなっていない

残念すぎる・・・

Restという考え方

さて、.NETのタプルは残念ですが、「足りない部分はRestで」というのはどこかで聞いたことのあるような話です。 そう、コンスセルですね!

ということで、コンスセルの考え方を使ってタプルのないJavaにタプルを作ってみましょう。 まずは、コンスセルの終端を表すためのクラスを導入します。 以降では、特にパッケージとか書きませんけど、全部同一パッケージに入っていると思ってください。

public final class TupleNil {
    static final TupleNil nil = new TupleNil();
    private TupleNil() {}
}

簡単ですね。 こいつは状態を持っておらず、インスタンスも外部からは生成できず、 さらには唯一の静的フィールドであるnilもパッケージプライベートなので、 パッケージ外からはこのフィールドにすらアクセスできません。 これだけでは本当に全く何の役にも立たないクラスです。

次に、2つの型パラメータを取って実際に要素を保持するクラスを作ります。

public final class TupleCons<T, Rest> {
    public final T value;
    public final Rest rest;

    TupleCons(T value, Rest rest) {
        this.value = value;
        this.rest = rest;
    }
}

これも簡単ですね。 単に、値を2つ持っているだけのクラスです。 このクラスのvalueに値が、restに残りの要素を表すものが格納されます。

コンストラクタがパッケージプライベートなので、パッケージ外からこのクラスのインスタンスを生成することはできません。

最後にタプルを作るためのメソッドを持ったクラスです。

public final class Tuple {
    private Tuple() {}

    public static <T> TupleCons<T, TupleNil> singleton(T value) {
        // こことか
        return new TupleCons<T, TupleNil>(value, TupleNil.nil);
    }

    public static <T1, T2, Rest> TupleCons<T1, TupleCons<T2, Rest>> cons(T1 value, TupleCons<T2, TupleNil> rest) {
        // ここってダイアモンド演算子使えるんですか?使えそうだけどJavaとかわかりません><
        return new TupleCons<T1, TupleCons<T2, Rest>>(value, rest);
    }
}

あとは、これを使ったコードです。

TupleCons<Integer, TupleCons<String, TupleNil>> t2 = Tuple.cons(42, Tuple.singleton("hoge"));
System.out.println(t2.value); // 42
System.out.println(t2.rest.value); // hoge

やったー!(何が

まとめ(ない)

  • .NETのタプルは割と現実見て、7要素までは自然に扱える
    • 大量要素のタプル使うな
    • でももし扱う場合があるといけないから、一応対応しとくぜ!
  • .NETのTuple.Createはクソ
    • 8要素版をなぜそういう形で用意したし
    • 9要素以上のタプルを作りたい場合はコンストラクタで頑張るしかない
    • それLangExtならCreate.Tuple(1, 2, 3, 4, 5, 6, 7, 8, 9)でできるよ!
    • それF#なら(1, 2, 3, 4, 5, 6, 7, 8, 9)でできるよ!
  • タプルってコンパイル時のリスト(コンスセル)だよね!
    • Javaでそういう実装してみた
    • 元記事が後ろに後ろに拡張していくのに対して、こっちは(consで)前に前に拡張していくという違いがちょっと面白い
    • 元記事がインスタンスメソッドで拡張していくのに対して、こっちはクラスメソッドで拡張していくのもちょっと面白い
      • インスタンスメソッドにもできるけど、キモイことになる(字面上は後ろに書いたものが前に追加される)のでやめた
    • 実用性?しりませんなぁ

*1:まぁ、そんなタプル作るなと言われればその通り、としか言えないですが

コンピュテーション式におけるreturnとyield

今日、id:htid46 とF#の話をしつつ帰った時のまとめです。

前提条件

次の2つのエントリを読んでいることが前提です。

  1. 詳説コンピュテーション式 - ぐるぐる~
  2. コンピュテーション式の実装にStateを用いる - pocketberserkerの爆走

returnとyieldの変換規則

先日のエントリでも書いたように、ReturnとYield、ReturnFromとYieldFromは全く同じ変換のされ方をします。

T(return e, C) = C(b.Return(e))
T(yield e,  C) = C(b.Yield(e))
T(return! e, C) = C(b.ReturnFrom(src(e)))
T(yield! e,  C) = C(b.YieldFrom(src(e)))

つまり、ReturnもYieldも同じ実装にしたとしても、コンパイルは通ります。 ということは、そのコンピュテーション式により合うと思う方を実装すればいい・・・?

returnの意味とyieldの意味の違い

ところで、returnyieldではそれを使ったコードの持つ意味が違うように思えます。 例として、listコンピュテーション式を作ったとしましょう。

let f x = list {
  if x = 0 then
    return -1
  return 1
}
let g x = list {
  if x = 0 then
    yield -1
  yield 1
}

let f0, f1 = (f 0, f 1)
let g0, g1 = (g 0, g 1)

さて、f0, f1, g0, g1のそれぞれの値は、どうなっていてほしいでしょうか?

こうなっていることを期待しませんか?

val f0 : int list = [-1]
val f1 : int list = [1]
val g0 : int list = [-1; 1]
val g1 : int list = [1]

つまり、returnはそこで処理を打ち切るけど、yieldは打ち切らない、という違いがあるように思うのです。 これ、C#で考えてみた場合、yield breakyield returnの関係に似ていませんか?

F#にはyield breakがない

そういえばありませんでした。 が、OptionBuilderでの実装を使うと、returnyield breakの代わりになるのでは!?

ということで、こういう実装を考えてみました。

open System
open Basis.Core.ComputationExpr

(* 最初にpredを満たさなかった要素は結果に含めるtakeWhileの別バージョン *)
module Seq =
  let takeWhileButFirst pred xs = seq {
    let cont = ref true
    use itor = (xs :> _ seq).GetEnumerator()
    while itor.MoveNext() && !cont do
      let x = itor.Current
      if not (pred x) then
        cont := false
      yield x
  }

type ListBuilder internal () =
  member this.Zero() = [], Continue
  (* returnはBreak、yieldはContinue *)
  member this.Return(x) = [x], Break
  member this.ReturnFrom(xs: _ list) = xs, Break
  member this.Yield(x) = [x], Continue
  member this.YieldFrom(xs: _ list) = xs, Continue
  (* fしていって、Breakが出たら後ろは捨てる(isBreakをtrueにしたうえでfalseを返す) *)
  member this.Bind(xs, f: _ -> _ list * FlowControl) =
    let isBreak = ref false
    let res =
      xs
      |> Seq.map f
      |> Seq.takeWhileButFirst (function _, Continue -> true | _ -> isBreak := true; false)
      |> Seq.collect fst
      |> Seq.toList
    (res, if !isBreak then Break else Continue)
  (* Continueだったらrestを実行してappend、Breakだったらrestは捨てる *)
  member this.Combine((x: _ list, cont), rest: unit -> _ list * FlowControl) =
    match cont with
    | Break -> x, Break
    | Continue -> let rest, cont = rest () in List.append x rest, cont
  (* 以降、OptionBuilderと型以外は同じ定義 *)
  member this.While(guard, f) =
    if not (guard ()) then this.Zero()
    else let x = f () in this.Combine(x, fun () -> this.While(guard, f))
  member this.For(xs: #seq<_>, f) =
    this.Using(
      xs.GetEnumerator(),
      fun itor -> this.While(itor.MoveNext, fun () -> f itor.Current))
  member this.Delay(f: unit -> _ list * FlowControl) = f
  member this.Run(f) = f () |> fst

let list = List.ListBuilder()

Bindは複雑ですが、まぁ、こんなもんでしょう。 最初に出てきたBreakは結果に含めたいので、takeWhileではなく別バージョンを定義して呼び出しています。

Combineも難しくはないでしょう。

ミソは、Return系とYield系で、タプルの第二要素が違う点です。 こうしてみると、シグネチャや変換規則が同じなだけで、ReturnとYieldは別のものだという感じがしませんか?

C#yield breakは式をとれないため、yield returnしてからyield breakする必要がありました。 ですが、今回実装したlistコンピュテーション式は、return exprとすることで値を返しつつ処理を抜けることができます。 ちなみに、C#でのyield breakがしたい場合は、return! []とでもするといいでしょう。

カスタムオペレータが使えたら素敵!

queryコンピュテーション式のheadなどのように、yieldBreakのようなカスタムオペレータが作れれば、よりそれっぽいと思いませんか?

・・・が、これは出来ません。 先日は簡単のために省略したqというパラメータを覚えているでしょうか? これは、「その中でカスタムオペレータが使えるかどうか」を示すフラグです。 通常、yieldBreakしたいのはifの中ですが・・・お気づきですね。 最後のパラメータがqです。

T(if e then ce1 else ce2, V, C, q) = Assert(not q); C(if e then {| ce1 |}0 else {| ce2 |}0)

ifを使うためには、qfalseである必要があります。 つまり、カスタムオペレータが使えない場所にしかifは書けないんですね。残念・・・

ちなみに、elseを伴わないifは、elseを伴うifに変換されたうえでさらに変換が走るので、結局上のAssert(not q)から逃れることは出来ません。 無念・・・

最後に

returnの挙動をどうするのがいいのか、実は2通り考えたんですが、どういう方針で行けばいいのか指針がないのでつらかったです。 yield breakをまねる、という方針があったのでよかったのですが、それがなかった場合はもう一方の(実装が楽な方)を実装してしまっていたでしょうね。 たぶん、それがなかったら今回のエントリの挙動ではなく、簡単な方を実装して済ませていたと思われます。

さて、もう一つの挙動はどんなものでしょうか。 これは読者への課題にしますね!

詳説コンピュテーション式

コンピュテーション式とは

コンピュテーション式とは、機能を制限したマクロです。 ・・・では投げやりすぎるので、もうちょっとだけ説明を試みると、 「式変形によって言語の用意する構文の意味をカスタマイズできるようにする仕組み」です。 モナド用の構文として紹介されることもありますが、それはコンピュテーション式という仕組みの上でモナドを扱っているだけに過ぎません。

もっとも、コンピュテーション式はモナド用の構文として使うことが一番多いでしょうから、 モナド用の構文と理解しても問題はないでしょう。 また、このような状況を考えると、モナド以外のことにコンピュテーション式を使う場合は、 現状では「これはモナドではありません」という表明をドキュメントなりなんなりでしておくのが無難でしょう。 特に、let!returnを提供する場合でコンピュテーション式をモナドではない構文とする場合は、 うるさいくらいその旨を伝えるようにしましょう。 作ったものがモナドっぽいけど確信が持てない・・・という場合にも、「モナドっぽいけど本当にそうかどうかはちゃんと確認してないよ」とか書いておくといいでしょう。 これは、見ず知らずの誰かのためですし、自衛のためでもあります。

このエントリはコンピュテーション式の仕様を、 日本語で出来る限り分かりやすく解説することを試みたナニカです。 コンピュテーション式自体を使ったことがある人を対象に、その変換の仕組みを説明します。 カスタムオペレータがかかわる部分を省いてあるため、仕様書よりも単純化されており、より理解しやすくなっていると思います。 これを読んでから仕様書を読めば、カスタムオペレータ部分を理解するのもそう難しくはないでしょう。

詳説コンピュテーション式

コンピュテーション式は、以下の構文で表されます。

builder-expr { cexpr }

builder-exprは、後述するReturnやBindなどのメソッドを持つ型(以降、ビルダー)をもった式です。 cexprは、通常のF#の文法に加えて、この中でのみ使える記法(let!など)を加えた文法によって書かれた式です。 cexprの中で使える記法は、ビルダーがどんなメソッドを提供しているかによります。 例えば、ビルダーがBindメソッドを提供している場合はcexprの中でlet!が使えますが、 提供されていない場合はlet!は使えません。

builder-exprには通常、後述するビルダーのインスタンス(例えば、asyncなど)を指定しますが、 関数呼び出しの結果やメソッド呼出しの結果としてビルダーが戻ってくるようなコンピュテーション式も当然考えられます。

let x = someCompExprBuilder (a, b) { ... }

これは、コンピュテーション式の動作を使う側で変更したい場合や、 ビルダーが内部に状態を持つなどして使いまわせない場合に有効です。

これを使っている例としては、Basis.CoreResultWithZeroBuilderがあります。 これは、コンピュテーション式を使う側でゼロ値を指定することで、 通常のResultBuilderでは提供不可能な構文をサポートしています。

(* xに負の数を指定した場合、Zeroメソッドが呼び出され、
   resultWithZeroに指定した"oops!"がFailureに包まれて返る *)
let f x = resultWithZero "oops!" {
  if x >= 0 then
    return x * 2
}

let x = f 10  (* => Success 20 *)
let x = f -10 (* => Failure "oops!" *)

コンピュテーション式の変換

コンピュテーション式はまず、以下のように変換されます((bは「フレッシュな変数(名前が他とかぶらない変数)」です))。

let b = builder-expr in {| cexpr |}c

ここで、{| ... |}cという表記が出てきますが、これは「その中の式をコア言語の式(つまり通常のF#の式)に変換する」ことを意味します。 似たような表記に、{| ... |}0もありますが、これは「カスタムオペレータが許されるかどうか」が違います。 {| ... |}cの方はカスタムオペレータが許されますが、{| ... |}0の方はカスタムオペレータが許されません。

以降では、話を単純化するためにカスタムオペレータについては省略します。 そのため、cや0といった使い分けはせず、単純に{| ... |}という表記を使いますが、言語仕様を読む際は注意してください。

また、builder-exprは1回しか実行されない点に注意してください。 cexprの変換中にビルダーにアクセスする際は、bを通してアクセスされます。 このルールによって変換の際に使われるオブジェクトは同じものであることが保証されているため、 毎回ビルダーを作るようにすれば、ビルダーの中に状態を持っても大丈夫なのです。

Run, Delay, Quote

ビルダーにRun, Delay, Quoteの各メソッドが実装されている場合、 コンピュテーション式は先ほどの変換の際に、これらのメソッドの呼び出しを差し込みます。

3つとも実装されている場合、以下のように変換されます。

let b = builder-expr
b.Run(<@ b.Delay(fun () -> {| cexpr |}) @>)

変換の順番としては、Delay, Quote, Runの順で変換されます。 もし対象のメソッドがなかった場合は、対応する変換は行われません。

Delay変換

ビルダーにDelayメソッドが存在する場合、cexprの変換した式をラムダ式で囲い、Delayに渡します。

builder-expr { cexpr }

これが、以下のように変換されます。

let b = builder-expr
b.Delay(fun () -> {| cexpr |})

ビルダーにDelayが存在しない場合、b.Delay(fun () -> {| cexpr |})の部分は単に{| cexpr |}となります。

{| cexpr |}に対する変換の結果の式を、delayed-exprとします。 Delayが存在する場合はb.Delay(fun () -> {| cexpr |})、存在しない場合は{| cexpr |}delayed-exprです。

Delayは、ここでの変換よりも、他の変換の中で現れる際に重要になってきます。 whileの変換までDelayは出てきませんが、Delayの存在はその時まで頭の片隅にでも置いておいてください。

Quote変換

ビルダーにQuoteメソッドが存在する場合、delayed-exprをコード引用符<@ ... @>で囲みます。

let b = builder-expr
delayed-expr

これが、以下のように変換されます。

let b = builder-expr
<@ delayed-expr @>

ビルダーにQuoteメソッドが存在しない場合、コード引用符で囲わずに、特に変換は行いません。

delayed-exprに対する変換の結果の式を、quoted-exprとします。 Quoteが存在する場合は<@ delayed-expr @>、存在しない場合はdelayed-exprquoted-exprです。

このように、Quoteメソッドは呼び出されることはありません。 単に、存在すればいいだけのメソッドです。 ビルダーに対する属性にしなかったのは、拡張メソッドでQuoteの機能を後付けできるようにするためでしょうか?

Run変換

ビルダーにRunメソッドが存在する場合、quoted-exprをRunに渡します。

let b = builder-expr
quoted-expr

これが、以下のように変換されます。

let b = builder-expr
b.Run(quoted-expr)

ビルダーにRunメソッドが存在しない場合、特に変換は行いません。

Runは最後に一回だけ呼び出されるメソッドになるので、このメソッドの結果がコンピュテーション式の結果になります。 活用方法としては、Readerモナド用のビルダーに対して、最終的に値を取り出すビルダーを作るようなものが考えられます。

type Reader<'TEnv, 'T> = 'TEnv -> 'T

type ReaderBuilder () =
  ...

(* 普通のreaderコンピュテーション式 *)
let reader = ReaderBuilder ()

type ReaderBuilderWithRun<'TEnv> (env: 'TEnv) =
  ...
  member this.Run(x: Reader<'TEnv, _>) = x env

(* 一番外側で使うと便利なコンピュテーション式 *)
let readerWithRun env = ReaderBuilderWithRun<_> env

変換のルール

仕様書では、

{| cexpr |}c ≡ T (cexpr, [], fun v -> v, true)
{| cexpr |}0T (cexpr, [], fun v -> v, false)

が変換のルールとして記載されています((このTは、translateもしくはtranslationの略でしょう))。 T(e, V, C, q)とあった時、

  • eは変換されるコンピュテーション式
  • Vはこれまでにバインドされた変数群
  • Cは変換済みのコンテキスト情報
  • qはカスタムオペレータを許すかどうかの真偽値

を表します。 Cの部分に変換されたものが入ると考えましょう。上の変換規則では、まだ何も変換されてないので、fun v -> vと、id関数になっています。

この中で、Vはカスタムオペレータに関係する変換中で使われるもののため、 またqもカスタムオペレータが絡む変換の際に必要になってくるもののため、今回は無視します。 そのため、このエントリでは

{| cexpr |}T (cexpr, fun v -> v)

という単純化したルールを使います。

returnの変換とyieldの変換

returnの変換は、以下のルールで表されます。

T(return e, C) = C(b.Return(e))

これは、return eb.Return(e)に変換されることを意味します。

簡単な例を考えてみましょう。

type SimpleBuilder () =
  member this.Return (x) = x

let simple = SimpleBuilder ()

let res = simple { return 10 }

この最後の行を変換してみます*1

(* 1. 全体の変換 *)
let res =
  let b = simple
  {| return 10 |}

(* 2. {| cexpr |}形式をT(e, C)形式に変換 *)
let res =
  let b = simple
  T(return 10, fun v -> v)

(* 3. T(return e, C) = C(b.Return(e))のルールを適用 *)
let res =
  let b = simple
  ((fun v -> v) (b.Return(10)))

(* 4. ラムダ式部分を計算 *)
let res =
  let b = simple
  b.Return(10)

変換できました。

yieldの変換は、

T(yield e, C) = C(b.Yield(e))

と、returnyieldに、ReturnがYieldになっただけなので、同じステップで変換できます。

return!の変換とyield!の変換

return!の変換は、以下のルールで表されます。

T(return! e, C) = C(b.ReturnFrom(src(e)))

これは、return! eb.ReturnFrom(src(e))に変換されることを意味します。

src(e)は、ビルダーがSourceメソッドを持っており、 かつ最も内側のForEachがユーザによるもの(変換により生成されたコードではなく、ユーザが書いたコードであるということ)である場合のみ、 b.Source(e)に変換されます。

2番目の条件は仕様書からのものですが、どうも実際の実装はちょっと違っているようです。 コードを軽く眺めただけですが、ForEachとForEachThenJoinOrGroupJoinOrZipClauseのForEach部分とLetOrUseBangの場合に、 ユーザコードかどうかの判定をしているようで、そのほかの部分ではビルダーにSourceメソッドがあればそれを呼び出しているようです。 そのため、return! eはビルダーにSourceメソッドが存在すればb.ReturnFrom(b.Source(e))に、存在しなければb.ReturnFrom(e)に変換されます。

yield!は、ReturnFromではなくYieldFromが使われる以外は同じです。

letの変換

letの変換は以下のルールで表されます。

T(let p = e in ce, C) = T(ce, fun v -> C(let p = e in v))

これは、returnreturn!とは異なり、変換後にもT(e, C)形式が出てくるとおり、変換後もさらに変換が行われます。 実際にどう変換されるかを一歩一歩確認していきましょう。

letの変換を追う

returnの時に定義したSimpleBuilderを使い、以下のコンピュテーション式を変換してみます。

let res = simple {
  let a = 10
  return a * 2
}

letの変換規則はこうでした。

T(let p = e in ce, C) = T(ce, fun v -> C(let p = e in v))

これを使って変換します。 returnの時も言いましたが、変換の途中は有効なF#コードではないことに注意してください。

(* 1. 全体の変換 *)
let res =
  let b = simple
  {| let a = 10 in return a * 2 |}

(* 2. {| cexpr |}形式をT(e, C)形式に変換 *)
let res =
  let b = simple
  T(let a = 10 in return a * 2, fun v -> v)

(* 3. T(let p = e in ce, C) = T(ce, fun v -> C(let p = e in v))のルールを適用 *)
let res =
  let b = simple
  T(return a * 2, fun v -> (fun v -> v) (let a = 10 in v))

(* 4. Tの最後の部分を計算 *)
let res =
  let b = simple
  T(return a * 2, fun v -> let a = 10 in v)

(* 5. T(return e, C) = C(b.Return(e))のルールを適用 *)
let res =
  let b = simple
  ((fun v -> let a = 10 in v) (b.Return(a * 2)))

(* 6. ラムダ式部分を計算 *)
let res =
  let b = simple
  let a = 10
  b.Return(a * 2)

これで変換できました。 ステップ5のコードは、有効なF#のコードにも見えますが、Returnの引数に出てくるaがどこにもないことからもわかる通り、 最終段階まで変換するまでは有効なF#のコードではないことに注意してください。

変換の結果を見ると、let p = e in ceの変換は、ceの部分のみ変換することになります。 これをT(e, C)形式ではなく{| ... |}形式で書くと、

{| let p = e in ce |} = (let p = e in {| ce |})

となります。 特に何も定義しなくても、letはコンピュテーション式の中でも使えるということですね。

let!の変換

let!の変換は以下のルールで表されます。

T(let! p = e in ce, C) = T(ce, fun v -> C(b.Bind(src(e), fun p -> v)))

letの変換を見たので、この変換は分かりやすいですね。 {| ... |}形式で書くと、

{| let! p = e in ce |} = b.Bind(src(e), fun p -> {| ce |})

となります。 このように、let!を使うためにはビルダーにBindメソッドが定義されている必要があります。 Bindメソッドは引数を2つ取り、2番目の引数は関数である必要があることが分かります。 また、eの型とpの型は一致していなくてもいいことも分かります。

epに代入するように見える構文のため、Bindメソッドの中では第一引数の値(e)を変換するなどして、 第二引数の関数に渡すようにBindメソッドを実装することがほとんどでしょう。 また、通常はBindメソッドはモナドにおける>>=の定義と同じようなものになるでしょう。

useの変換とuse!の変換

useの変換は以下のルールで表されます。

T(use p = e in ce, C) = C(b.Using(e, fun p -> {| ce |}))

ここまで理解できた人にとっては簡単ですね。

use!も見てみましょう。

T(use! p = e in ce, C) = C(b.Bind(src(e), fun p -> b.Using(p, fun p -> {| ce |})))

ちょっと複雑ですね。 が、順番に見て行けば難しくはありません。

SimpleBuilderにUsing(とBind)を追加してみましょう。

type SimpleBuilder with
  member this.Bind(x, f) = f x
  member this.Using(x, f) =
    printfn "start"
    try f x
    finally printfn "end"

これを変換します。

let res = simple {
  use! a = 10
  return a * 2
}

順番に変換していきます。

(* 1. 全体の変換 *)
let res =
  let b = simple
  {| use! a = 10 in return a * 2 |}

(* 2. {| cexpr |}形式をT(e, C)形式に変換 *)
let res =
  let b = simple
  T(use! a = 10 in return a * 2, fun v -> v)

(* 3. T(use! p = e in ce, C) =
      T(ce, fun v -> C(b.Bind(src(e), fun p -> b.Using(p, fun p -> v))))
      のルールを適用 *)
let res =
  let b = simple
  T(return a * 2, fun v -> (fun v -> v) (b.Bind(10, fun a -> b.Using(a, fun a -> v))))

(* 4. Tの最後の部分を計算 *)
let res =
  let b = simple
  T(return a * 2, fun v -> b.Bind(10, fun a -> b.Using(a, fun a -> v)))

(* 5. T(return e, C) = C(b.Return(e))のルールを適用 *)
let res =
  let b = simple
  ((fun v -> b.Bind(10, fun a -> b.Using(a, fun a -> v))) (b.Return(a * 2)))

(* 6. ラムダ式部分を計算 *)
let res =
  let b = simple
  b.Bind(10, fun a -> b.Using(a, fun a -> b.Return(a * 2)))

変換作業には慣れましたか?

use/use!の注意点

use/use!は、let/let!と意味が似ている(変数を導入する)うえ、Usingのシグネチャlet!を使えるようにするために必要なBindのシグネチャと同じようなものであるため、 何も定義せずともuseが使え、Usingによってuse!が使えると思ってしまうかもしれません。 しかし、変換規則を見たとおり、useであってもUsingがビルダーに定義されていなければ使えません。 また、Usingの第一引数はuseの場合はuse p = eeがそのまま、use!の場合はBindの第二引数で渡された関数の引数が入ってくることから、 UsingはそのままBindに相当するような機能*2は持たせられず、単にletlet!をラップする存在でしかないことが分かります。

useletに対してUsing機能を付けたもの、use!let!に対してUsing機能*3を付けたもの、と考えてください。 通常、Usingの実装は以下のようになるでしょう。

member this.Using(x: #IDisposable, f) =
  try
    f x
  finally
    match x with
    | null -> ()
    | _ -> x.Dispose()

doの変換

doの変換は以下のルールで表されます。

T(do e in ce, C) = T(ce, fun v -> C(e; v))

特に問題はないでしょう。eが実行された後、変換されたceが実行されるだけです。

do!の変換

do!の変換はルールが2つあり、後ろに式が続く場合と、do!より後ろにこれ以上変換する式がなかった場合で変換のされ方が若干異なります。

T(do! e in ce, C) = T(let! () = e      in ce,         C)
T(do! e;,      C) = T(let! () = src(e) in b.Return(), C)

do!より後ろにこれ以上変換する式がなかった場合、return ()が書かれているような動作になります。 do!の変換規則は、let!に変換してからlet!の変換規則を適用するような形になっています。 DRYでいいですね。 それ以外は、特に難しいところはないでしょう。

do!の変換にはlet!が必要となるため、do!を使うためにはBindメソッドがビルダーに必要になります。 また、場合によってはBindメソッドのほかに、Returnメソッドも必要となります。

if-then-elseの変換

2分岐の条件分岐の変換は以下のルールで表されます。

T(if e then ce1 else ce2, C) = C(if e then {| ce1 |} else {| ce2 |})

特に難しくありませんね。 ビルダーに何も定義しなくても、elseを伴うifを使えることが分かります。

if-thenの変換

elseを省略したifの変換は以下のルールで表されます。

T(if e then ce, C) = T(ce, fun v -> if e then v else b.Zero())

elseを持つifとは違い、ビルダーにZeroメソッドが必要なことが分かります。

elseを省略しないifと表記を合わせると、変換規則は以下のようになります。

T(if e then ce, C) = C(if e then {| ce |} else b.Zero())

matchの変換

matchの変換は以下のルールで表されます。

T(match e with pi -> cei, C) = C(match e with pi -> {| cei |})

piはi番目のパターンを表し、ceiはi番目の式を表します。 パターンが複数あった場合は、それぞれが変換されます。

また、何も定義しなくてもmatchは使えることが分かります。

whileの変換

whileの変換は以下のルールで表されます*4

T(while e do ce, C) = T(ce, fun v -> C(b.While((fun () -> e), b.Delay(fun () -> v))))

Whileのほかに、Delayメソッドも必要であることが分かります。 手で変換してみましょう。 ビルダーには、SimpleBuilderの拡張を使います。

type SimpleBuilder with
  member this.While(cond, body) = while cond () do body
  member this.Delay(f) = f ()

変換する対象はこれです。

let f x = simple {
  while x do
    printfn "while"
    return ()
}

変換してみましょう。

(* 1. 全体の変換 *)
let f x =
  let b = simple
  b.Delay(fun () -> {| while x do let () = printfn "while" in return () |})

(* 2. {| cexpr |}形式をT(e, C)形式に変換 *)
let f x =
  let b = simple
  b.Delay(fun () -> T(while x do let () = printfn "while" in return (), fun v -> v))

(* 3. T(while e do ce, C) =
      T(ce, fun v -> C(b.While((fun () -> e), b.Delay(fun () -> v))))
      のルールを適用 *)
let f x =
  let b = simple
  b.Delay(fun () -> T(let () = printfn "while" in return (), fun v -> (fun v -> v) (b.While((fun () -> x), b.Delay(fun () -> v)))))

(* 4. Tの最後の部分を計算 *)
let f x =
  let b = simple
  b.Delay(fun () -> T(let () = printfn "while" in return (), fun v -> b.While((fun () -> x), b.Delay(fun () -> v))))

(* 5. T(let p = e in ce, C) = T(ce, fun v -> C(let p = e in v))のルールを適用 *)
let f x =
  let b = simple
  b.Delay(fun () -> T(return (), fun v -> (fun v -> b.While((fun () -> x), b.Delay(fun () -> v))) (let () = printfn "while" in v)))

(* 6. Tの最後の部分を計算 *)
let f x =
  let b = simple
  b.Delay(fun () -> T(return (), fun v -> b.While((fun () -> x), b.Delay(fun () -> let () = printfn "while" in v))))

(* 7. T(return e, C) = C(b.Return(e))のルールを適用 *)
let f x =
  let b = simple
  b.Delay(fun () -> (fun v -> b.While((fun () -> x), b.Delay(fun () -> let () = printfn "while" in v))) (b.Return()))

(* 8. ラムダ式部分を計算 *)
let f x =
  let b = simple
  b.Delay(fun () -> b.While((fun () -> x), b.Delay(fun () -> let () = printfn "while" in b.Return())))

変換できました。 コンパイル時に行われる変換はここまでですが、これだと結局どうなるのか分かりにくいので、Delayメソッドを展開してみましょう。 Delayメソッドは単に受け取った関数を実行するだけのものとして実装しているので、b.Delay(fun () -> expr)exprにするだけですね。

(* 内側のDelayを展開 *)
let f x =
  let b = simple
  b.Delay(fun () -> b.While((fun () -> x), let () = printfn "while" in b.Return()))

(* 外側のDelayを展開 *)
let f x =
  let b = simple
  b.While((fun () -> x), let () = printfn "while" in b.Return())

whileを使うためには、このようにWhileメソッドとDelayメソッドが必要です。 が、これで本当に大丈夫でしょうか・・・? 実際に、fを呼び出したときにどう動くのか見てみます。 falseを渡してみましょう。

(* whileの実装:
  member this.While(cond, body) = while cond () do body *)

(* step 1: 呼び出し *)
f false

(* step 2: fを展開 *)
let b = simple
b.While((fun () -> false), let () = printfn "while" in b.Return())

(* step 3: b.Whileの引数を評価 *)
let b = simple
let arg1 = (fun () -> false)
let arg2 =
  printfn "while"
  b.Return()
b.While(arg1, arg2)

(* step 4: b.Whileを展開 *)
let b = simple
let arg1 = (fun () -> false)
let arg2 =
  printfn "while"
  b.Return()
while arg1 () do
  arg2

なにかおかしいのが分かりますか? 変換する前のコードを再掲します。

let f x = simple {
  while x do
    printfn "while"
    return ()
}

変換する前のコードは、printfnによる出力はwhileの中にありました。 そのため、直感的にはxfalseであれば、何も出力されることなくループが終了すると考えてしまいます。 しかし、変換後のコードではwhileの外側に出力が移動してしまいました。 これでは、xtrueを渡そうがfalseを渡そうが元のwhileの処理が呼び出されてしまいます((さらには、trueを渡したにもかかわらず、whileの中の処理は一回しか実行されません))。 これは使い物になりませんね。

これではまずい場合は、WhileメソッドとDelayメソッドの定義を変更し、Runメソッドを実装することで解決します。

SimpleBuilderを再実装

SimpleBuilderを拡張してきましたが、もう最初らへんのメソッド忘れ気味ですよね。 なので、ここで今までのSimpleBuilderを捨て、新たなSimpleBuilderを定義します。

(* Usingは提供しない *)
type SimpleBuilder () =
  member this.Zero() = Unchecked.defaultof<_>
  member this.Return(x) = x
  member this.Bind(x, f) = f x
  member this.While(cond, body) = while cond () do body () (* body -> body () *)
  member this.Delay(f) = f  (* f () -> f *)
  member this.Run(f) = f () (* new! *)

let simple = SimpleBuilder ()

let f x = simple {
  while x do
    printfn "while"
    return ()
}

fの本体を変換した結果がこちらになります(実際の変換は各自の課題とします)。

let f x =
  let b = simple
  b.Run(b.Delay(fun () -> b.While((fun () -> x), b.Delay(fun () -> let () = printfn "while" in b.Return()))))

今回のビルダーでは、Delayは何もせずに返しますので、b.Delay(fun () -> ...)は単に(fun () -> ...)に展開できます。 展開してみましょう。

(* 内側のDelayを展開 *)
let f x =
  let b = simple
  b.Run(b.Delay(fun () -> b.While((fun () -> x), (fun () -> let () = printfn "while" in b.Return()))))

(* 外側のDelayを展開 *)
let f x =
  let b = simple
  b.Run(fun () -> b.While((fun () -> x), (fun () -> let () = printfn "while" in b.Return())))

(* Runも展開してしまう *)
let f x =
  let b = simple
  b.While((fun () -> x), (fun () -> let () = printfn "while" in b.Return()))

展開できました。 Runが必要なのは、Delayを実装すると一番外側も関数にくるまれてしまうため、それを実行しないと全体として関数が戻ってしまうからですね。

元の展開結果と比べてみます。

(* 元の展開結果 *)
let f x =
  let b = simple
  b.While((fun () -> x), let () = printfn "while" in b.Return())

(* 今回の展開結果 *)
let f x =
  let b = simple
  b.While((fun () -> x), (fun () -> let () = printfn "while" in b.Return()))

第二引数部分が関数で包まれたままになりました。 この関数ffalseを渡すと、最終的には以下のようなコードが実行されることになります。

(* whileの実装:
  member this.While(cond, body) = while cond () do body () *)

let b = simple
let arg1 = (fun () -> false)
let arg2 =
  (fun () ->
    printfn "while"
    b.Return())
while arg1 () do
  arg2 ()

これで、思い通りの結果になりますね。

このように、whileの展開にはWhileメソッドとDelayメソッドのみが必要ですが、現実的にはRunメソッドも実装する必要がある場合が多いです。

ce1; ce2の変換

ce1; ce2の変換は以下のルールで表されます。

T(ce1; ce2, C) = C(b.Combine({| ce1 |}, b.Delay(fun () -> {| ce2 |})))

CombineメソッドとDelayメソッドが必要ですね。 簡単です。

ですが、ce1; ce2の変換は、whileなどを実用的なものにするために必要になるため、非常に重要です。

whileを実用するためのCombine

whileの変換で、「whileの展開にはWhileメソッドとDelayメソッドのみが必要ですが、現実的にはRunメソッドも実装する必要がある場合が多いです」と書きましたが、 実はこれだけではまだ足りません。 今のWhileの実装では、while以降に別のceを置けず、かつwhileの本体はunitを返す必要があります。

member this.While(cond, body) =
  while cond () do
    body () (* while式の本体はunitである必要があるため、bodyの型はunit -> unitのみ許される *)

どうすればいいか。そうですね、Combineです。

type SimpleBuilder () =
  let mutable isExit = false
  member this.Zero() = Unchecked.defaultof<_>
  member this.Return(x) = isExit <- true; x
  member this.Bind(x, f) = f x
  member this.Combine(x, f) = if isExit then x else f ()
  member this.While(cond, body) =
    if not (cond ()) then this.Zero()
    else let x = body () in this.Combine(x, fun () -> this.While(cond, body))
  member this.Delay(f) = f
  member this.Run(f) = isExit <- false; f ()

let simple () = SimpleBuilder ()

isExitとCombineの追加、ReturnとWhileの変更を行いました。 さらに、simpleを関数にしています。 詳しい説明は省きますが、これによって以下のようなコードが書けるようになりました。

let f x = simple () {
  while x do
    return 10
  return 0
}

これで、trueを渡すと10が返り、falseを渡すと0が返ります。 展開や、なぜsimpleを関数にしたのかを考えるのは読者の課題とします。

ちなみに、simpleを関数にしなくても済む方法があります。 それを実装した例がBasis.CoreのOptionBuilderなどで見れます。*5 このエントリは、その時の苦労の反動が書くきっかけとなっています。

よくあるコンピュテーション式の実装では、上のようなwhileの中でreturnするコードを上手く扱えないと思います。 「このコードを扱えるコンピュテーション式を実装しているコードが他にもあるよ!」というものがあったらコメント欄で教えてくれると嬉しいです。

tryの変換

tryの変換は以下のルールで表されます。

T(try ce with pi -> cei, C) = C(b.TryWith(b.Delay(fun () -> {| ce |}), fun pi -> {| cei |}))
T(try ce finally e, C) = C(b.TryFinally(b.Delay(fun () -> {| ce |}, fun () -> e)))

ここまで来たら、もはや何も難しいことはありませんね。 ただ、withの変換はちょっと怪しいです。 正しくは多分、こうですね。

T(try ce with pi -> cei, C) = C(b.TryWith(b.Delay(fun () -> {| ce |}), function pi -> {| cei |}))

forの変換

forの変換は以下のルールで表されます。

T(for x in e do ce, C) = T(ce, fun v -> C(b.For(src(e), fun x -> v)))
T(for x = e1 to e2 do ce, C) = T(for x in e1 .. e2 do ce, C)

forも難しいところはありません。 下の形式は、上の形式のforに書き換えたうえで変換するようになっていますね。

eの変換

eの変換は以下のルールで表されます。

T(e, C) = C(e; b.Zero())

簡単ですね。 simple { () }や、simple { printfn "hoge" }などは、Zeroの呼び出しを付けて変換されます。

まとめ

他にもカスタムオペレータが絡む変換規則がいくつかありますが、 取りあえずコンピュテーション式の実装でよく使うであろう変換規則の説明は以上です。 ここまで理解できれば、カスタムオペレータが絡む変換規則も理解できるでしょう。

みなさんもコンピュテーション式の変換規則を理解して、色々なコンピュテーション式を作ってみましょう!

付録 変換規則一覧

このエントリで紹介した変換規則の一覧を載せておきます。

(* {| cexpr |}とT(...)の関係 *)
{| cexpr |}T (cexpr, fun v -> v)
(* return *)
T(return e,  C) = C(b.Return(e))
T(return! e, C) = C(b.ReturnFrom(src(e)))
(* let/let! *)
T(let p  = e in ce, C) = T(ce, fun v -> C(let p = e in v))
T(let! p = e in ce, C) = T(ce, fun v -> C(b.Bind(src(e), fun p -> v)))
(* use/use! *)
T(use p  = e in ce, C) = C(b.Using(e, fun p -> {| ce |}))
T(use! p = e in ce, C) = C(b.Bind(src(e), fun p -> b.Using(p, fun p -> {| ce |})))
(* do/do! *)
T(do e  in ce, C) = T(ce, fun v -> C(e; v))
T(do! e in ce, C) = T(let! () = e in ce, C)
T(do! e;,      C) = T(let! () = src(e) in b.Return(), C)
(* if *)
T(if e then ce1 else ce2, C) = C(if e then {| ce1 |} else {| ce2 |})
T(if e then ce,           C) = C(if e then {| ce  |} else b.Zero())
(* match *)
T(match e with pi -> cei, C) = C(match e with pi -> {| cei |})
(* while *)
T(while e do ce, C) = T(ce, fun v -> C(b.While((fun () -> e), b.Delay(fun () -> v))))
(* ; *)
T(ce1; ce2, C) = C(b.Combine({| ce1 |}, b.Delay(fun () -> {| ce2 |})))
(* try *)
T(try ce with pi -> cei, C) = C(b.TryWith(b.Delay(fun () -> {| ce|}), function pi -> {| cei |}))
T(try ce finally e,      C) = C(b.TryFinally(b.Delay(fun () -> {| ce |}, fun () -> e)))
(* for *)
T(for x in e do ce, C) = T(ce, fun v -> C(b.For(src(e), fun x -> v)))
T(for x = e1 to e2 do ce, C) = T(for x in e1 .. e2 do ce, C)
(* e *)
T(e, C) = C(e; b.Zero())

Delay変換、Quote変換、Run変換は上記規則で表現できないので省略します。

*1:もちろん、変換の途中段階は有効なF#コードではありません

*2:モナドの話をすると、モナドで包まれた値を「はがす」機能

*3:通常は第一引数に対するDispose呼び出し

*4:仕様書上はWhileの第一引数を囲むカッコはありませんが、 このカッコがないとWhileが「ユニットを受け取りタプルを返す関数」を受け取る一引数メソッドと解釈されてしまうため、必要です

*5:id:pocketberserkerコンピュテーション式の実装にStateを用いる - pocketberserkerの爆走で解説記事を書いてくれました!

進捗ダメ報告

進捗Advent Calendar 2013 の14日目

進捗ダメです(挨拶

進捗Advent Calendar 2013の14日目らしいですが、なんですかこのAdvent Calendarは!!!

今日は

NGK2013Bという、名古屋の合同忘年会の日です。 そして、発表者です。 資料はまだない。

ということで、進捗ダメです。

9月は

色々あって、札幌で発表したりしたんですけど、その資料まだ上げれてないです。

進捗ダメです。 年内には上げます。

仕事は

色々と巻き込まれて、タイとか行きました。 その仕事まだやってるんですが、進捗ダメです。明日は休出です。

次の人は

進捗いいといいですね。

明日はkamekoopaさんです。進捗どうですか?

.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に対するコンピュテーション式を提供しています。 独特なところを上げると、

  • returnreturn!で必ずコンピュテーション式から抜け出せる
  • Failure側用のコンピュテーション式も用意している
  • "ゼロ値"を与えることで、通常はResultで用意できないwhileforに対応している

ところです。

returnreturn!でコンピュテーション式から抜けるのを実現しているのは、 Combineです。 こんな感じのCombineの定義は他のライブラリでは見かけないですが、 こうしないとreturnreturn!してもコンピュテーション式から抜け出さないという、 なんだかなぁ、なものになってしまいます。

実は今日までバグがあって、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ですが・・・

*1:他にも、ジェネリックメソッドを関数で置き換えることもできません。

*2:関数を返すバージョンは、引数を一度に与える必要がないという違いはあるけど、ここではそこには触れません。

*3:delegateはあるけど、ううむ

そろそろ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に「欲しかったのはお前じゃないんだ!」を突き付けたい!!!