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が使えるようになれば、と願っています。 お金の話は置いといて。

*1:ちなみに、当日はでも動画を流したのではなく、自分で作ったデモを実際に動かしました

*2:2.0からは、C#VBといった言語もサポートするようになりました

*3:もちろんC#VBも使えます

*4:補完機能やエラー表示機能も持っています

なごやまつりでF# Type Providerについて(?)話してきた

してきました。 あれだけの人数が集まって、F# 知らない人が全然いないとかすごい勉強会でしたね。

Excel方眼紙、どうにかしたいものですね。 今回作った(作りかけ)コードは、GitHubに置いてあります。

ExcelHouganshi.TypeProvider

現実と戦うための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ハッシュにかけ、ファイル名をそれにすることで存在チェックできるようにしました。

最後のはまぁ、えっと、その。

Excel-DNA を使うメリット

VBA を使うことに対して、Excel-DNA を使うことのメリットは大体こんな感じです。

  • テストが容易
  • コードの管理ができる
  • 抽象度の高いコードが書ける
  • 保守しやすい

一つずつ見ていきましょう。

テストが容易

VBA にも一応、VBAUnit などのテスティングフレームワークはあります*1
しかし、VBA の言語自体の貧弱さもあって、あまり使いやすいものはありません。
さらに、ライブラリのような形で提供されるため、ツール連携などは絶望的です。


対して、Excel-DNA では通常の dll を作るだけですので、普通にテストを書くことができます。
先ほどの組み合わせの例にも、NaturalSpec というテスティングフレームワークを使ってテストを書いています。

コードの管理ができる

VBA でも頑張ればできなくはないですが、Excel-DNA では何も頑張る必要はなく、ごく普通にコードを管理することができます。
github に公開するのも簡単ですし、コードの検索性にも優れます。


また、エディタに Visual Studio が使えるのもうれしい点です。
無限ループに陥ってもコードを失わずに済みますし、Visual Studio に搭載されたさまざまな機能が使えます*2

抽象度の高いコードが書ける

VBA では、オブジェクト型かどうかで代入を書き分ける必要があります。
高階関数も (たぶん) 使えませんし、標準では配列以外のコレクションは用意されていません。


それに対して Excel-DNA では、.NET Framework のライブラリや言語固有のライブラリが普通に使えますので、より高レベルな部分に思考を集中させることができます。

保守しやすい

VBA は小さいものでも保守が大変・・・そう思っていた時期が、俺にもありました。

先ほどの例を VBA で書いてみたものです。
なんかもう色々とひどいですよね。


ですが、ちゃんとした VBA プログラマが書くとこの通り・・・

いげ太さんマジ半端ないです。
ただ、

とのことなので、やはり大きいものになると、VBA だとうまく管理しないと破綻しそうです。


このように、開発面におけるメリットは大きいので、Excel-DNA が使える場面ではどんどん使っていきましょう!

*1:VBAUnit は既に保守されていないようです

*2:VsVim が無いとか考えられない・・・

Excel-DNA で XLL をつくる (その17) を F# でやってみた

今自分の中で Excel-DNA がとてもアツいです。
で、Excel-DNA のことを非常にわかりやすくまとめている supermab さんという方がいるのですが、今日はその方のエントリを F# でやってみた、という話です。
Excel-DNA については supermab さんのブログの Excel-DNA タグ をどうぞ。

ところで、F# のような関数型言語は、並列化処理に向いているようですが今回のような組合せ探索の問題はどのように記述出来るのでしょう?Excel-DNA と F# の組合せは、とても興味のあるところです♪

Excel-DNA で XLL をつくる(その17)

あれ?なんか呼ばれてる?ということでやってみました。
ただし並列処理は今回やってないです。お手軽にやるなら Array.Parallel モジュールの関数群を使う感じでしょうか。
今回は、より簡潔に書けるよー、という部分を強調した感じに書いてみました*1


完成品は github に上げてあります。
https://github.com/bleis-tift/DNA/tree/supermab17


コメント入れても 30 行以下、コメント抜くと 20 行以下です。短い!
元の C# での実装に比べると、解を二次元配列として返すメソッドが無いですが、それを実装したとしても数行です*2

module Combination

