Quantcast
Channel: いげ太の日記

[C#] タプルの構造的等値および比較

0
0

NOTE: この記事は、当初、ココログの「いげ太のブログ」で公開していたものです。

.NET 4 から、BCL にタプルが入りました。

組を構築する(Building Tuple)

結局、タプルは参照型として実装されましたが、それはほとんど値型として振る舞うべきものであるようにも見えます。タプルはただ複数の値をまとめるためにあり、興味があるのはそのまとめられた値の方です。となれば、なにか 2 つのタプルを比較するときには、タプルのインスタンスの参照比較ではなくタプルの中身の値によって比較を行いたい、という場合がほとんどではないでしょうか。

Equals および等値演算子 (==) 実装のガイドライン」によれば、参照型においては、Point、String、BigNumber などの基本型を除き、等値演算子 (==) をオーバーロードしない方針がとられるようです。タプルはどうでしょうか。

var x = Tuple.Create(42, "Foo");
var y = Tuple.Create(42, "Foo");
var z = Tuple.Create(42, 256);

Console.WriteLine(x == y); // False
Console.WriteLine(x == z); // Compile Error

タプルは基本型とはみなさない、ということでしょうか。等値演算子によるタプルの比較は参照比較になります。等値演算子を用いた場合は引数に同じ型の値が要求されますが、それ以外は ReferenceEquals メソッドを使った場合となんら変わりません。

Console.WriteLine(Object.ReferenceEquals(x, y)); // False
Console.WriteLine(Object.ReferenceEquals(x, z)); // False

タプルの内容物がそれぞれ等値であるかどうか、つまり構造的等値を評価したい場合は、前述のガイドラインが示すように Equals メソッドを使います。

Console.WriteLine(x.Equals(y)); // True
Console.WriteLine(x.Equals(z)); // False

ここで考えてみたいのは x.Equals(z) です。これは妥当でしょうか。x は Tuple<Int32, String> で、z は Tuple<Int32, Int32> です。このような処理が必要な場面もあるでしょう。しかし、強い静的型付けによる便益を最大限活用しようとするなら、これはなるべく避けたい事態です。そもそも型が違う値どうしが構造的な等値関係になることはありえませんから、コンパイル エラーで弾いてもらった方が賢明でしょう。

そこで、他の手段を考えます。

Console.WriteLine(EqualityComparer<Tuple<int, string>>.Default.Equals(x, y)); // True
Console.WriteLine(EqualityComparer<Tuple<int, string>>.Default.Equals(x, z)); // Compile Error

EqualityComparer<T>.Default プロパティから等値比較子クラス EqualityComparer<Tuple<Int32, String>> のインスタンスを得て、その Equals メソッドで等値比較を行います。EqualityComparer<Tuple<Int32, String>>.Equals メソッドによって、比較する 2 つの値がともに Tuple<Int32, String> 型であることがコンパイル時に保証され、そして構造的な等値比較が行われます。

なお、EqualityComparer<T> は構造的等値のためだけにあるのではありません。あくまで、型 T によって定義された等値比較を行うためのものです。タプルに定義された等値比較が構造的等値を行うものであるので、このような結果が得られるのです。

さて、等値比較とあわせて大小比較についても見ておきましょう。比較対象が同じ型であることをコンパイル時に保証し、かつ、構造的比較を行うコードです。

var x = Tuple.Create(1, 0);
var y = Tuple.Create(1, 9);
var z = Tuple.Create(2, 0);
var foo = Tuple.Create(1, "Foo");

Console.WriteLine(Comparer<Tuple<int, int>>.Default.Compare(x, y)); // -1
Console.WriteLine(Comparer<Tuple<int, int>>.Default.Compare(x, x)); // 0
Console.WriteLine(Comparer<Tuple<int, int>>.Default.Compare(y, x)); // 1
Console.WriteLine(Comparer<Tuple<int, int>>.Default.Compare(y, z)); // -1
Console.WriteLine(Comparer<Tuple<int, int>>.Default.Compare(x, foo)); // Compile Error

EqualityComparer<T> クラスによる等値比較とほとんど同じです。Comparer<T>.Default プロパティから比較子クラス Comparer<Tuple<Int32, Int32>> のインスタンスを得て、Compare メソッドで比較を行います。

IComparable インターフェイス経由で比較を行うこともできますが、タプルが実装しているのは IComparable<T> でなく IComparable ですので、比較対象の値の型が異なる場合は実行時に例外がスローされます。ほとんどの場合、タプルの比較は Comparer<T> で行うのがよいでしょう。

Console.WriteLine(((IComparable)x).CompareTo(y));     // -1
Console.WriteLine(((IComparable)x).CompareTo(foo)); // ArgumentException
Console.WriteLine(((IComparable)x).CompareTo("foo")); // ArgumentException

このとき、タプルを IComparable にキャストするのを忘れないでください。タプルでは、IComparable.CompareTo メソッドを明示的に実装しているので、キャストしなければ呼び出すことができません。

最後に、この記事のサンプル コードでは考慮していませんが、EqualityComparer<T>.Default および Comparer<T>.Default を大量に呼び出すようなケースでは、それらをいったん変数に代入しておいた方がパフォーマンスがよいかもしれません。

Conclusion

.NET のジェネリックを十分に使いこなしている方々にとっては、なにをいまさらといった話かもしれません。しかしながら、僕が今回書いたようなことにずばり言及している情報は、すこしググっただけでは見つかりませんでした。

知らなければ、ただ回りくどいコードを量産していたかもしれません。

var x = Tuple.Create(42, "Foo");
var y = Tuple.Create(42, "Foo");

Console.WriteLine(x.Item1 == y.Item1 && x.Item2 == y.Item2);

便利なものは便利なままに使いたい。タプルというとても便利なクラスが BCL 入りしたことで、EqualityComparer<T> や Comparer<T> は、これから、よりいっそう使われていくことになるのではないでしょうか。


書籍『実践 F# 関数型プログラミング入門』を書きました

0
0

NOTE: この記事は、当初、ココログの「いげ太のブログ」で公開していたものです。

荒井さんじゃない方の著者のいげ太です。



  • 価格:¥3,360
  • 大型本: 464ページ
  • 発売日: 2011/01/07
  • 著者:荒井省三:いげ太
  • 出版社:技術評論社
  • ISBN-10: 4774145165
  • ISBN-13: 978-4774145167

というわけで。詳しい目次は出版社様のサイトからどうぞ。以下、すこしだけご紹介を。

まず、F# とはどんな言語でしょうか。最初は、まったく OCaml に見えます。すこしして、Python にも似て見えてきます。また学び進めていくと、結局 C# だなと思えてきます。あるいは VB の方が近いようにも思えてきます。そしてそのうちに、Haskell を連想したり、さらには Erlang や Scala に相通ずるものをも感じます。

そして最後に気づくでしょう。これは F# だと。

F# はおもしろい言語です。さまざまな側面を持っています。ベースこそ関数型言語ですが、その実さまざまな言語のいいとこどりをした、先進的な、マルチ パラダイム言語です。あなたがもし好奇心にあふれるプログラマーであるなら、是非一度、触れてみることをお勧めします。コンパイラ言語でありながら対話環境を持つ F# なら、お手軽に始めることができます。

とはいえ、手続き型言語やオブジェクト指向言語といった、メインストリームのプログラミング言語に慣れた方には、F# は、いささか小難しいように感じられるかも知れません。はい、その通りです。そんなことない簡単だ、などと言い張るつもりはありません。

「高ければ高い壁の方が 登った時気持ちいいもんな」と歌ったのはミスチルですが、あなたがプログラマーとして、まだ限界だなんて認めないつもりなら、F# は良き壁となってくれるでしょう。

本書では、F# の言語仕様についてかなり広範に説明しています。しかしまた、初心者の方にとって難しくなりすぎないように、一歩ずつ学び進めることができるように工夫を凝らしました。本書が F# 学習の一助となり、F# の楽しさをすこしでも伝えることができたなら、無上の幸いです。

ところで、執筆にあたっては、たくさんのレビュアーの方々にご協力をいただき、素晴らしいツッコミを多数いただきました。この本がご評価いただけることがあれば、それはレビュアーの方々のご尽力あってこそです。この場を借りてお礼申し上げます。本当にありがとうございます。

一方で、著者の至らなさから、発売後、少なくない数の誤植についてご指摘をいただいております。誠に申し訳ありません。この点につきましては、現在、出版者様のサイトにて公開すべく正誤表を準備しております。公開時期が決まり次第、追ってご連絡いたしますので、もうしばらくお待ちください。

なお、もしなにかツッコミがありましたら、本エントリへのコメントやトラックバック、igeta{dot}net{at}gmail{dot}com 宛のメール、Twitter では専用ハッシュタグ #PracticalFS付き tweet か @igetaへのリプライなどで、お気軽にご連絡ください。

F# 開発チームの面々はいつも、F# の名前の由来を「F is for Fun!」と茶目っ気たっぷりに語ってくれます。F is for Fun! の精神がすこしでも伝わりますように。

Enjoy!

C#er のためのやさしい再帰入門

0
0

NOTE: この記事は、当初、ココログの「いげ太のブログ」で公開していたものです。

よく訓練された C# 使いならばご存じの通り、C# に末尾最適化はない。より正確に言い換えるなら、C# 4.0 コンパイラは 'tail.'プリフィックスを付与しない。このことによって、C# プログラミングにおいては、再帰はおよそ避けるべきものとして認識されている。しかし。

しかしポインタと再帰の明らかな重要性以上に重要なのは、これらの学習から得られる精神的な柔軟さと、これらを教えている授業からふるい落とされないために必要な精神的態度が、大きなシステムを構築する上で欠かせないということだ。ポインタと再帰には、ある種の推論力、抽象的思考力、そして何よりも問題を同時に複数の抽象レベルで見るという能力が要求される。そしてポインタと再帰を理解できる能力は、優れたプログラマになるための能力と直接的に相関している。

Javaスクールの危険 - The Joel on Software Translation Project

ならば。もしあなたが再帰の得意でない C#er であるならば。末尾最適化を備えた .NET 言語である F# との対比によって再帰に入門してみるってのはどうか。

はじめに

この記事では、末尾再帰とそうでないふつうの再帰の違いについて述べません。while ループを末尾再帰に書き換えるだけのことを説明します。

while

ありがちな学習用コードを示そう。1 から引数 n までの整数の総和をとる関数 Sum だ。ここでは、ごく簡単な例として示すために for や foreach なんてな高級なループ構文を使わずに、while で書く。

int Sum(int n)
{
var ret = 0;
var i = 1;
while (i <= n) {
ret = ret + i;
i = i + 1;
}
return ret;
}

// Console.WriteLine( Sum(10) ); // 55

対応するように F# コードも示そう。上の C# コードと下の F# コードに、たいした違いがないとわかるだろう。

let sum n =
let mutable ret = 0
let mutable i = 1
while i <= n do
ret <- ret + i
i <- i + 1
ret

// printfn "%d" (sum 10) // 55

変数を減らす工夫

さて上記のつくりでは、1、2、3、4と、数値を1づつ加算してループを行うために、ループ カウンターの i と、どこまで加算するかの終了条件となる n を、それぞれ個別の変数として持っている。

しかしこれは、n、n-1、n-2、n-3と、数値を1づつ減算していって最後は1で終わるようにすれば、引数の n 自体をループ カウンターとして使うことができるので、変数 i をなくしてしまえる。

int Sum(int n)
{
var ret = 0;
while (n > 0) {
ret = ret + n;
n = n - 1;
}
return ret;
}

同じように F# コードも書き直そう。ただしちょっとした問題がある。F# の引数には基本的に再代入ができない。よってここでは、関数内で、引数と同じ名前の再代入可能な変数を定義することでやってみよう。結局、実質の変数は減ってないじゃんって話だけど、気分はいくぶん楽になるだろう。

let sum n =
let mutable n = n
let mutable ret = 0
while n > 0 do
ret <- ret + n
n <- n - 1
ret

変数でなく、引数として

さて、いま引数の n と変数の n は同質化してしまったってわけ。でもどうせなら、こんなまがいもんの同質化じゃなくて、ほんとうに同じものって呼べるようにしてしまいたい。もちろん、F# では引数への再代入ができないから、そう単純にはいかないわけだけど。

// こうしたい 
let sum n =
let mutable ret = 0
while n > 0 do
ret <- ret + n
n <- n - 1
ret

てゆうか、ついでだから ret もなくしてしまおうか。いま関数内の n をなくそうとしているわけだけど、これはつまり、変数の n を引数の n だけでまかなってしまおうってことで、すなわち、関数内にある変数を引数に追い出してしまえば万事オーケーって寸法。なら ret も引数に追い出せばいいんじゃん。

// むしろこうしたい 
let sum ret n =
while n > 0 do
ret <- ret + n
n <- n - 1
ret

// 呼ぶときはこんな感じで
// printfn "%d" (sum 0 10) // 55

じゃあやってみよう。でもどうすれば。再代入をなくせばいい。なぜいま再代入が必要。ループがあるから。ループの代わりを探せばいい。そう、それが、つまり、再帰だ。

繰り返しの条件

でも while は条件判断をもやっている。while ループを使わずに、でも条件判断は行いたい。よろしい、ならば if だ。while の中に隠れている if を引きずり出せ。白日の下に。コードの上に。

let sum ret n =
if n > 0 then
ret <- ret + n
n <- n - 1
ret

としたところで一つ触れておきたいのは、実のところ F# の if は、C# でいうところの条件演算子(3項演算子)の方に近いってこと。cond ? exp1 : exp2 の形で、値を返すアレ。つまり、上記のコードじゃあ exp2 がなくて変だ。ここでは、なにもしないことをあらわす () という値を当て込んで、ひとまずの体面を整えておきたい。

let sum ret n =
if n > 0 then
ret <- ret + n
n <- n - 1
else
()
ret

sumthing else

さて、肝心の再帰呼び出しをまだ書き入れていない。関数の内側からその関数自身を呼ぶんだ、再代入していた ret + n と n - 1 を引数に渡して。

let sum ret n =
if n > 0 then
sum (ret + n) (n - 1)
else
()
ret

しかしこれはおかしなコードだ。なにか変だ。そんな匂いがする。無理やりに突っ込まれた () がアンバランスさを教えている。これは C# で書き直すならこんなコードだ。

int Sum(int ret, int n)
{
(n > 0) ? Sum(ret + n, n - 1) : null;
return ret;
}

() は null ではない。() を null で置き換えるのは無理やりにすぎたのだとしたら、あるいはこんなコードと見立てよう。

int Sum(int ret, int n)
{
if (n > 0)
Sum(ret + n, n - 1);
else
;
return ret;
}

なにか嫌な予感がしてこないか。

再帰

問題の本質はこうだ。while 版の Sum が呼ばれるとき、定義中の return ret が評価されるのは一度っきりだ。しかし再帰版の Sum では、再帰によって Sum が呼ばれる度に return ret が評価される。これはイケナイ。

ということは、return ret が評価されるのを一度っきりにすればいいのであり、つまり、再帰時には return ret を通らないようにする。そんな場所に return ret を置くのだ。そんな場所はどこ。else 節だ。

int Sum(int ret, int n)
{
if (n > 0)
Sum(ret + n, n - 1);
else
return ret;
}

オーケー。まだバランスが悪い。分岐の片側でしか return されていない。再帰呼び出しの Sum の返り値が捨てられている。きっとこうだ。

int Sum(int ret, int n)
{
if (n > 0)
return Sum(ret + n, n - 1);
else
return ret;
}

「きっと」って、アバウトすぎる。なら声に出して読んでみよう。指差し確認、声出し確認。

「n > 0 のときは、ret + n を ret に、n - 1 を n に渡して再度 Sum を呼んでその結果を、そうでないときは ret の値を返す」

ほら、なにも問題ない。

なにも再帰だけ難しく考えすぎる必要はないんだ。ループを使う時に「この条件が成立する間はループの中身を実行して、成立しなくなったらそこから抜けて変数の値を返す」ということだけを意識すればよかったように、再帰だって前述のように読めばよいだけ、「この条件が成立するときは再帰して結果を返す、成立しなかったら引数の値を返す」と認識すればよいだけだ。

重要なのは、どんな条件で再帰して、そして再帰の果てでなにを返すのか。

再帰によって、いずれその果てで得られる最終の結果を返すためには、再帰呼び出し部分にも return 付けておかなければいけないのであり、再帰呼び出し先に結果の算出をお任せしておいてそいつを捨てちゃあ元も子もない。

それ一行で

F# の if は C# の条件演算子により近しいと言った。なら条件演算子を使おう。

int Sum(int ret, int n)
{
return (n > 0) ? Sum(ret + n, n - 1) : ret;
}

そして F# に戻ろう。こうなる。

let sum ret n =
if n > 0 then sum (ret + n) (n - 1) else ret

寄せ

実はまだ上記の F# コードは動かない。再帰関数には rec キーワードを付ける必要がある。

F# では、コードは上から順に定義され、未定義のモノを使用することはできない。また、定義中のモノは未定義状態とみなすのがデフォルトの解釈になるのだけれど、これを定義済とみなすべく rec を付与する。つまり、rec の存在が定義中を未定義とみなすか定義済とみなすか制御可能にするのである。と、込み入った説明をするとそういうことだ。

let rec sum ret n =
if n > 0 then sum (ret + n) (n - 1) else ret

さあ、これで動くコードになった。けどもうちょっとイイ感じに仕上げよう。かっこよく洗練された再帰関数にするのだ。

まずは ret 引数だ。ret じゃよくわからない。この場合は accumulation から acc と名付けるのがよいだろう。そして、このような再帰の間中引き回すためだけの取って付けたような引数は、引数リストの最後に鎮座するのがお定まりだ。

let rec sum n acc =
if n > 0 then sum (n - 1) (acc + n) else acc

次に、再帰呼び出しの位置。別に現状でもなんら問題はないが、せっかく末尾再帰と呼ばれるものを書いたのだから、コード上でも後ろの方にあった方が“この例の場合は”それっぽく見えていいと思える。if の条件をうまくひっくり返して、then 節と else 節を入れ替えよう。

let rec sum n acc =
if n <= 0 then acc else sum (n - 1) (acc + n)

最後に、sum の呼び出し側を考える。いま sum 関数の呼び出しは、たとえば10までの総和であれば、sum 10 0 のように行う必要がある。そのくせ第2引数は常に0でよい。常に0でよいのなら、常に0を与えるような関数でラップして、sum 10 で呼び出せるようにするべきだ。

let sum n =
let rec f n acc = if n<=0 then acc else f (n-1) (acc+n)
f n 0

おつかれさま。これでできあがり!

書評「C#プログラミング入門」

0
0

著者の出井秀行さんから献本いただいた。Web では Gushwell さんとお呼びした方が通りがよいだろう。窓際プログラマーの独り言あらため Gushwell's C# Dev Notes、あるいは C# プログラミングレッスンで著名なあの Gushwell さんの著書である。

C#プログラミング入門
  • 価格:¥2,415
  • 単行本: 319ページ
  • 発売日: 2011/5/12
  • 著者:出井秀行
  • 出版社:工学社
  • ISBN-10: 4777515982
  • ISBN-13: 978-4777515981

目次など記載の出版社様ページはこちら、Gushwell さんご自身によるサポート ページはこちらから。

一言でいえば、新人にはとりあえずこれ渡しとけば OKの一冊である。

C# の学習資料は Web に充実している。DOBON.NETしかり、++C++;// 未確認飛行 Cしかり、そしてもちろん先に上げた Gushwell さんのブログ、メルマガしかりである。ただし一方で、パラパラとめくり、付箋でメモりながら、時と場所を選ばずに読むことができるという優位性が紙の本にはある。あるいは逆に、コピペできない不便さが、サンプル コードの写経への強制力として働くかもしれない。

しかし Gushwell さんの語り口はおもしろい。ともすれば淡々と事務的に書かれているようで、どこかウェットな、人情味のあるやさしさをたたえている。なんとも不思議な文章を書く人である。そしてそういった文章は、実に初心者にフィットする。

C# という言語は進化の著しい言語だ。いまや当初から比べれば仕様もずいぶん大きくなった。それは、300ページそこそこの単行本サイズには土台おさまるものでもない。

ところが Gushwell マジックである。なぜだかよくわからないが、この本にはそれが無理なく収まっている。値型と参照型、ボックス化、ポリモーフィズム(多態性)、そしてラムダ式や LINQ のみならず dynamic まで。初心者が過不足なく学べるよう巧妙に配慮され、あまり深入りせず、けれど躓きやすいポイントにはコラムで先回りした文章たちが、整然と並んでいるのである。

また、本題に入る前に Visual Studio によるデバッグ法に触れられているのは C# ならではといったところ。IDE を使用したブレークポイントの使用、それにステップ イン、ステップ オーバー、ステップ アウトについての解説がまずある。実際のコードの動きを知ることの大切さ、動かしながら学ぶことのたのしさが強調されている。

ただ一点注意しておきたいのは、本書の冒頭「本書を読むにあたって」にも書かれているが、まったくのプログラミング初心者を対象とした本ではないというところである。「『変数』『代入文』『演算子』『条件分岐文』『繰り返し文』について理解していることを前提としています」とあり、個人的にすこし付け加えると、なんとなく使ったことあるだろう程度には配列についての知識を期待しているように思われる。

一見すると対象読者が狭いようにも感じられる、が、実際のところこれって、いわゆる新人さんにはよくありがちなスキル セットではないだろうか。プログラミングについて勉強してきました、でもアプリケーションの作成経験は、えーっと、がんばります、みたいな。

だからこれ、入門書として、もっともとは言わないまで、かなりマスをターゲットにしているんではないかなーと僕には思える。というか、そもそもそこさえわからない場合は、先輩が手取り足取り反応を見ながら教えた方がよいだろう。

というわけで、C# をすでに知っている方には読む必要のない本ではある。けれど、不必要な本ではないかもしれない。あなたの後輩のために、一部署に一冊、購入しておいて損はない。いまから C# を学ぼうという方にとって、本書は明らかにすばらしい選択肢だ。

まずはこの薄い本から。秋の夜長に C# をはじめよう。

「プログラミングの魔導書 Vol.2」が濃ゆい

0
0

ウワサの書籍、「プログラミングの魔導書 Vol.2」を発売前に拝見する機会をいただいた。ので、ご紹介など。今回のテーマは「言語の進化」だとか。

  • 価格:書籍版:2,000円/PDF版:1,500円/セット:2,500円
  • ページ数: 204ページ
  • 発売日: 2011/10/05
  • 著者: Dave Abrahams、江添 亮、小泉 将久、Masahiro Nakagawa、k.inaba、水島 宏太、shelarcy、菊池 正史、近藤 貴俊、柏田 知洋
  • 出版社:ロングゲート
  • 書籍版ISBN: 978-4-9905296-2-8
  • PDF版ISBN: 978-4-9905296-3-5

というか、のぶひささん著の記事「風とF#とメビウスの輪」のレビューに参加させていただいたわけで、そちらの内容についてまずは。

相変わらず摩訶不思議アドベンチャーといった文体で小気味よく、手始めに F# の特徴的な機能の解説からかいつまんで、それからライブラリーを使用したサンプル コードがちょくちょく引き合いに出されるのだけれどもこれがゴイスー。NaturalSpecFParsec、それに AcceleratorZ3とか。あっついで~。

あと個人的に「おわりに」に書かれている一節が非常に F#er らしいな、とか思った。

新しいものは良いものだ。現状に満足しない技術者達が今日も未来を作っているからである。だから、ひょっとしたら数年後にはより良い言語が生み出されていて、その時の私はもうF#を使っていないかもしれない。先のことはわからないが、

ネタバレしないようひかえめに、中途半端に切り取って引用してみた。続きはぜひ書籍を購入して確かめていただきたい。

そしてもちろん、ほかの記事もすばらしいものばかり。ていうか濃ゆい。Web を検索・徘徊してもいまひとつ出てこなかった情報がこんなにもてんこ盛り。やばい。胸焼けしそう。

D 言語、Scala、Haskell、Coq ときて依存型(Dependent Type)とかまじやばい。てか「Boostを使い倒してTwitterクライアントを作る」で C++ に入門したい。C++ 使えますとか言ってみたい。ホント、刺激的な一冊(いろんな言語に触ってみたい病を発症・悪化させるおそれのあるキケンな一冊でもある)。

しかし単純に買いでしょうと、思うわけで。

F# コンパイラをビルドする

0
0

F# Advent Calendar 2011 25日目の記事です。今日が最終日と思いきや、ご好評につきボーナス ステージ突入! とのことで。 もうちっとだけ続くんじゃ。

さて、F# は実に心憎いプログラミング言語です。

たとえば fsi(F# Interactive)、いわゆる REPL と呼ばれる対話型評価環境の存在です。API の動作をちょっと調べたいとき、コード片の実行結果をサクッと見てみたいとき、fsi は実に重宝します。なにより、fsi console を開いて、黒い画面に向かて白い文字列をつかつかと打ち込む行為そのものが、なにかハッカーっぽくてすごく気分のよいものです。

「捕獲・・・完了・・・!」

言っちゃうか、そんなことも。

もう一つ、F# にはハッカー気分に浸ることのできる特徴があります。それは、コンパイラのソースコードが公開されている、それもオープン ソース ライセンスで、ということです。標準ライブラリの詳しい挙動を確認したいとき、必要であれば、ソースコードという生の情報にまでたどることが可能です。そして、ソースはただ参照できるだけではありません。

「君のプログラミング言語で、コンパイラのビルド、できる?」

余裕ッス! そう、F# ならね。

ではでは。まず、F# コンパイラは F# で書かれており、これをビルドするためには、F# + .NET 4 の環境がセットアップされていることを前提とします。

次に、CodePlex 上で公開されるコンパイラ ソースコードを取得します。下記リンクから Source Code ページに飛んで、Last Version 枠内にある Download リンクをクリックしてダウンロードしましょう。

F# PowerPack, with F# Compiler Source Drops

落ちてきた fsharppowerpack-<ChangeSet>.zip 内の compiler\2.0\Aug2011.1 ディレクトリをどこか適当な作業ディレクトリに解凍します。あわせて、ディレクトリ名が Aug2011.1 ではわかりづらいですから、fsharp とでもリネームしておきましょう。

で、ここから実際にビルドしていくわけですが、必要な情報は fsharp ディレクトリ内に同梱の README.html に全部書いてあったりします。で、それをバッチにまとめたものが以下になります。

@echo setting environment
cd %~dp0
call run40.bat

@echo cleaning
cd src
if exist ..\Proto (rmdir /s /q ..\Proto)
if exist ..\Debug (rmdir /s /q ..\Debug)
if exist ..\Release (rmdir /s /q ..\Release)

@echo building a Proto Compiler
msbuild fsharp-proto-build.proj /p:TargetFramework=cli\4.0

@echo for faster future startup (optional)
@rem ngen install ..\Proto\cli\4.0\bin\fsc.exe

@echo building the F# core library and unittests
msbuild fsharp-library-build.proj /p:TargetFramework=cli\4.0
@echo building a bootstrapped F# Compiler
msbuild fsharp-compiler-build.proj /p:TargetFramework=cli\4.0

@echo unit tests for NUnit
@rem msbuild fsharp-library-unittests-build.proj /p:TargetFramework=cli\4.0
@rem msbuild fsharp-compiler-unittests-build.proj /p:TargetFramework=cli\4.0

build.bat など適当な名前を付けて fsharp ディレクトリにこれを保存、そして実行します。ビルドには5分前後かかるので、カプチーノでも作って待ちましょう。すると、fsharp\Debug\cli\4.0\bin に、fsc や fsi や標準ライブラリ群がどっさとできあがります。

そうして最後に、fsi を立ち上げて 1 + 1;; なり任意のコードを実行して、動作確認しておくとよいでしょう。自分でビルドしたコンパイラで実行されるコードには、いつもとは違う、新鮮な感動があります。

今日から君はコンパイラ ハッカー!

ここでは取ってきたソースコードを単にビルドしただけですが、いまや私たちは、コンパイラを、標準ライブラリを、自在に操るチケットを手に入れたのです。思う存分いじくり回しちゃいましょう。

とはいえ、ふつうの F# プログラマーは Visual Studio 搭載の Microsoft 謹製コンパイラを使う・使いたいことでしょう。コンパイラをいじくり倒して、ここはこう改良できるんじゃないかとよいアイデアが浮かんだら、UserVoice サイトがあります。

Visual Studio: Languages - F# – Customer Feedback for Microsoft

UserVoice サイトでは、Visual Studio 次期バージョンへの提案を広く受け付けています。ぜひ、あなたのアイデアをフィードバックしてください。あなたのアイデアが、次期 F# コンパイラに取り込まれるかもしれませんよ?

Enjoy together!

「実践 F#」の正誤表について

0
0

大変お待たせして申し訳ありません。書籍「実践 F#」の正誤表が公開されました。出版社サポート サイトで重要な訂正を、荒井さんのブログでさらなる訂正を公開している形となります。

すべての正誤情報を保存・印刷するには不便な格好での公開となり、重ねてお詫び申し上げます。つきましては、正誤情報をスクレイピングして以下画像のような HTML にまとめて整形する F# スクリプトをご用意いたしました。

F# スクリプトは以下の通りです。使用方法は、スクリプトの先頭のコメントに記載しております。よろしければお使いください。

(**
*                          PracticalFS.Errata.fsx
*                                by @igeta
*
* 書籍「実践 F#」の正誤表を取得する F# スクリプトです(以下、本スクリプト)。
*
* 本スクリプトは、Html Agility Pack ライブラリ(ライセンス:Ms-PL)を使用
* します。Html Agility Pack は、http://htmlagilitypack.codeplex.com/ から
* HtmlAgilityPack.1.4.0.zip をダウンロードして使用します。
*
* 本スクリプトを使用するには、まず適当なフォルダーを作成し、その中に本スク
* リプトと、HtmlAgilityPack.1.4.0.zip を解凍してできる HtmlAgilityPack.dll
* など3つのファイルを格納します。
*
*    HtmlAgilityPack.dll
*    HtmlAgilityPack.pdb
*    HtmlAgilityPack.XML
*    PracticalFS.Errata.fsx
*
* そして、「fsi PracticalFS.Errata.fsx」コマンドを実行して、同フォルダー内に
* Errata.html を生成します。
*)

#r@"HtmlAgilityPack.dll"

module Scraper =
    open System
    open HtmlAgilityPack

    let readUri (addr: Uri) =
        use webc = new System.Net.WebClient()
        let strm = webc.OpenRead(addr)
        new System.IO.StreamReader(strm)
    let loadHtml (addr: Uri) =
        let html   = HtmlDocument()
        use reader = readUri addr
        html.Load reader
        html.DocumentNode

    let selectNode xpath (node: HtmlNode) =
        node.SelectSingleNode(xpath)
    let selectNodes xpath (node: HtmlNode) =
        node.SelectNodes(xpath)

    let getHtml (node: HtmlNode) = node.InnerHtml
    let getText (node: HtmlNode) = HtmlEntity.DeEntitize(node.InnerText)

module Program =
    open System
    open System.IO
    open System.Text.RegularExpressions
    open Scraper

    let split (s:string) =
s.Split([|' '; ' '|], StringSplitOptions.RemoveEmptyEntries)
    let pageNum s =
String.Format("{0:000}", Regex.Replace(s, "[^\d]*(\d+).*", "$1") |> int)
    let correct s =
Regex.Replace(s, @"<strong>", @"<strong style=""color: #c00;"">")
    let ssget ss i =
if i < Array.length ss then ss.[i] else""
    
    let fromGihyo () =
        Uri(@"http://gihyo.jp/book/2011/978-4-7741-4516-7/support")
        |> loadHtml
        |> selectNode @"//div[@class='readingContent01']"
    let fromMsdnB () =
        Uri(@"http://blogs.msdn.com/b/shozoa/archive/2012/02/29/practical-f-errata1.aspx")
        |> loadHtml
        |> selectNode @"//div[@id='ctl00_content_ctl00_content']//div[contains(@class, 'post-content')]"
    let getRecords root =
        Seq.zip (selectNodes @"./h4"          root)
                (selectNodes @"./table/tbody" root)
        |> Seq.map (fun (h4, tb) ->
             let xs = h4 |> getText |> split
             let ys = tb |> selectNodes @"./tr/td" |> Seq.map getHtml |> Seq.toArray
             [| pageNum (ssget xs 0); ssget xs 1; ssget ys 0; correct (ssget ys 1) |])

    let writeTable (writer: StreamWriter) =
        writer.WriteLine(@"<table>")
        writer.WriteLine(@"<tr><th style=""width: 36px;"">Page</th><th style=""width: 98px;"">見出し</th><th>誤</th><th>正</th></tr>")
        let xs, ys = getRecords <| fromGihyo (),
                     getRecords <| fromMsdnB ()
        Seq.append xs ys
        |> Seq.sort
        |> Seq.iter (fun arr ->
             writer.WriteLine("<tr>" + String.concat "" (Array.map (fun s ->"<td>"+s+"</td>") arr) + "</tr>"))
        writer.WriteLine(@"</table>")

    let main () =
        let path = Path.Combine(__SOURCE_DIRECTORY__, "Errata.html")
        use writer = new StreamWriter(path)
        writer.WriteLine(@"<!DOCTYPE html>")
        writer.WriteLine(@"<html>")
        writer.WriteLine(@"<head>")
        writer.WriteLine(@"<title>実践F# 関数型プログラミング入門(正誤表)</title>")
        writer.WriteLine(@"<meta http-equiv=""Content-Type"" content=""text/html; charset=utf-8"">")
        writer.WriteLine(@"<style>")
        writer.WriteLine(@"    table { border-collapse: collapse; empty-cells:show; }")
        writer.WriteLine(@"    th, td { border: solid 2px #ccc; }")
        writer.WriteLine(@"</style>")
        writer.WriteLine(@"</head>")
        writer.WriteLine(@"<body>")
        writeTable writer
        writer.WriteLine(@"</body>")
        writer.WriteLine(@"</html>")

    do main ()

いつ FSharpFunc.Adapt を使うべきか?

0
0

この記事は、Stack Overflow のすばらしいスレッド、When should we use FSharpFunc.Adapt?を和訳したものです。和訳文章のライセンスは、Stack Overflow のそれと同じく Creative Commonsに従います。

Question

Looking at the source in FSharp.Core and PowerPack, I see that a lot of higher-order functions that accept a function with two or more parameters use FSharpFunc.Adapt. For example:

FSharp.Core や PowerPack のソースを見ると、“2つ以上の引数を持つ関数”を引数に取るような高階関数の多くで、FSharpFunc.Adaptが使われています。以下に一例を挙げます。

let mapi f (arr: ResizeArray<_>) =
let f = FSharpFunc<_,_,_>.Adapt(f)
let len = length arr
let res = new ResizeArray<_>(len)
for i = 0 to len - 1 do
res.Add(f.Invoke(i, arr.[i]))
res

The documentation on FSharpFunc.Adapt is fairly thin. Is this a general best practice that we should be using any time we have a higher-order function with a similar signature? Only if the passed-in function is called multiple times? How much of an optimization is it? Should we be using Adapt everywhere we can, or only rarely?

FSharpFunc.Adapt のドキュメントは薄っぺらすぎます。これって、同じようなシグネチャの高階関数を持つときはいつでも使うべき、一般的なベスト プラクティスなんでしょうか? それか、渡された関数が複数回呼び出される場合だけ? どのくらいの最適化なんだろう? 私たちが Adapt を使用するべきときとは、できるならどこでもなのか、あるいはごくまれになのか?

Thanks for your time.

お時間を取っていただいてありがとう。

asked Mar 17 '11 at 6:58
Joel Mueller

Answers

That's quite interesting! I don't have any official information (and I didn't see this documented anywhere), but here are some thoughts on how the Adapt function might work.

実に面白い! 私はまったく公式情報を持っていませんが(というか、どこにも文書化されてるのを見たことありませんが)、Adapt 関数がどのように働くのかちょっと考察してみます。

Functions like mapi take curried form of a function, which means that the type of the argument is compiled to something like FSharpFunc<int, FSharpFunc<T, R>>. However, many functions are actually compiled directly as functions of two arguments, so the actual value would typically be FSharpFunc<int, T, R> which inherits from FSharpFunc<int, FSharpFunc<T, R>>.

mapi みたいな関数は、カリー化形式の関数を受け取りますが、これは引数の型が FSharpFunc<int, FSharpFunc<T, R>> のようにコンパイルされる、ということを意味します。しかしながら、多くの関数が実際には2引数の関数に直接コンパイルされる、つまり実際の値は FSharpFunc<int, FSharpFunc<T, R>> を継承した FSharpFunc<int, T, R> になる、というのが普通です。

If you call this function (e.g. f 1 "a") the F# compiler generates something like this:

この関数を呼び出すならば(例:f 1 "a")、F# コンパイラはこんなものを生成します。

FSharpFunc<int, string>.InvokeFast<a>(f, 1, "a");

If you look at the InvokeFast function using Reflector, you'll see that it tests if the function is compiled as the optimized version (f :? FSharpFunc<int, T, R>). If yes, then it directly calls Invoke(1, "a") and if not then it needs to make two calls Invoke(1).Invoke("a").

Reflector を使って InvokeFast 関数を見てみると、(f :? FSharpFunc<int, T, R>) という風に、渡された関数が最適化されたバージョンとしてコンパイルされているかどうかテストしていることがわかります。yes の場合は Invoke(1, "a") を直接呼び出して、そうでない場合は Invoke(1).Invoke("a") という2つの呼び出しを行う必要があります。

This check is done each time you call a function passed as an argument (it is probably faster to do the check and then use the optimized call, because that's more common).

このチェックは、関数に実引数を1つ渡して呼び出すたびに行われます(チェックを行ってから最適化された呼び出しを使用する方がたぶん速いです、その方がより一般的ですから)。

What the Adapt function does is that it converts any function to FSharpFunc<T1, T2, R> (if the function is not optimized, it creates a wrapper for it, but that's not the case most of the time). The calls to the adapted function will be faster, because they don't need to do the dynamic check every time (the check is done only once inside Adapt).

Adapt 関数が行うこととは、任意の関数を FSharpFunc<T1, T2, R> に変換することです(関数が最適化されていない場合、そのためのラッパーを作成しますが、そんなケースはほとんどありません)。adapted な関数は、動的チェックを毎回行う必要がないために、呼び出しがより速くなります(チェックは Adapt 内部で一度だけ行われます)。

So, the summary is that Adapt could improve the performance if you're calling a function passed as an argument that takes more than 1 argument a large number of times. As with any optimizaions, I wouldn't use this blindly, but it is an interesting thing to be aware of when tuning the performance!

で、要するに Adapt は、2つ以上の引数を取る関数に対して引数を渡して呼び出してを何度も繰り返し行う場合に、パフォーマンスを向上させることができます。 任意の最適化と同様に、盲目的に使うようなものではないだろうけど、パフォーマンス チューニングするときには注意が必要な興味深い点ですね!

(BTW: Thanks for a very interesting question, I didn't know the compiler does this :-))

(追伸:とっても興味深い質問ありがとう、コンパイラがこんなことしてるなんて知らなかったよ :-))

answered Mar 17 '11 at 12:33
Tomas Petricek


F# Interactive を独自ビルドしてプロンプトにカレント ディレクトリを表示する

0
0

F# Advent Calendar 2012 19日目の記事です。前日のご担当は nomurabbit さんです。

昨年の F# Advent Calendar で「F# コンパイラをビルドする」という記事を書いたものの、一向にコンパイラを弄ろうという人が増えなくて残念な気持ちのいげ太です。確かに、ただビルドするだけでは、あまりおもしろくはないかもしれませんですが。はい。

というわけで、今年も懲りずに独自ビルドなネタで行こうと思います。今年は、去年よりもみなさんの興味を引ける記事になればと思っております。よろしくお願いします。

ちなみに、本記事の更新が遅れましたのは、やんごとなき理由でも年末進行で残業をしていたわけでもなく、Wii U を購入してはじめての本体更新を行っていたためであり、GamePad に向かって「がんばれ」「ありがとう」「ドラミちゃーん」などと話しかけてみたものの更新は遅々として進まず、このような時間のブログ更新となりました次第です。

石を投げられても文句は言えませんが、VBA の仕事は投げて来ないでください。

で、今回行うのは、source drops に少しばかり手心を加えて独自ビルドしてみようというもので、表題の通り、F# Interactive Console のプロンプト文字列にカレント ディレクトリを表示するべく改造を施します。なんとなくシェルっぽくなって、より実用的な fsi.exe になること請け合いです。本当です。いろいろ捗るのでぜひ。

F# コンパイラのビルド方法については、前述した昨年の記事内容と変わりありません。ただし、現在の最新版ソースは compiler\3.0\Sep2012 になっていますので、その点だけは読み替えてください。

コンパイラ、というか対話型シェルを改造するというと大げさに聞こえるかもしれませんが、「プロンプト文字列を変える」ぐらいのお題であればかんたんで、compiler\3.0\Sep2012\src\fsharp\fsi\fsi.fs の700行目あたりにある、FsiConsolePrompt なるこじんまりとした型をほんのすこし変更するだけで済みます。まずは対象のソースを引用します。

//----------------------------------------------------------------------------
// Prompt printing
//----------------------------------------------------------------------------

type FsiConsolePrompt(fsiOptions: FsiCommandLineOptions, fsiConsoleOutput: FsiConsoleOutput) =

// A prompt gets "printed ahead" at start up. Tells users to start type while initialisation completes.
// A prompt can be skipped by "silent directives", e.g. ones sent to FSI by VS.
let mutable dropPrompt = 0
// NOTE: SERVER-PROMPT is not user displayed, rather it's a prefix that code elsewhere
// uses to identify the prompt, see vs\FsPkgs\FSharp.VS.FSI\fsiSessionToolWindow.fs
let prompt = if fsiOptions.IsInteractiveServer then "SERVER-PROMPT>\n" else "> "

member __.Print() = if dropPrompt = 0 then fsiConsoleOutput.uprintf "%s" prompt else dropPrompt <- dropPrompt - 1
member __.PrintAhead() = dropPrompt <- dropPrompt + 1; fsiConsoleOutput.uprintf "%s" prompt
member __.SkipNext() = dropPrompt <- dropPrompt + 1
member __.FsiOptions = fsiOptions

prompt 変数に ">"なるプロンプト文字列が設定されていることがすぐにわかるでしょう。まずはこれを "$ "あたりに書き変えましょう。たった1文字、これを変えてリビルドするだけでも、オレオレ fsi が手に入って割とテンション上がります。みなぎってきた。はいそこ、プロンプト文字列くらい実行時に変更可能なよう作られているべきとか言わない。

カレント ディレクトリを表示させるには、prompt を実際に print する箇所、すなわち Print および PrintAhead メソッドにそれぞれ1行追加します。Directory.GetCurrentDirectory() を print すればよいだけですね。

そこで、fsiConsoleOutput.uprintf "%s" prompt を習って、fsiConsoleOutput.uprintf "%s" (Directory.GetCurrentDirectory()) のような1行をその手前に書き足せばよい、のですが、文字色を変えた方が断然見やすいですので、ここは fsiConsoleOutput.uprintf の代わりに fsiConsoleOutput.ucprintf を使っておくのが粋ってもんでしょう。

とすると、まとめて、FsiConsolePrompt はだいたい以下のように書き換わります。

//----------------------------------------------------------------------------
// Prompt printing
//----------------------------------------------------------------------------

type FsiConsolePrompt(fsiOptions: FsiCommandLineOptions, fsiConsoleOutput: FsiConsoleOutput) =

// A prompt gets "printed ahead" at start up. Tells users to start type while initialisation completes.
// A prompt can be skipped by "silent directives", e.g. ones sent to FSI by VS.
let mutable dropPrompt = 0
// NOTE: SERVER-PROMPT is not user displayed, rather it's a prefix that code elsewhere
// uses to identify the prompt, see vs\FsPkgs\FSharp.VS.FSI\fsiSessionToolWindow.fs
let prompt = if fsiOptions.IsInteractiveServer then "SERVER-PROMPT>\n" else "$ "
let cd () = Directory.GetCurrentDirectory()

member __.Print() =
if dropPrompt = 0 then
fsiConsoleOutput.ucprintf ConsoleColor.Green "%s" (cd ())
fsiConsoleOutput.uprintf "\n%s" prompt
else
dropPrompt <- dropPrompt - 1
member __.PrintAhead() =
dropPrompt <- dropPrompt + 1
fsiConsoleOutput.ucprintf ConsoleColor.Green "%s" (cd ())
fsiConsoleOutput.uprintf "\n%s" prompt
member __.SkipNext() =
dropPrompt <- dropPrompt + 1
member __.FsiOptions = fsiOptions

めんどくさければこれをコピペしましょう。そしてビルドです。むずかしいところは何もありませんよね。

あ、ついでですが、compiler\3.0\Sep2012\src\fsharp\fsi\FSIstrings.txt という文字列リソースがあるのですが、この中の最終行、fsiProductName に起動時に表示される製品名・バージョン情報が設定されていまして、これにちょこっと追記して、自分の名前なんかいれちゃったりしても、オレオレ感が増してよいかと思われます。

てな感じで、コンパイラを弄るとか言ってもパーサーに手を入れるんでもなければ、ぜんぜんこわくないので、ゆるふわな気持ちでどんどん独自ビルドにチャレンジしちゃいましょう。すごい人になれた気分を味わえますおすすめ。

F# Advent Calendar 2012、続いてのご担当は Arigata さんです。F# と農業のコラボを見逃すな!

Micro ORM を128行で

0
0

今年も F# Advent Calendar 2013の季節がやってまいりました。というわけで、初日、12/1担当の私です。

タイトルの通りなわけですが。ここで訂正です。「128行で」と申し上げましたが、よく数えたら124行でした。加えて、「Micro ORM」と書きましたが、Micro RRM でした。Micro Record-Relational Mapper です。はいそこ、レコードも結局オブジェクトでしょ? とか言わない。

ソースは github に上げておきましたので、そちらでご覧ください。

https://github.com/igeta/FsugJp.MicroRrm

実装コードはすべて FsugJp.MicroRrm.fsに書いていて、繰り返すようですがたった124行です。で、簡単な使い方を Sample.fsxで示しています。

何ができるか? 何ができないか?

現状、DB からのデータの読み込みのみできます。つまり、SELECT 文を投げて、その結果セットのデータを指定したレコードに詰めて返してくれる、ということだけをします。結果セットとレコードは、対応するように、フィールド名およびその型を合わせておく必要があります。追加・更新・削除はできません。

使い方は?

Sample.fsx の通りですが、ざっと解説。

まず、必要な結果セットに対応するレコードを作ります。いわゆる POCO ですね。POCR と言ってもいいかもしれない、ダメかもしれない。まあ何でもいいです。ありがちには、テーブルに対応するような型として作ります。説明上、たとえば MyTable とでもしましょうか。

んで、それに DB からデータを取得して詰め込むわけですが、そのための API として関数が2個あるだけでして、readBy と read です。名前でお気づきかもですが、実質1個って言ってもいいようなものですね。

readBy から説明します。DbRecord.readBy<MyTable> dbConnection "SELECT * FROM MyTable"のような形で使います。第1引数で指定したコネクションに対して、第2引数で指定した SQL 文字列を実行し、その結果セットを seq<MyTable> として返します。

read は、readBy の SQL 文指定を省略した版です。C# ならオーバー ロードで実装するところですね。DbRecord.read<MyTable> dbConnection のような形で使い、MyTable テーブルのすべてのレコードを取得します。内部的に readBy を呼んでいて、それに渡す "SELECT * FROM MyTable"な SQL を勝手に作ってくれるってだけのものです。

何がうれしいか? 見どころは?

F# 的にうれしいのは、すでに説明した通り、オブジェクトでなくレコードでデータが取得できるというところです。パターン マッチが使えます、比較演算も構造的に行われます、と。それから、Null 許容なフィールドのデータは、Nullableでなく 'a option の値としてマップするようになってます。やっぱ option でしょ、そうでしょ? すべては構造的な値として、ね。

SQL Server 的にうれしいのは、レコードを DbSchemaAttribute 属性が付与されたモジュール下に定義することによって、そのモジュール名をスキーマ名として使用する、というところ。具体的には、[&ltDbSchema>] module MySchema に MyTable レコードを定義することにより、read によって自動生成される SQL が、"SELECT * FROM MySchema.MyTable"となります。そうでない場合は "SELECT * FROM MyTable"です。まあ、やや蛇足的な機能ではあります。

それと、当たり前に欲しい機能として、レコードのフィールドと結果セットのフィールド、これらが異なる定義である場合でも、きちんと動きます。わかりにくいので具体例で言うと、MyTable レコードにはフィールド F1、F2、F3 を定義しました、でも DB 側の MyTable テーブルにはフィールド F1、F3、F5 がありますよ、という場合でも、ちゃんと F1 と F3 の値だけを取ってきてくれて、F2 にはそのフィールドの型の規定値をセットします。

今後の展開は?

もしご好評いただくようなら、開発を継続、機能拡張していきたいです。ホントに好評なら NuGet に登録したりとか? わかりませんが。

目下、個人的にちょっとやりたいなーと思っているのは、「DB 側のテーブル名やフィールド名はスネーク ケースで作ってるんだけど、F# でマップするときはキャメル ケースにしたいんだよねー」な要求への対応。あと、DB 側の名前に F# のキーワードが使われていた場合への対応とか。

それから追加・更新・削除をどうするか。そもそも ORM にも Micro ORM に詳しくないのでどうすればいいやらという感じですが。

まとめ

F# 的な Micro ORM とは、という議題についての取っ掛かりのようなものになればということで、ごく簡単なコードを示したので作ってみた、というところです。実用性は不明です。

ただ、DB からサクッとデータが取ってこれて、それを fsi でごにょれるの、割と便利。


つーわけで、F# Advent Calendar 2013、初日終了っす。2日目は 7shi さんです。バトンターッチ!

Ariawase v0.6.0 解説(vbac 編)

0
0

まずはこいつから。ライブラリ本体には興味ないけどこいつは要る、という話もあるとかないとか。

Ariawase is free library for VBA cowboys.

vbac は、フリーの VBA ライブラリ Ariawase に同梱されるツールで、各種のマクロ有効 Office ファイルから VBA コードをエクスポートしたり、あるいは逆にインポートしたりするためのものです。VBA のソースコードを git などでバージョン管理するために、また Emacs や Vim や Sublime Text あるいはサクラエディタなどの任意のエディタで VBA を編集するために、さらに grep や diff のような外部コマンドとの連携を図るために使用できます。

本ツールは、Word ファイル(doc、dot、docm、dotm)、Excel ファイル(xls、xla、xlt、xlsm、xlam、xltm)、Access ファイル(mdb、accdb)に対応しています。Outlook(otm)に関してはその仕様によりサポートができません。PowerPoint については、現状で対応していませんが、単に僕にとって必要ないというだけなので、もしご要望があれば実装します。あと Access adp も今のところ未対応です。

vbac を使用するにあたり、最初にやっておかなければならない設定があります。vbac は VBA プロジェクトにアクセスするので、Word、Excel では、セキュリティ設定を変更する必要があります。Office のバージョンによって設定場所が違いますが、2013 を例に取れば、[オプション]-[セキュリティ センター]-[セキュリティ センターの設定...]-[マクロの設定]から、[VBA プロジェクト オブジェクト モデルへのアクセスを信頼する]のチェックをオンにします。

使い方です。vbac は、vbac.wsf という単一の WSH スクリプト(実装言語は JScript)です。コマンド プロンプトから以下のような形式で実行します。

cscript //nologo vbac.wsf ...

help コマンドを実行すると、あんちょこ程度の簡単なヘルプメッセージを表示します。

>cscript //nologo vbac.wsf help
vbac (version 0.6.0)

Usage: cscript vbac.wsf <command> [<options>]

Commands:
combine Import all VBComponents
decombine Export all VBComponents
clear Remove all VBComponents
help Display this help message

Options:
/binary:<dir> Specify directory of macro-enabled Office files
(default: bin)
/source:<dir> Specify directory of source code files
(default: src)
/vbaproj Use .vbaproj file
/dbcompact With Access DB compaction

それでは実例として、Book1.xlsm から VBA コードをエクスポートしてみます。基本的には、フォルダ構成を以下のようにします。任意のフォルダ(ここでは Book1 とします)に vbac.wsf を置いて、同階層に bin フォルダを作成して対象となる Office ファイル(Book1.xlsm)を格納する、という形です。

Book1
│ vbac.wsf

└─bin
Book1.xlsm

エクスポートには decombine コマンドを使用します。Book1.xlsm には、Class1 クラス モジュール、Module1 標準モジュール、Sheet1 シート モジュールが含まれるものとすると、以下のようになります。

>cscript //nologo vbac.wsf decombine
begin decombine

> Target: Book1.xlsm
- Export: Class1.cls
- Export: Module1.bas
- Export: Sheet1.cls

end

decombine は、src フォルダを作ってそこに対象の Office ファイルと同名となる Book1.xlsm フォルダを作成し、その中に VBA ソース ファイルをエクスポートします。

Book1
│ vbac.wsf

├─bin
│ Book1.xlsm
└─src
└─Book1.xlsm
Class1.cls
Module1.bas
Sheet1.cls

では逆に、src\Book1.xlsm フォルダ内の VBA ソース ファイルを bin\Book1.xlsm にインポートしてみます。これには combine コマンドを使用します。

>cscript //nologo vbac.wsf combine
begin combine

> Target: Book1.xlsm
- Import: Class1.cls
- Import: Module1.bas
- Import: Sheet1.cls

end

combine は、bin\Book1.xlsm ファイルの内容を上書きします。あるいは、bin に Book1.xlsm ファイルが存在しなかった場合には、新しいファイルを作ってインポートを行います。

ここで重大な注意点があります。このインポート処理は、bin\Book1.xlsm ファイルに存在する VBA コードがいったんすべて削除された後で行われます。つまり、src 側にはないモジュールが bin 下の Office ファイルに存在したとすると、それらは消されてなくなってしまう、ということです。

この注意点は、combine だけでなく decombine にも当てはまります。エクスポート処理においても同様に、その前処理として、src\Book1.xlsm フォルダに存在する VBA ソース ファイルがすべて削除されます。これは、VBA ソース ファイルに対してだけ行われるのであり、何か他の、たとえば txt ファイルがあったとしてもそれは単純に無視されます。

つまり combine と decombine は、単にインポート・エクスポートを行うのでなく、いわば同期(ファイル同期)を行うためのコマンドであるということです。

これを踏まえて、X.xlsm と Y.xlsm の2つのファイルから VBA コードをエクスポートして、それらをまとめて新しいファイル Z.xlsm にインポートする、コード統合作業について考えてみます。vbac は複数の Office ファイルに対応します。ですから、まず X.xlsm と Y.xlsm の両方を bin フォルダに格納して decombine します。すると、src\X.xlsm および src\Y.xlsm フォルダに VBA ソース ファイルが作成されます。両フォルダに作成されたファイルを、新しく src\Z.xlsm フォルダを作ってそこにコピーし、今度は combine します。そうすると、2ファイルの VBA コードを統合した bin\Z.xlsm ファイルを得ることができます。

さて、ここまでを箇条書きでまとめます。

  • vbac.wsf と同じ階層に bin フォルダと src フォルダを持つ
  • bin フォルダには Office ファイルを格納する
  • src フォルダには VBA ソース ファイルを格納する
  • エクスポートは decombine コマンドを使用する
  • インポートは combine コマンドを使用する
  • combine および decombine は VBA コードを同期する
  • vbac は複数の Office ファイルにも対応

あとはだいたいおまけ。bin とか src って名前じゃなく違うフォルダを指定したい場合には、/binary および /source オプションを使用します。/binary:Debug\bin のように、コロン区切りでフォルダ パスを指定します。フォルダ パスには絶対パスおよび相対パスが指定でき、相対パスは vbac.wsf が置かれているフォルダを起点とします。

cscript //nologo vbac.wsf combine /binary:Debug\bin /source:code

対象が Access ファイルである場合、/dbcompact オプションを使用すれば、事後処理として[データベースの最適化/修復]をあわせて行うことができます。

cscript //nologo vbac.wsf combine /dbcompact

/vbaproj はちょっとしたオプションです。これを付加して decombine すると、src\Book1.xlsm フォルダに App.vbaproj というファイルが作成されます。これはいわゆるプロジェクト ファイルです。中身は素朴な ini ファイル形式になっていて、プロジェクト プロパティおよび参照設定の情報が出力されます(ただし条件付きコンパイル引数を除く。API がない)。

cscript //nologo vbac.wsf decombine /vbaproj

そして、combine 時に /vbaproj を指定すれば、App.vbaproj に出力されたプロジェクト プロパティと参照設定が bin\Book1.xlsm に復元されます。なお、参照設定の復元に関しても、既存の参照がすべて削除された上で行われますので注意してください。

あまり使う機会はないかもしれませんが、clear コマンドについても紹介しておきましょう。このコマンドは、bin\Book1.xlsm ファイルが含む VBA コードをすべて削除します。

>cscript //nologo vbac.wsf clear
begin clear

> Target: Ariawase.xlsm
- Remove: Class1
- Remove: Module1
- Remove: Sheet1

end

最後に、vbac を使用して git など VCS で VBA を管理する上での Tips です。ここまでの内容で気づかれた方もいるかもしれませんが、vbac.wsf は、案件ごとのフォルダに1つ置いて使用するよう設計されています。リポジトリには vbac.wsf を含めてコミットするのがよいでしょう。

また、特に Excel でその傾向が強いように思いますが、VBA は Office ドキュメント(ワークシート)の内容と一体となって動くものが多いです。そのため、src フォルダ内の VBA ソース ファイルだけでなく bin フォルダに格納した Office ファイルも一緒に、リポジトリにコミットしておいた方がよいかもしれません。

さて、つらつらと解説して参りましたが、vbac は我ながら本当に便利なツールに仕上がっていると思います。VBAer 必携のツールです。ぜひ使ってみてください。

余談。

実は vbac には隠し機能があります。疑り深いあなたのために、combine 時に bin の内容をバックアップするための /binback オプションと、Access ファイルのクエリも管理対象にしたい人のための /incquery オプションです。これらオプションは、実装はされているものの無効化されています。vbac.wsf のソースのずっと下の方で無効化処置が行われていますので、これらを使いたい方は、対応する無効化箇所をコメントアウトして有効化してください。

    // It's guard for internal impl. If necessary, you can comment out to enable this feature.
param.binbak.flag = false;
param.incQuery = false;

/binbak オプションに関して、バックアップは vbac.wsf と同階層の bak フォルダに作成されます。別の場所にバックアップしたい場合は /binbak:tmp などと指定することもできます。

Ariawase v0.6.0 解説(ライブラリ概要編)

0
0

Ariawase はフリーな VBA ライブラリです。貧弱な VBA をもうすこしまともに扱うために開発されました。足りない組み込みライブラリ、標準的な方法が存在しない抽象化機構、そして、やりづらいテストを補うべく機能を提供します。

Ariawase is free library for VBA cowboys.

ライブラリの全体像をまずは一覧表で。計11個のモジュールを、ライブラリの基盤となるものから順に4つの区分、Standard、Advance、Option、Other に分けて以下に示します。

  • Standard
    • Core.bas - 基本モジュール
    • IO.bas - 入出力
  • Advance
    • Ext.bas - 拡張モジュール
    • Assert.bas - ユニット テスト
    • ArrayEx.cls - 自動拡張配列
    • Tuple.cls - タプル
    • Func.cls - 関数ポインタ
  • Option
    • WinInet.bas - ファイル ダウンロード
    • CdoMail.cls - メール送信
    • Resource.bas - リソース
  • Other
    • MonkeyTest.cls - Ariawase 自身のテスト

Ariawase は、すべてのモジュールをいつも使用する必要はありません。自分に必要なモジュールだけを取り出して利用するのでも構いません。その場合は、この区分に沿って取捨選択するとよいでしょう。

最小構成は Standard で、これは常に必要となります。より多くの機能を利用する場合は Standard に加えて Advance を使用します。Option の各モジュールは、それぞれ特定の機能を提供するものですので、必要なものがあれば適宜利用します。Other は、あなたのコードのためには必要とはならないでしょう。Ariawase の操作と動作を確認するために使用できますが、あなたのマクロをリリースするときには、最終的に消されるべきものです。

Standard

Ariawase のもっとも基盤となるモジュール群です。

特に Core.bas は必須のモジュールとして設計されており、多くの基本的な関数や、抽象機能を提供します。Core.bas は、他のモジュールへの依存を持たないモジュールとして設計されており、今後においてもこれは変わることのない仕様です。IO.bas はファイルの入出力機能を提供します。ファイルの入出力が必要なければ削除してもかまいませんが、VBA を書くにあたって Ariawase のようなライブラリを必要とする多くの場合、IO.bas は必要となるでしょう。

Advance

より高度な追加機能を提供します。もっとコード寄りの視点で言えば、Core.bas 以外のモジュールにも依存があるような標準モジュールと、その実現のために必要なクラス モジュールがこれに区分されます。

Ext.bas は、Core.bas よりも高度な機能を提供します。現在は、主に配列操作のための関数群を提供しており、いわゆる関数型言語に見るようなリスト処理(配列処理)を実現します。Assert.bas はユニット テストのためのモジュールです。簡単ながらも必要十分なテスト機能を実装します。

ArrayEx.cls は自動拡張する配列です。Tuple.cls はタプルと呼ばれる、不変な値の組を提供するものです。Func.cls は関数ポインタを扱います。これらクラス モジュールは Ext.bas や Assert.bas から使用されますが、もちろんあなたのマクロのために使用することもできます。

Option

特定機能のためのモジュール群です。その機能の必要に応じて利用します。

WinInet.bas は、インターネット上からファイルをダウンロードする機能を持ちます。CdoMail.cls は、電子メールを送信するだけの機能を持ちます。Resource.bas は、CdoMail.cls で使用する定数値を定義します。

これらモジュールは、Ariawase の将来のバージョンからは削除され、別途提供となるかもしれません。

Other

あなたのマクロのリリースのためには不必要なモジュール群です。

MonkeyTest.cls は、Ariawase 自体のテスト コードです。Ariawase のテストを確認したり、各関数の使用例の確認のために利用できますが、あなたのマクロのリリース時には削除されるべきものです。

バージョンについて

というか今後の展望についてですが。

v0.6.0 というバージョン番号からもお分かりの通り、Ariawase はまだベータ的な位置付けです。と言っても、バグがバンバン残っているというわけではありませんが、まだまだ洗練の余地が、API の破壊的変更の可能性がいくらか残されている状況とお考えください。

v1.0 が出せるまではがんばりたい気持ちもあるのですが、一方で、vbac は別として、Ariawase 本体を活用しているという話はあまり聞かないところでありまして、利用されていないものをメンテし続けるほどのモチベーションはないので、今後 Ariawase が活発に更新されるかどうかは普及する・しないによる、といったところです。

ただまあ普及って言っても僕の閾値は低くて、10人ぐらいの人から「使ってるよ!」って言われたら、その気になって開発し続けるとは思いますけど。そうでない場合も、細々とはメンテし続けようとは思っていますし、特にバグ修正に関しては、見つけ次第、ご報告いただき次第対処していきます。

という感じで。

[C#] COM と RCW と参照カウント

0
0

NOTE: この記事は、当初、ココログの「いげ太のブログ」で公開していたものです。

いたるところで書かれてはいるものの、個人的に、それその記事ひとつで理解できるというようなズバピタな記事がなかったので学習メモ。ていうか、調べてたらキリがなくなってきたので、いったんフィックスしたい。

ランタイム上で暗躍する RCW

.NET から COM オブジェクトを扱うということは、マネージドからアンマネージにアクセスするということであり、すなわちマーシャリングが必要となる。これは、ランタイム呼び出し可能ラッパー(RCW: Runtime Callable Wrapper)なるしくみによって行われる。

図: クライアント --> RCW --> COM オブジェクト

通常、.NET クライアント(アプリケーション)が直接的に扱うのは、生身の COM オブジェクトではなく、そのラッパーたる RCW である。ランタイム(.NET Framework)によってサポートされるため意識しづらいが、RCW というプロキシを挟んで COM オブジェクトにアクセスしているのだ。これは重要な事実であり、認識しておかなければならないことだ。

COM オブジェクトとメモリ

さて、ここで、COM プログラミングの世界をすこしだけのぞいてみる。COM の規定によれば、COM オブジェクトたるもの「自分のメモリは自分で解放する責任がある」とのこと。そして、それを実現するためには、参照カウントというテクニックが用いられる。

「参照カウント」とはすこしあいまいなネーミングだ。どうせなら「被参照数」と呼びたいところ。要するに、参照カウントとは、その COM オブジェクトが他からどれだけ参照されているかを示す数値である。COM オブジェクトは、自分が使ったメモリを自分で解放するために、自身への参照数を自分で管理していて、参照数が "0"になったら他からの参照がなくなった(=用済み)と判断して、自分自身のメモリを解放するのである。

COM オブジェクトは健気な奴で、いらなくなったら、自らの居場所を明け渡すわけだけれど、だけどもそれ故に、いま誰が必要とし、必要としなくなったのかは、使う側から通達しなくてはならない。因果なものだが、ロジカルな帰結だ。いつまでもメモリに居座らせず、任務を終えた COM オブジェクトは解放してやらないといけない。

GC と参照カウント

.NET の世界に戻ろうか。.NET でメモリ管理といえばガベージ コレクション(GC)である。COM オブジェクトを内包する RCW は、マネージドであり、もちろん GC の管理対象だ。しかし、COM オブジェクトは、自分のメモリは自分で解放する責務を負っている。

ここに、メモリ管理のアプローチの違いからくるミスマッチが生まれる。RCW は GC の管理下にあるが、内包する COM オブジェクトが GC の対象でないので、結局、アプリケーション作成者が RCW を通じて COM オブジェクトが使用するメモリを、参照カウントというしくみで管理しなければならない、ということになる。

RCW に宿る参照カウント

前述したように、.NET の世界から COM オブジェクトにアクセスするには、それをラップした RCW を通じて行うのだが、COM オブジェクト上の参照カウントというのは、プライベートなメンバなのである。

つまりどういうことかというと、RCW は、COM オブジェクト上の参照カウントを直接見ることはできないということであり、それはすなわち、RCW 上にも参照カウントを持たなければならない、ということを意味する。見えないのだから、別立てで、擬似的に管理してやるしかないというわけ。

そう、.NET デベロッパーは、この RCW 上の参照カウントを管理してやるのだ。そして、これが非常にめんどくさい。

Excel における COM オブジェクト

ここからは、実例に沿って説明した方がわかりやすいので、みんな大好き Excel くんにご登場願おう。

レガシー VB では、自動参照カウンタなるものが提供されるので、たとえば、hoge.xls を開いて、1 つ目のシートの A1 セルの値を表示しようと思えば、以下のように書けた。VB6 は手元にないので、VBScript / WSH で書いてみる。

Sub ShowRangeA1()
Dim excel
Dim book
Set excel = CreateObject("Excel.Application")
Set book = excel.Workbooks.Open("hoge.xls")

Dim a1val
a1val = book.Sheets(1).Range("A1").Value

book.Close False
excel.Quit

WScript.Echo a1val
End Sub

これは、参照カウントを VBscript 側で勝手に管理してくれるからこそ可能な書き方である。どういうことか。まずは、セル(Range)を扱うまでに必要となる、Excel が内包する COM オブジェクトの階層構造を知らなければならない。

  • Application
  • Workbooks
  • Workbook
  • Worksheets
  • Worksheet
  • Range

Excel アプリケーションがあって、Excel は MDI だからアプリケーションごとに複数のブックを持っていて、その中から必要なブックを指定し、ブックの中には複数のシートがあるから、そこから特定のシートを指定し、そしてシート上の多数のセルから任意のひとつを指定し、その値を得る。セルの値を取ってくるということは、つまりそういうことであり、上記の要素、その一つひとつがすべて COM オブジェクトなのである。

参照カウント プログラミング

そう、上記に挙げた COM オブジェクトごとにそのラッパーである RCW も存在し、そして、それぞれに参照カウントを持っていて、それらすべてを管理しなければいけないのだ。それが、.NET で COM を扱うということなのだ。文章だけではわかりづらい。ともかく、上記のコードを C# で書き換えてみることとしよう。

public void ShowRangeA1()
{
Excel.Application xlApp = new Excel.Application();
Excel.Workbooks xlBooks = xlApp.Workbooks;
Excel.Workbook xlBook = xlBooks.Open(@"hoge.xls",
Type.Missing, Type.Missing, Type.Missing, Type.Missing,
Type.Missing, Type.Missing, Type.Missing, Type.Missing,
Type.Missing, Type.Missing, Type.Missing, Type.Missing,
Type.Missing, Type.Missing);
Excel.Sheets xlSheets = xlBook.Worksheets;
Excel.Worksheet xlSheet = (Excel.Worksheet)xlSheets[1];
Excel.Range xlCells = xlSheet.Cells;
Excel.Range xlRangeA1 = (Excel.Range)xlCells[1, 1];

string a1val = (string)xlRangeA1.Value2;

Marshal.ReleaseComObject(xlRangeA1);
Marshal.ReleaseComObject(xlCells);
Marshal.ReleaseComObject(xlSheet);
Marshal.ReleaseComObject(xlSheets);
xlBook.Close(false, Type.Missing, Type.Missing);
Marshal.ReleaseComObject(xlBook);
Marshal.ReleaseComObject(xlBooks);
xlApp.Quit();
Marshal.ReleaseComObject(xlApp);

MessageBox.Show(a1val);
}

管理するとは、つまり、RCW をすべて変数に保持しておくということであり、そしてその参照カウントをデクリメントするのが Marshal.ReleaseComObject メソッドである。見るからにめんどくさい。処理の本質ではないコード、タイプ量が激増している。

が、これだけでも十分めんどくさいのに、実は、例外発生時にも確実にデクリメントを行うためには、RCW を生成するごとに、try finally してやらなければいけないのである。詳しくはじゃんぬねっとさんの記事を参考にしていただきたいが、こうなると、超絶ネスト構造の一丁上がりである。

そんなの絶対に書きたくない。なにがあろうと絶対にイヤである。しかるに結局、じゃんぬねっとさんが書かれているように、もし COM オブジェクトを扱うのなら、COM 専用の言語で、つまり、自動参照カウンタが提供される言語で実装するのがベターだ、ということになるのである。

他にも実装の方法はいくつかあるようなのだけれど、いまの僕にはちょっとハードルの高いお話になっているので、その辺は今回は割愛ということで。

参考記事

関連記事



Latest Images