コンピュテーション式の変形後を覗き見るを改良する
さてさて、気になるネタがまた増えたようです。
コンピュテーション式の変形後を覗き見るというエントリがF# Advent Calendar 2014の10日目の記事として公開されました。
Quote
を差し込んで、Run
で表示する、というのは面白いアイディアですね。
だが俺の前でコンピュテーション式の変換を扱ったのが運の尽きだ! 添削をくらうがいい!
・・・同僚だからってやりたい放題ですね。
変換に必要なメソッド
変換対象が
let ret = simple { let x = 10 return x }
程度であれば、必要なメソッドはReturn
だけです。
Bind
もReturnFrom
も不要です。
ですが、消してしまってはあまりにも単純なので、
Bind
を使う例を扱えるようにしましょう!
変換対象はこうなります。
let res = simple { let! x = 10 return x }
このコードがコンパイルできるようにするために必要なコンピュテーションビルダーの定義はこれです。
type SimpleBuilder () = member __.Return(x) = x member __.Bind(x, f) = f x let simple = SimpleBuilder ()
手動で変換する
では、このコードを変換していきます。
let res = simple { let! x = 10 return x }
一番外側はいいでしょう。
let res = let b = simple in {| let! x = 10 in return x |}
let!の変換
{|
と|}
に挟まれた部分(cexpr)がlet! ...
という形をしているので、
let!
の変換規則を最初に適用します。
let b = simple in T(return x, fun v -> C(b.Bind(10, fun x -> v)))
Bind
の呼び出しが出てきました。
returnの変換
T
の最初の引数(変換対象)がreturn x
という形をしているので、
return
の変換規則を適用します。
let b = simple in b.Bind(10, fun x -> b.Return(x))
Return
の呼び出しが出てきました。
これ以上変換すべき個所はありませんので、手動変換はここまでです。
ビルダーにはBind
もReturn
も適切に定義されているため、コンパイル可能です。
変換を出力する
さて、元の記事ではSimpleBuilder
を弄って変換の様子を出力するようにしていました。
同じ方法だと面白くないので、この記事では別の方法を取ってみることにします。
その前に、Quote
について突っ込んでおきます。
Quote変換
Quote
は元記事でも使っていますが、その定義は
member this.Quote(x) = x
と、あたかも式木がx
として渡されてそれをそのまま返しているように見えます。
しかし、実際はQuote
メソッドは呼び出されることはありません。
ただ単に、Quote
という名前で定義されてさえいればいいのです。
そのため、紛らわしさを無くす意味でもQuote
の実装は、
member __.Quote() = ()
としておいた方がいいと思います。
コンピュテーションビルダーに機能を後付けする
さて、コンピュテーションビルダーに対する機能の追加ですが、方法としては以下のものがあるでしょう。
- コンピュテーションビルダーの書き換え
- 既存のコンピュテーションビルダーを継承して機能を追加
- 型拡張として機能を追加
- 拡張メソッドとして機能を追加
1つ目の方法が元記事のやり方です。 一番シンプルですが、コードがない場合にはこの方法は選べません。
2つ目の方法は、面倒ですしそれほど説明すべき点もないので省略します。
3つ目と4つ目の方法を今回紹介します。
型拡張として機能を追加
コンピュテーション式の変換は、コードの表現しか見ておらず、 実現方法は問題にしていません*1。 そのため、ビルダーに対してメソッドを呼び出せるようになっていれば、 コンピュテーションビルダーにメソッドが定義されている必要はありません。
型拡張でQuote
機能を追加する例を見てみましょう。
open FSharp.Quotations.Evaluator type SimpleBuilder with member __.Quote () = () member __.Run(expr: Quotations.Expr<_>) = // ここにexprを出力するコードを書く // 式木を実行するために、FSharp.Quotations.Evaluatorを使用 expr.Evaluate()
この型拡張を適当なモジュールに入れ、
そのモジュールをopen
するかどうかでexpr
を出力するかどうかを切り替えれるようになります。
しかし、ビルダーにQuote
メソッドを追加してRun
の引数の型をExpr<_>
型にしてしまい、
Run
メソッドの中で引数をそのまま返してしまうと、
コンピュテーション式利用側はExpr<_>
を受け取ることになってしまいます。
これでは、元のコードをコンパイルできなくなってしまいます。
そこで、FSharp.Quotations.Evaluatorというライブラリを使います。
このライブラリは、F#のコードクォートを実行するためのライブラリです。
Run
メソッドの最後でEvaluate
を呼び出し、
Quote
でExpr<_>
にしたものをまた戻します*2。
拡張メソッドとして機能を追加
型拡張ではなく、もちろん拡張メソッドとして定義することもできます。
open System.Runtime.CompilerServices open FSharp.Quotations.Evaluator [<Extension>] type SimpleBuilderExtension private () = [<Extension>] static member Quote (_: SimpleBuilder) = () [<Extension>] static member Run(_: SimpleBuilder, expr: Quotations.Expr<_>) = // ここにexprを出力するコードを書く // 式木を実行するために、FSharp.Quotations.Evaluatorを使用 expr.Evaluate()
少々面倒なので、型拡張でいいですね。 一応、できるということで。
exprを出力するコード
少し改良していますが、基本的には元の記事と同じです。
open Microsoft.FSharp.Quotations.Patterns open System.Collections.Generic module Printer = let mutable private i = 0 let private valueMap = Dictionary<obj, string>() let printKnownVar obj = if obj = box simple then "simple" else "unknown_var" let printValues () = valueMap |> Seq.sortBy (fun kv -> kv.Value.Substring(2) |> int) |> Seq.map (fun kv -> sprintf "let %s: %s = %s" kv.Value (kv.Key.GetType().Name) (printKnownVar kv.Key)) let rec print (expr: Quotations.Expr) = match expr with | Call (Some receiver, m, args) -> (* インスタンスメソッド呼び出しを文字列化 *) let receiver = print receiver let args = List.map print args |> String.concat ", " (* reduceじゃなくてconcatでOK *) sprintf "%s.%s(%s)" receiver m.Name args | Value (obj, typ) -> (* 値を文字列化 *) if typ = typeof<SimpleBuilder> then (* 値が辞書にあったらそれを変数として扱うように変更 *) match valueMap.TryGetValue(obj) with | true, name -> name | false, _ -> let name = sprintf "$b%d" i i <- i + 1 valueMap.Add(obj, name) name else string obj | Lambda (arg, Let(var, value, body)) when arg.Name = print value -> (* 単純なラムダ式を文字列化 *) (* Expr<_>の表現力の問題によって、Lambdaではラムダ式すべてを表現できない。 例えば、ラムダ式の引数部分でパターンマッチできるが、 Expr<_>のLambdaはargがVarなので、Letと組み合わせることでこれを再現している模様。 *) let arg = var.Name let body = print body sprintf "(fun %s -> %s)" arg body | Var v -> (* 変数を文字列化 *) v.Name | _ -> (* 対応できない式木はエラーにするように変更 *) failwithf "%A is not supported." expr type SimpleBuilder with member __.Quote () = () member __.Run(expr: Quotations.Expr<_>) = let exprStr = Printer.print expr let values = Printer.printValues () values |> Seq.iter (printfn "%s") printfn "%s" exprStr expr.Evaluate()
改良した点としては、
- 変数を出力するようにした
- 実装を洗練
Run
の結果を変えないように、式木を実行するようにした
というところです。
実行してみる
拡張を施したモジュールをopen
すると、既存のコンピュテーション式の挙動が変わり、
Printer.print
の結果が出力されるようになります。
let res = simple { let! x = 10 return x }
このコードを実行してみると、このような出力が得られます。
let $b0: SimpleBuilder = simple $b0.Bind(10, (fun x -> $b0.Return(x)))
結果比較
手動変換した結果はこうでした。
let b = simple in b.Bind(10, fun x -> b.Return(x))
そして、Quote
によってExpr<_>
にしたものを文字列化したのがこれです。
let $b0: SimpleBuilder = simple $b0.Bind(10, (fun x -> $b0.Return(x)))
完全に一致!!!
まとめ
コンピュテーション式がどんな風に変換されるのか見るために、
Quote
で式木化してF#コードに文字列化するのはいいアイディアでした。
この記事では、以下のことをやりました。
Bind
を必要な例に変更- 既存のコンピュテーションビルダーに機能を追加する方法
- 出力されるコードに何も手を加えなくても手動変換の結果とほとんど同じになるような
Expr<_>
の出力
Expr<_>
の限界*3はありますが、
これを推し進めれば、かなり良い感じに色々なコンピュテーション式の変換結果を出力できるようになりそうです。
今年もF# Advent Calendarは豊作ですね!素晴らしいです。
クラスのコンストラクタ呼び出し時にプロパティも同時に初期化する
これはF# Advent Calendar 2014の9日目の記事(ピンチヒッター)です。 昨日の記事はねのさんによるFSharperならこう書く(A Poker Game Program) #FsAdventでした。 この記事を勝手に添削したエントリも、よろしければ合わせてどうぞ。 何度も言いますが、コードが読みやすくて書き換えが大変楽でした。
さて、今日はF# のネタリストから、クラスのコンストラクタ呼び出し時にプロパティも同時に初期化する方法です。 Advent Calendarのネタに困っている人は、参考にしてみるといいかもしれません。
F#のプロパティ
F#のプロパティの定義は、複数の書き方があります。 C#の自動実装プロパティに初期値を持たせた書き方もサポートしていますので、今回はそれを使いましょう。
type Class(x: int) = member val Name = "" with get, set
これで、C#での
public class Class { readonly int x; public string Name { get; set; } public Class(int x) { this.x = x; this.Name = ""; } }
に大体対応します。ヤバい短さですね。
コンストラクタ呼び出し時にプロパティも同時に初期化したい
上のように作ったクラスを生成する際に、C#ではオブジェクト初期化子という構文を使って、プロパティも同時に設定できます。
var c = new Class(10) { Name = "hoge" };
便利ですね! 実は(?)、これと同じことがF#でもできます。
let c = Class(10, Name="hoge")
これだけです!
これは、F#での名前付き引数とまったく同じ構文ですね。 なので、当然(?)これらは混ぜることができます。
let c1 = Class(x=10, Name="hoge") let c2 = Class(Name="hoge", x=10)
素晴らしい!
本日は以上です。 明日は、ざっきーさんの番ですね!
Re: FSharperならこう書く(A Poker Game Program) #FsAdvent
FSharperならこう書く(A Poker Game Program) #FsAdventというエントリがF# Advent Calendar 2014の8日目の記事として公開されました。 この記事自体は、主に設計よりな話をしていますが、ここでは実装寄りの話を中心に、 勝手に添削してみました。 また、記事の中に書かれていた疑問にも回答したりしています。
添削その1(全体)
型とモジュールの名前
元のコードでは、モジュールの中にT
という型を入れるスタイルのようです。
(* 柄 *) module Suit = (* 柄 *) type T = | Spade | Heart | Diamond | Club (* 全ての柄 *) let values = [Spade; Heart; Diamond; Club]
自分も昔はこんな感じにしていたのですが、現在のところこのスタイルはやめ、モジュール名と型名に同じ名前を付けるスタイルを採用しています。
type Suit = Spade | Heart | Diamond | Club [<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>] module Suit = let values = [Spade; Heart; Diamond; Club]
なんでかというと、
あたりが大きな理由です。
CompilationRepresentation
とかCompilationRepresentationFlags.ModuleSuffix
とか面倒ですが、覚えてしまえばどうということはないですよ。
回答その1(Card)
レコード生成の制限
こんな疑問が書かれていました。
番号は、1から13に含まれない値は生成できないようにしたいので、classになりそうです。 classにするとinterfaceの実装が煩わしいところですが、constructorを制限しようと思うと、レコードや判別共用体では厳しそうです。 もし他に良い方法をご存知の方がいらっしゃったら教えてください。
これに対する回答としては、2通りあります。
- シグネチャファイルで型の実装を隠蔽する
private
で型の実装を隠蔽する
前者はfsiファイルを書かなければならず、面倒というのと、型の実装を隠蔽してしまうとcomparison
制約とかも自分でやる必要が出てきてそれも面倒なので、今回はお手軽にprivate
で隠蔽する方法を紹介します。
元のコードはこんな感じです。
(* 番号 *) module Number = type T(value:int)= do (* 数値範囲チェック *) if not (min <= value && value <= max) then failwith "The Number is required 1 to 13."
これを、型名を変更しつつレコードで実現してみましょう。
type Number = private { Value: int } [<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>] module Number = let min = 1 let max = 13 let value x = x.Value let create x = (* 数値範囲チェックをこちらに書く *) if not (min <= x && x <= max) then failwith "The Number is required 1 to 13." { Value = x }
こうすることによって、それを含むモジュール(ここではファイルと思ってください)の外からは{ Value = 42 }
のようには書けなくなりますし、当然let f (x: Number) = x.Value
というコードもコンパイルエラーになります。
パターンマッチも(それを含むモジュールの外では)できなくなるので注意してくださいね。
IComparable
レコードにしたため、IComparable
等の実装が不要になりましたが、元のバージョンではこれらを実装していました。
さて、これらを実装しているのはいいのですが、実は大小比較だけしたいだけであれば実装するインターフェイスはかなり減らせます。
type T(value: int) = (* snip *) member this.Value = value interface System.IComparable with member this.CompareTo(other) = match other with | :? T as x -> compare value x.Value | _ -> failwith "other is not T" (* snip *)
これだけで、警告は出るものの=
も<
も他の比較演算子も使えるクラスになります。
このあたりは、言語仕様(PDF)の8.15.3を参照してください。
ざっくり説明すると、System.IComparable
を明示的に実装している場合、Equals
メソッドは
override x.Equals(y : obj) = ((x :> System.IComparable).CompareTo(y) = 0)
という実装が生成されるという感じです。
CompareTo
はジェネリック版ではなく非ジェネリック版が呼び出されるので、System.IComparable
だけ実装すればよい、ということですね。
また、compare
関数を使えばCompareTo
メソッドは簡単に実装できます。
ただし、これに頼る場合は警告が出るみたいです。言語仕様にはその記述はなかったように思いますが・・・
添削その2(Deck)
単純なfor式
山札を表すDeck
モジュールのdeck_without_joker
関数ですが、一番内側のfor
式はyield
しか含みません。
(* デッキ(ジョーカー抜き) *) let deck_without_joker = List.ofSeq(seq{ (* シーケンス式の二重ループでNumberとSuitの組み合わせを列挙する *) for num in Card.Number.min .. Card.Number.max do for suit in Card.Suit.values do yield { Card.Number = Card.Number.create num; Card.Suit = suit } })
このような場合、do yield
の代わりに->
が使えます。
名前もlikeThis
形式に変えておきましょう。
let deckWithoutJoker = List.ofSeq (seq { for num in Card.Number.min .. Card.Number.max do for suit in Card.Suit.values -> { Card.Number = Card.Number.create num; Card.Suit = suit } })
添削その3(List)
take関数の名前
既存のList
モジュールに足りない処理を追加するために、独自のList
モジュールを作っています。
その中で、take
という関数を追加しているんですが、これは標準のtake
を隠してしまううえ、やっていることもtake
ではない気がします。
やっていることは、count
でのリストの分割ですので、splitAt
がいいでしょう。
名前は、Hoogleで探しました。
また、option
を返す関数はF#ではtry
プレフィックスを付けるのが慣習ですので、trySplitAt
としてみました。
let trySplitAt count list = let rec loop i acc tail = if i = count then Some ((List.rev acc), tail) else match tail with | [] -> None | h::t -> loop (i + 1) (h::acc) t loop 0 [] list
tail |> function
としている部分は、好みもありますが、個人的にはmatch tail with
の方が好きなのでその部分も変更してあります。
また、Deck.pick
もoption
を返すので、Deck.tryPick
にしました。
回答その2(Hand)
単一ラベルの判別共用体
こんな疑問がありました。
単一ラベルの判別共用体ってどうなのでしょうか。
これは、ありだと思います。 FsControlを使うと、単一ラベルの判別共用体を多用するようになります(そうじゃない)。
添削その4(Hand)
System.String.Join
.NETの便利関数にSystem.String.Join
というのがあるのですが、
これはBasis.CoreのStr.joinを使いましょう。
嘘です。String
モジュールのconcat
関数を使いましょう。
ToString
ToString
の呼び出しは面倒なので、string
関数を使いましょう。
コードは2つ下を見てください。
List.toArray呼び出し
String.concat
は'T seq
を受け取り、'T list
は'T seq
なのでtoArray
する必要はありません。
コードは1つ下を見てください。
逆向きパイプライン演算子
ToString
の実装で、逆向きのパイプライン演算子を使っています。
(* CUIの選択肢表示用ToString *) member this.ToString indexes = this |> function | Hand xs -> xs |> List.map2 (fun x y -> sprintf "[%s]:%s" y (x.ToString()) ) <| indexes (* 逆向きパイプライン演算子<|はロマン *) |> List.toArray |> curry System.String.Join "/"
ここは、||>
の出番です。
member this.ToString indexes = match this with | Hand xs -> (xs, indexes) ||> List.map2 (fun x y -> sprintf "[%s]:%s" y (string x)) |> String.concat "/"
あ、curry
関数は使わなくなったので削除しました。
引数の位置でのパターンマッチ
createBy
関数やchange
関数で、レコードや判別共用体のパターンマッチをしている部分が何か所かあります。
(* 山札から最初のcount枚を取得して手札を作り、残りの山札とペアにして返す。 山札の枚数が足りない場合はNone *) let createBy deck = Deck.pick count deck |> Option.map (fun pickResult -> (* let束縛のレコードパターン *) let { Deck.Picked = picked; Deck.Deck = d } = pickResult ((Hand picked), d) ) (* 手札のうちchangeCardsで指定されたカードを捨て、山札から同じ枚数を加え、残りの山札とペアにして返す。 *) let change hand deck changeCards = Deck.pick (Set.count changeCards) deck |> Option.map (fun pickResult -> let { Deck.Picked = picked; Deck.Deck = d } = pickResult let hand = hand |> function | Hand xs -> xs |> List.filter (changeCards.Contains>>not) Hand (List.append picked hand), d )
これらは、引数の位置でパターンマッチすればいいので、こう書き直せます。
let tryCreateBy deck = Deck.tryPick count deck |> Option.map (fun { Deck.Picked = picked; Deck.Deck = d } -> ((Hand picked), d)) (* ↑ラムダ式の引数の位置でレコードを分解 *) (* ↓関数の引数の位置で判別共用体を分解 *) let change (Hand xs) deck changeCards = Deck.tryPick (Set.count changeCards) deck |> Option.map (fun { Deck.Picked = picked; Deck.Deck = d } -> (* ↑ラムダ式の引数の位置でレコードを分解 *) let hand = xs |> List.filter (changeCards.Contains >> not) Hand (List.append picked hand), d )
添削その5(Rank of Hands)
役の強弱
F#の話ではないですが、役の強弱がおかしい気がしますね。 一般的なルールに従って、修正します。
type Rank = | OnePair | TwoPair | ThreeCards | Straight | Flush | FullHouse | FourCards | StraightFlush | RoyalStraightFlush
型の別名
役判定関数モジュールで、Card.NormalCard array
という型名を大量に使っています。
こういう場合、型略称を定義してしまうと楽でしょう。
type private Cards = Card.NormalCard array
pairwise
straight
やflush
の判定に、前のカードと今のカードをペアにしたシーケンスを得るために、
xs |> Seq.ofArray |> (fun xs -> Seq.zip xs (Seq.skip 1 xs)) |> Seq.forall (fun t -> (fst t).Number ++ 1 = (snd t).Number)
というコードが出てきます。
これは、Seq.pairwise
というそのままな関数があるので置き換えてしまいましょう。
cards |> Seq.pairwise |> Seq.forall (fun t -> (fst t).Number ++ 1 = (snd t).Number)
タプルを引数の位置で分解する
上のコードはラムダ式の中でfst
とsnd
を使っているので、これを引数の位置でのパターンマッチにしてしまいましょう。
cards |> Seq.pairwise |> Seq.forall (fun (x, y) -> x.Number ++ 1 = y.Number)
配列 vs リスト
型略称を用意したCards
は現在、配列です。
リストではなく配列にしたのは、
引数の型を配列にしたのは、インデックスでのアクセスが多そうだったから、という程度の理由です。
とありました。 しかし、リストでもインデックスでアクセスできるうえ、引数として渡される配列もしくはリストの要素数は、5です。 この部分がクリティカルに効いてくるとは思えないので、リストに変更しました。
type private Cards = Card.NormalCard list
もし問題が起こったら、この型略称の部分を戻して、呼び出し部分の型を変えればいいでしょう。
SequenceEqual
royalStraightFlush
の判定で、System.Linq.Enumerable.SequenceEqual
を使って判定していますが、
List
をそのまま扱えば=
で比較できるので、変更してしまいましょう。
let royalStraightFlush (cards: Cards) = (straightFlush cards) && ( let royal = [10..13]@[1] (cards |> List.map (fun x -> x.Number |> Card.Number.value)) = royal )
List.filter Option.isSomeしてList.map Option.get
Evaluator.evaluate
等の関数で、List.filter Option.isSome
してからList.map Option.get
している箇所があります。
evals |> List.map (fun f -> f x) |> List.filter Option.isSome |> List.map Option.get
こういった処理には、List.choose
という関数が使えます。
evals |> List.map (fun f -> f x) |> List.choose id
回答その3(Rank of Hands)
判別共用体の大小
判別共用体の大小に関して、
大小比較が定義順に依存するのはちょっとしたポイントです。ソースは忘れました。
とありました。 言語仕様を参照してみると、8.15.4に、
If
T
is a union type, invokeMicrosoft.FSharp.Core.Operators.compare
first on the index of the union cases for the two values, and then on each corresponding field pair ofx
andy
for the data carried by the union case. Return the first non-zero result.
とあります。
- 2つの値の共用体ケースのインデックスが
Microsoft.FSharp.Core.Operators.compare
に渡される - 付随するデータがある場合、それらが
Microsoft.FSharp.Core.Operators.compare
に渡される 0
ではない最初の値が返される
ということのようですね。 共用体ケースのインデックスは、定義順に振られる*1ため、大小比較が定義順に依存するのです。
添削その6(メイン処理)
リストの生成
input_choices
の生成に、リストリテラルを使っています。
let input_choices = ["a"; "b"; "c"; "d"; "e"]
好みはわかれるかもしれませんが、ここは
let inputChoices = ['a'..'e'] |> List.map string
のように、文字のリストを..
で作ってから文字列化したほうが楽な気がします。
まとめ
テストとかしてない*2ので動くかどうかわからないですが、コード全体はGistに上げました。
元のコードがとても読みやすく素晴らしかったです。 引き続き、F# Advent Calendarをお楽しみください!
NGK2014Bで発表してきた
NGK2014Bで、FCellという製品紹介の発表をしてきました。 詳細は、発表資料よりもその中で紹介しているデモを参考にしてください*1。
FCellと類似した(?)ものとしては以下のようなものがあります。
COMは置いといて、そのほかのものとFCellがどう違うかということを軽く説明しますね。
NPOIやEPPlusとの違い
NPOIやEPPlusは、基本的に「Excelを外から操る」ためのライブラリです。 こいつらはこいつらで便利なんですが、「Excelを中から操る」ことはできないので、こいつらで置き換え可能なVBAというのはほんの一部なんですね。
それに対してFCellは、ユーザ定義関数(UDF)をF#*2で定義できるのです。 そして定義したUDFは当然のようにExcel内で使えます。
Excel DNA
Excel DNAも、F#*3でUDFを定義できます。 しかし、Excel DNAは「Excelの外部」で動くものであり、Excelファイルにコードを埋め込めないです。
それに対してFCellは、Excelファイルにコードを埋め込めます。 埋め込むという選択肢のみであればVBAと変わらないのですが、埋め込まずに外に出す、ということもできます。
VSTO
VSTOは、UDFを定義する程度のものにはフォーカスしておらず、アドイン開発に特化したものという認識です。 そのため、小回りが利かないという欠点を持ちます。 また、Excelファイルにコードを埋め込むのもできないようです。
それに対してFCellは、UDFを定義する部分から、UIをカスタマイズする部分までをターゲットにしているため、非常に小回りが利きます。 F#を利用する場合であれば、Excelに統合されたF#エディタが使える*4ため、VBAと同じ感覚で使えるのが大きな利点です。
この辺りは、棲み分けの話だと思うのですが、VSTOは中規模から大規模なものに、FCellは小規模から中規模なものに使うのがいいのでは、と思います。
VSTOには詳しくないので間違ったことを言っているかもしれないです。間違っていたら指摘ください。
FCellに足りない部分
VBAの最大のメリットは、ユーザの操作を記録してそれをマクロとして使いまわしたり、生成されたコードをベースに開発したりできる点にあると思っています。 FCellは現状それができませんので、Excel利用者のものというよりはやはり開発者のためのものになっています。 今後、これが改善されてExcel利用者も気軽にFCellが使えるようになれば、と願っています。 お金の話は置いといて。
実例に見るSource変換活用術
これはF# Advent Calendar 2014の5日目の記事です。 昨日の記事はyukitosさんによるF# Project Scaffoldを使ってプロジェクトを作成するでした。 F# Project Scaffold、便利そうですね。 Chocolateyみたいに導入が簡単だと、もっといい感じになるような気がします。
さて、コンピュテーション式おじさんです、こんにちは。 最近はPersimmonというF#用のテスティングフレームワークを作っています。 Persimmon自体はまだベータ版ですが、今回はこのプロジェクトで得た知見の一つを紹介したいと思います。 誰得エントリですね。
続行可能なアサーション
Persimmonでのアサーションは、続行可能なアサーションと続行不可能なアサーションに分類されます。 例えば、
test "sample test" { do! assertEquals x y do! assertEquals a b }
とあった時、最初のアサーションに通らなくても次のアサーションは実行できるとうれしいです。
それに対して、
let originalAssert x y = test "" { do! assertEquals x y return x } test "sample test" { let! b = originalAssert x y do! assertEquals a b }
とあった時、二番目のアサーションは一番目のアサーションの結果を使っていますから、次のアサーションは実行しようがありません。
これを実現するためにはどうすればいいでしょうか?
Bindのオーバーロード?
最初に考えたのは、Bind
をオーバーロードしてしまうというものでした。
その時に書いたのが以下のコードです。
アイディアとしては、Bind
の第二引数として渡される関数(後続の処理)が必要としている型がunit
かそれ以外かでBind
を分けてしまう、というものです。
しかしこれは、コメントにもあるようにコンパイルエラーになってしまいます。
Sourceメソッドのオーバーロードによる疑似的なBindのオーバーロードの実現
let!
(やdo!
)は、コンパイラによってBind
メソッドに変換されますが、
ビルダーにSource
メソッドが定義されている場合、第一引数がSource
に渡されます。
Sourceがある場合のlet!の変換
T(let! p = e in ce, C) = T(ce, fun v -> C(b.Bind(b.Source(e), fun p -> v)))
Source
をオーバーロードし、中で同じ型にしてしまえばBind
のオーバーロードを間接的に実現できそうです。
やってみた
以下、単純化した例です。
type BindingValue<'T> = | UnitValue of 'T (* always unit *) | NonUnitValue of 'T type SomeBuilder() = member __.Source(x: unit) = UnitValue x member __.Source(x: _) = NonUnitValue x member __.Bind(x: BindingValue<'T>, f) = match x with | UnitValue x -> (* unitの場合の処理を書く Persimmonの場合、アサーションに通らなくても以降の処理を続行するようなコードになっている アサーションの結果は、BindingValueが直接'Tを保持するのではなく、 AssertionResult<'T>を保持することで持ちまわすようにしている *) assert (typeof<'T> = typeof<unit>) (* ここで直接()を渡してしまうと、fがunit -> 'Uのように推論されてしまうので、 Unchecked.defaultof<'T>を使うことで回避 *) f Unchecked.defaultof<'T> | NonUnitValue x -> (* unit以外の場合の処理を書く Persimmonの場合、アサーションに通らない場合は以降の処理を続行せずに結果を返し、 アサーションを通っていた場合は後続の処理を続行するようなコードになっている *) f x
このように、Source
メソッドの引数で分岐し、戻り値をBindingValue
という同じ型にするようなオーバーロードを用意することで、
Bind
メソッドの中で処理を分岐できるようになります。
ちなみにreturn!
を提供する場合、ReturnFrom
でもSource
メソッドが呼び出されるため、ReturnFrom
メソッドの中でBindingValue
を剥いでやる必要があります。
このように、Source
メソッドを使うとBind
のオーバーロードが疑似的に実現できます。
Persimmonのほかのコンピュテーション式
Persimmonでは、他にもパラメタライズテスト用のコンピュテーション式と、例外のテスト用のコンピュテーション式を用意しています。 パラメタライズドテスト用のコンピュテーション式は、シンプルなカスタムオペレーションの実装例となっています。 例外のテスト用のコンピュテーション式は、シンプルな割に便利なものになっています。 興味のある人は見てみるといいでしょう。
みなさんも、PersimmonやBasis.Coreのコンピュテーション式を参考に、 色々なコンピュテーション式を作ってみましょう!
参考URL
- persimmon-projects/Persimmon: Persimmonのソースコード
- Persimmonの特徴: Persimmonの特徴について
- 詳説コンピュテーション式 - ぐるぐる~: コンピュテーション式の解説
- コンピュテーション式のSourceメソッドを試す - pocketberserkerの爆走: Sourceメソッドについてのエントリ
- F# でのテスト用 DSL について考える - a wandering wolf: Persimmon誕生までの経緯
- BasisLib/Basis.Core: 自称「唯一きちんとreturnするコンピュテーション式を持ったライブラリ」
明日のF# Advent Calendar
日本語版はkos59125さん、英語版はSteve Shogrenさんです。 Steve Shogrenさんは「F# Polymorphism」とありますね。楽しみです!
なごやかJavaで発表してきた
なごやかJava第一回で、「.NET系開発者から見たJava」というタイトルで発表してきました。 Javaのこの機能って.NET開発者から見てどうなの?というような内容です。
大阪から参加してくれた方の感想を載せておきます。
おかしい、終わった後の感想が「F# すごい!」だ…… #ngojava
— irof@Javarista (@irof) 2014, 11月 30
F# 4.0プレビュー版公開!
拡張機能が使えるVisual Studioが条件付きで無償化されたことによって、 快適な開発環境でF#を学べるようになることでしょう。
それはそれで楽しみなことは間違いないのですが、今回はF#自体の使い勝手の向上に目を向けたいと思います。
昨日はVS Communityが無償提供されたほか、様々な発表がありましたが、F#も4.0のプレビュー版が公開されました。
新機能一覧
プレビュー版までで実装されたものは以下の通りです。
- 言語に関するもの
- ライブラリに関するもの
- VSツールの改善
- プロジェクトテンプレートへの
AssemblyInfo.fs
の追加 - F# Interactiveの起動の高速化
- F# Interactiveのショートカットを追加(セッションのリセット、すべてクリア)
- プロジェクトテンプレートへの
あっと驚く新機能はないものの、全体的に使い勝手を向上させることを目的にしているように思います。 このほかの実装予定の機能については、Status of F#4.0+ Approved Language/Library Itemsを参考にしてください。
今回は、F#4.0のプレビュー版までで実装されたもの中から、コレクションモジュールの正規化を取り上げたいと思います。
コレクションモジュールの正規化
F#はList
/Array
/Seq
という3つのコレクションモジュールを持っていましたが、
「こっちのモジュールでは提供されているけどこっちのモジュールでは提供されていない」という関数が多くありました。
F#4.0では、この状況が大幅に改善されます。 どのように改善されるのか見ていきましょう。
Seq
にしかなかった関数のList
/ Array
への追加
compareWith
第一引数として渡された関数をもとに、コレクションの大小を求める関数です。
(* val List.fで、Listモジュールのf関数と思ってください *) val List.compareWith: ('a -> 'a -> int) -> 'a list -> 'a list -> int val Array.compareWith: ('a -> 'a -> int) -> 'a [] -> 'a [] -> int
groupBy
要素を変換した結果と、その変換のもとになったコレクションのペアのコレクションにして返す関数です。
val List.groupBy: ('a -> 'b) -> 'a list -> ('b * 'a list) list when 'b : equality val Array.groupBy: ('a -> 'b) -> 'a [] -> ('b * 'a []) [] when 'b : equality
countBy
要素を変換した結果と、その個数のペアのコレクションにして返す関数です。 変換結果でグループ化して個数も求めるような感じですね。
val List.countBy: ('a -> 'b) -> 'a list -> ('b * int) list when 'b : equality val Array.countBy: ('a -> 'b) -> 'a [] -> ('b * int) [] when 'b : equality
distinct
/ distinctBy
distinct
は要素の重複を取り除く関数で、
distinctBy
は重複の基準を関数として渡せるdistinct
です。
val List.distinct: 'a list -> 'a list when 'a : equality val List.distinctBy: ('a -> 'b) -> 'a list -> 'a list when 'b : equality val Array.distinct: 'a[] -> 'a [] when 'a : equality val Array.distinctBy: ('a -> 'b) -> 'a [] -> 'a [] when 'b : equality
exactlyOnce
コレクション中に要素が一つしかなかった場合に、それを取り出す関数です。 複数の要素があった場合は例外を投げます。
val List.exactlyOnce: 'a list -> 'a val Array.exactlyOnce: 'a [] -> 'a
last
コレクションの最後の要素を返す関数です。
val List.last: 'a list -> 'a val Array.last: 'a [] -> 'a
pairwise
/ windowed
windowed
は、スライド式のウィンドウを生成する関数です。
pairwise
はこの特別なバージョンで、ウィンドウのサイズが2に固定化され、ウィンドウは配列ではなくタプルとしてあらわされます。
val List.pairwise: 'a list -> ('a * 'a) list val List.windowed: int -> 'a list -> 'a [] list val Array.pairwise: 'a[] -> ('a * 'a) [] val Array.windowed: int -> 'a[] -> 'a [] []
singleton
渡された要素のみを含むコレクションを生成する関数です。
val List.singleton: 'a -> 'a list val Array.singleton: 'a -> 'a []
skip
/ skipWhile
/ take
/ takeWhile
/ truncate
なぜなかった感のある関数群ですね。説明は不要でしょう。
unfold
種となる値からコレクションを生成する関数です。 これも、なぜなかったのか・・・
where
filter
の別名です。これ、むしろいらんやろ・・・
List
にしかなかった関数のArray
/ Seq
への追加
tail
先頭要素を除くコレクションを返す関数です。
map3
3つのリストに対するmap
関数です。
val Array.map3: ('a -> 'b -> 'c -> 'd) -> 'a [] -> 'b [] -> 'c [] -> 'd [] val Seq.map3: ('a -> 'b -> 'c -> 'd) -> 'a seq -> 'b seq -> 'c seq -> 'd seq
replicate
指定した個数の指定した要素を含むコレクションを返す関数です。
val Array.replicate: int -> 'a -> 'a [] val Seq.replicate: int -> 'a -> 'a seq
List
とArray
にあった関数のSeq
への追加
fold2
/ foldBack
/ foldBack2
/ reduceBack
/ scanBack
fold
系の派生関数群ですね。
iteri2
/ mapi2
インデックスを伴ったiter2
とmap2
です。
今回、map3
は追加されますが、List
にもともとmapi3
がなかったためか、mapi3
は追加されないようです。
permute
インデックスの変換関数を元に要素を置換したコレクションを返します。
val Seq.permute: (int -> int) -> 'a seq -> 'a seq
rev
シーケンスを逆順にしたシーケンスを返す関数です。
sortWith
比較関数を指定してコレクションをソートする関数です。
val Seq.sortWith: ('a -> 'a -> int) -> 'a seq -> 'a seq
List
とSeq
にあった関数のArray
への追加
head
コレクションの先頭要素を返す関数です。
今までなかった関数のList
とArray
への追加
splitAt
指定されたインデックスでコレクションを分割します。
val List.splitAt: int -> 'a list -> 'a list * 'a list val Array.splitAt: int -> 'a [] -> 'a [] * 'a []
今までなかった関数のList
とArray
とSeq
への追加
contains
今まで、
xs |> List.exists ((=)x)
などとしていましたが、今後はcontains
が使えます。
findBack
/ findIndexBack
/ tryFindBack
/ tryFindIndexBack
find
系の関数に、後ろから探すものが追加されました。
tryHead
/ tryLast
失敗するかもしれない関数にoption
を返すバージョンが追加されました。
tryTail
が無いのは・・・?
indexed
コレクションの要素にインデックスを付けたコレクションにして返す関数です。
val List.indexed: 'a list -> (int * 'a) list val Array.indexed: 'a [] -> (int * 'a) [] val Seq.indexed: 'a seq -> (int * 'a) seq
item
/ tryItem
今まで、n番目の要素を取り出す関数としてnth
がありました。
しかし、これはArray
にはなく、またList
とSeq
で引数の順番が違うというとてもアレな関数でした。
val List.nth: 'a list -> int -> 'a val Seq.nth: int -> 'a seq -> 'a
F#4.0では、nth
は非推奨となり、引数の順番が統一されたitem
が提供されます。
val List.item: int -> 'a list -> 'a val Array.item: int -> 'a [] -> 'a val Seq.item: int -> 'a seq -> 'a
option
を返すバージョンであるtryItem
も追加されました。
val List.tryItem: int -> 'a list -> 'a option val Array.tryItem: int -> 'a [] -> 'a option val Seq.tryItem: int -> 'a seq -> 'a option
これは朗報ですね!
mapFold
/ mapFoldBack
fold
は種の型が戻り値の型になりましたが、mapFold
は第一引数として渡す関数の結果として次の種だけではなく追加の値も返せるようにしたことで、
map
とfold
の処理を同時に行えるようになりました。
結果として返されるタプルの一つ目にmap
の結果が、二つ目にfold
の結果が入ってきます。
val List.mapFold: ('b -> 'a -> 'c * 'b) -> 'b -> 'a list -> 'c list * 'b val Array.mapFold: ('b -> 'a -> 'c * 'b) -> 'b -> 'a [] -> 'c [] * 'b val Seq.mapFold: ('b -> 'a -> 'c * 'b) -> 'b -> 'a seq -> 'c seq * 'b
mapFoldBack
はfoldBack
とmap
を同時にするバージョンです。
sortByDescending
/ sortDescending
逆順のソートをする関数です。 これないのつらかったんですが、ようやく入ってくれました。