let combIf cond xss =
  // 補助用関数
  //   要素があるときは、先頭要素をばらして再帰呼び出し
  //   要素が無いときは、
  //     計算データ(tmp)が条件を満たせば反転してリストに包む
  //     満たさなければ空のリストを返す
  let rec combIf' cond tmp = function
  | xs::xss -> xs |> List.collect (fun x -> combIf' cond (x::tmp) xss)
  | [] when cond tmp -> [tmp |> List.rev]
  | [] -> []
  
  // 組み合わせ探索
  xss |> combIf' cond []

open ExcelDna.Integration // あ、これ結局使ってない

let AnsCount(range: obj[,], trg: int) =
  // 二次元配列をint list listに変換
  let range =
    [0..range.GetLength(1) - 1]
    |> List.map (fun i -> [ for x in range.[*, i..i] -> x :?> float |> int ])

  // 組み合わせ探索
  range |> combIf (List.sum >> ((=)trg))
        |> List.length

二次元配列をリストのリストに変換している部分ですが、このあたり F# は短くかけてとてもいい感じです。
ちなみに、range.GetLength の引数を 0 にして、range のインデクサの * と i..i を逆にすると、行と列が入れ替わったリストになります。


型も、AnsCount の引数と、obj のキャスト部分の計 4 回しか書いていません*3
これだけで、他のすべての部分は型推論によって適切な型が付けられます。動的型付けじゃないですよ。


combIf の呼び出し部分では、関数を合成しています。関数の合成が手軽に書けるのは、関数型言語である F# ならではです。
C#VB だと、こうはいきません。


補助関数である combIf' は、関数内関数となっています。
combIf' を使うのは combIf だけなので、combIf 以外からはアクセスできないように関数内関数として定義しています。
識別子名として、「'」を含むことができていることにも注目してください。
これは途中に含まれてもいいため、「it's」なども識別子として使えます。


combIf' で使っているパターンマッチも強力です。
switch 文を強力にしたもの、と説明されがちなパターンマッチですが、xs::xss のようにデータ構造を分解し、変数に格納するといったことも簡単に行えます。
インデックスによるランダムアクセスは使用していませんので、インデックスを 1 間違えるといったよくあるエラー *4 は起こりえません。
行のインデックスが・・・とか、列のインデックスが・・・とか考えなくていいので、とても楽ができます。


パターンマッチは網羅性のチェックもしてくれるのですが、今回は when を使ったので、条件を満たさない空のリストの場合の考慮を忘れてしまい、テストで検知しました・・・
ここを

let rec comb tmp = function
| xs::xss -> xs |> List.collect (fun x -> comb (x::tmp) xss)
| [] -> [tmp |> List.rev]

として、呼び出し側でフィルタする形にすればよかったのですが、色んな機能が使いたかったのでこれで。
ちゃんとしたプログラムを書くときは、できる限り when は使わず、網羅性のチェックが働くようなコードを心がけましょう。


全体を見てみましょう。一度変数に代入した値を書き換えていないことがわかるでしょうか?
このように、一度代入した変数を書き換えないので、変数の値に気を配る必要がなくなり、より脳力を消費せずにプログラミングすることができます。


どうでしょうか?
F# の魅力が伝わったならうれしいです。

*1:あといろんな機能を使ってみました

*2:'a list list を 'a[,] に変換する関数を書くだけなので、F# の練習としてやってみるのもいいでしょう

*3:trg の型指定も実は省略できるのですが、ドキュメント用として書いておきました。

*4:off-by-one error と呼びます。

Excel-DNA のセルに対する書き込み性能を計ってみた

ExcelDnaUtil.Application を F# から使ったらえらい遅かったので、色々試してみました。
結論を書くと、キャッシュしてなかったから遅かっただけでした。

ベンチマーク概要

100 * 100 のすべてのセルに 1 を設定していく時間を計測しました。
セルに対する書き込み性能を見るためのベンチマークなので、実際の使用状況では全く別の結果となることに注意してください。

ベンチマークの取り方

  1. https://github.com/bleis-tift/DNA からコードを取得する
  2. dna.sln をビルドする
  3. Excel を起動する
  4. dna_fsdll/bin/Release/dna_fsdll.xll を起動した Excel に DnD
    • 「このアドインをこのセッションに限り有効にする」を選択
  5. dna_csdll/bin/Release/dna_csdll.xll を起動した Excel に DnD
    • 「このアドインをこのセッションに限り有効にする」を選択
  6. リボンからアドインタブを選択
  7. 好きなベンチマークを走らせる
  8. ステータスバーにかかった時間が秒で表示される

ベンチマーク環境

CPU
Core i5 M560 2.67GHz
メモリ
8GB
OS
Winows 7 Ultimate x64
Excel
Excel 2010 64bit

ベンチマーク結果

コード 結果 (秒)
F#(COM/キャッシュなし) 362
F#(COM/キャッシュあり) 2.65
F#(DNA) 0.56
C#(COM) 1.84
C#(DNA) 0.55
VBA 0.48

キャッシュなしの F#(COM) は VBA の 700 倍以上遅いという結果に。
まぁぶっ飛んだ値はとりあえず置いときましょう (なんでこんなに遅くなったのかは後述)。
傾向としては、セルに対する書き込み性能では

  • 言語が変わってもあまり差はないが、F# よりも C# の方が若干高速
  • COM は Excel-DNA で用意されたオブジェクトを使うよりも大体 3 倍以上遅い
  • VBAが最速

ということが言えるでしょう。

結論

セルへの書き込み速度が重要になるようなものの場合、COM ではなく Excel-DNA で用意されたオブジェクトを使うべきです。
最速は VBA ですが、単純にセルへの書き込みのみのマクロは少なく、何らかの計算などが行われるでしょう。
そうなると C# や F# で書いた方が高速になる (要出典) ため、VBA の出番は限られます。

使用したコード

C#

Range メソッドとかタプルの扱いとかがちょっと面倒でした。
ローカル変数以外で var が使えないのも残念です。


COM を使用したコードでは、dynamic が大活躍です。
Excel-DNA のオブジェクトを使用したコードでは、ExcelDnaUtil.Application をいったん dynamic で受ける必要があるのはちょっと面倒です。

static IEnumerable<Tuple<int, int>> range =
    from r in Enumerable.Range(1, 99)
    from c in Enumerable.Range(1, 99)
    select Tuple.Create(r, c);

[ExcelCommand(MenuName = "Bench(C#)", MenuText = "COM Object")]
public static void ComCs()
{
    var sw = Stopwatch.StartNew();

    dynamic excel = ExcelDnaUtil.Application;
    dynamic sheet = excel.ActiveSheet;
    foreach (var r_c in range)
    {
        dynamic cell = sheet.Cells.Item(r_c.Item1, r_c.Item2);
        cell.Value2 = 1;
    }

    sw.Stop();
    excel.StatusBar = sw.Elapsed.TotalSeconds;
}

[ExcelCommand(MenuName = "Bench(C#)", MenuText = "DNA Object")]
public static void DnaCS()
{
    var sw = Stopwatch.StartNew();

    foreach (var r_c in range)
    {
        var row = r_c.Item1 - 1;
        var col = r_c.Item2 - 1;
        var cell = new ExcelReference(row, col);
        cell.SetValue(1);
    }

    sw.Stop();
    dynamic excel = ExcelDnaUtil.Application;
    excel.StatusBar = sw.Elapsed.TotalSeconds;
}
F#

C# よりもやりたいことに集中できるのはいいのですが、dynamic に相当する仕組みを自分で作らなければならないのは面倒です。
幸い、gist に上げてくれていた人がいたため、ほぼそのまま使わせてもらいました。
が、これが dynamic に比べて遅い遅い・・・
C# の dynamic は裏でキャッシュされるので、上の仕組みにキャッシュ機能を組み込んだら 100 倍以上速くなりました。

let range = [ for r in [1..100] do
              for c in [1..100] -> (r, c) ]

[<ExcelCommand(MenuName="Bench(F#)", MenuText="COM Object ??? Cache")>]
let nonCachedCom () =
  let sw = Stopwatch.StartNew()

  let excel = ExcelDnaUtil.Application
  let sheet = excel?ActiveSheet
  range
  |> List.iter (fun (row, col) ->
       let cell = sheet?Cells?Item(row, col)
       cell?Value2 <- 1
     ) 

  sw.Stop()
  excel?StatusBar <- sw.Elapsed.TotalSeconds

[<ExcelCommand(MenuName="Bench(F#)", MenuText="DNA Object")>]
let dna () =
  let sw = Stopwatch.StartNew()

  range
  |> List.iter (fun (row, col) ->
       let row, col = row - 1, col - 1
       let cell = ExcelReference(row, col)
       cell.SetValue(1) |> ignore
     ) 

  sw.Stop()
  ExcelDnaUtil.Application?StatusBar <- sw.Elapsed.TotalSeconds
VBA

単純なコードだと VBA って一番シンプルに書けるんですね。
Dim t = Timer とかって書けたら割と満足かもしれない・・・
ま、現実世界でかかわる VBA プログラムはホビロンなのでアレですががが。

Sub vba()
  Dim t
  t = Timer

  For r = 1 To 100
  For c = 1 To 100
    ActiveSheet.Cells(r, c).Value2 = 1
  Next c
  Next r
  
  Application.StatusBar = Timer - t
End Sub