Microsoft MVP for .NET(C#)を再々々々受賞しました

今年も受賞で、5年目です。実は今年から受賞分野がC#が.NETに統合されたので、エキスパタイズとしてはfor .NETになります。

会社は第一段階が終わり、といった感じで、それに付随する活動内容としても総まとめみたいなものが多かったかな、といったところでしょうか。今年はまた次の段階の始まりということで、より新しい勝負が必要になってきています。今、私が主に力を入れているのはUnityと、そのReactive Extensions実装のUniRxで、特にUniRxはかなりヒットさせられたとは思います。が、まだまだ兆しといったところなので、確固たるものにしなければならない。また、それを基盤にして、C#の強さというのを、ただの今までの.NETコミュニティにだけに留まらず、幅広い世界に届ける、伝えていきたいし、幸いにして私はそれが出来る立場にいると思っています。

より力強く、Real World C#というのを示し続けてきます。そんなわけで引き続き、今年もよろしくお願いします。

グラニのC#フレームワークの過去と未来、現代的なASP.NETライブラリの選び方

Build Insider MEETUP with Graniというイベントで、グラニのC#フレームワーク(というほどのものはない!)の今までとこれからってのを話しました。

A framework for light up applications of grani from Yoshifumi Kawai

そのうちBuild Insiderで文字起こしとか公開されると思います。

2015年の今、どういうライブラリを選んだか、とかNLog大脱却、とかって話が見どころですかね。うちの考えるモダンなやり方、みたいな感じです。

実際、EventSourceSemantic Logging Application Blockは良いと思いますので、触ってみるといいですね。少なくとも、イマドキにハイパーヒューマンリーダブル非構造化テキストログはないかなぁ、といったところです。

スライドにしたら判別不能になったOWINのStartup部分も置いておきます、参考までに。

// 開発環境用Startup(本番では使わないミドルウェア/設定込み)
public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app = new ProfilingAppBuilder(app); // 内製Glimpse表示用AppBuilderラッパー(Middlewareトラッカー)
        app.EnableGlimpse(); // Glimpse.LightNdoe同梱ユーティリティ
        app.Use<GlobalLoggingMiddleware>(); // 内製ロギングミドルウェア
        app.Use<ShowErrorMiddleware>(); // 内製例外時表示ミドルウェア
 
        app.Map("/api", builder =>
        {
            var option = new LightNodeOptions(AcceptVerbs.Get | AcceptVerbs.Post,
                new LightNode.Formatter.Jil.JilContentFormatter(),
                new LightNode.Formatter.Jil.GZipJilContentFormatter())
            {
                OperationCoordinatorFactory = new GlimpseProfilingOperationCoordinatorFactory(),
                ErrorHandlingPolicy = ErrorHandlingPolicy.ThrowException,
                OperationMissingHandlingPolicy = OperationMissingHandlingPolicy.ThrowException,
            };
 
            builder.UseLightNode(option);
        });
        // Indexはデバッグ画面に回す
        app.MapWhen(x => x.Request.Path.Value == "/" || x.Request.Path.Value.StartsWith("/DebugMenu"), builder =>
        {
            builder.UseFileServer(new FileServerOptions()
            {
                EnableDefaultFiles = true,
                EnableDirectoryBrowsing = false,
                FileSystem = new PhysicalFileSystem(@".\DebugMenu"),
            });
        });
        // それ以外は全部404
        app.MapWhen(x => !x.Request.Path.Value.StartsWith("/Glimpse.axd", StringComparison.InvariantCultureIgnoreCase), builder =>
        {
            builder.Run(ctx =>
            {
                ctx.Response.StatusCode = 404;
                return Grani.Threading.TaskEx.Empty;
            });
        });
    }
}

インデックスでアクセスすると表示するページはGlimpse.axdと、シングル全画面ページで表示できるローンチ部分へのリンクを貼っつけてあります。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Debug Index</title>
</head>
<body>
    APIのデバッグ<br />
    <p>
        <a href="../../Glimpse.axd?n=glimpse_redirect_popup">Glimpse Launch</a>
    </p>
    <p>
        <a href="../../glimpse.axd">Glimpse Config</a>
    </p>
</body>
</html>

まぁ、こういうのあると、Glimpseへのアクセスが近くで非常に便利です。

あと最後に、OWINでやるならこーいうのどうでしょう、というWeb.config。Owin Middlewareと機能重複して鬱陶しいからHttpModule丸ごと消そうぜ、という過激派な案ですにゃ。

<?xml version="1.0" encoding="utf-8"?>
 
<!-- OWIN向けウェブコン -->
<!-- Glimpse系のはリリース時にはxsltでまるっと消す -->
<configuration>
    <configSections>
        <section name="glimpse" type="Glimpse.Core.Configuration.Section, Glimpse.Core" />
    </configSections>
    <connectionStrings configSource="<!-- 接続文字列は外部に回す(DebugとReleaseでxsltで変換して別参照見るように) -->" />
    <appSettings>
        <!-- なんかここに書いたり外部ファイルとmergeしたり:) -->
    </appSettings>
    <system.web>
        <!-- system.web配下のは片っ端から消してしまう -->
        <httpModules>
            <clear />
            <add name="Glimpse" type="Glimpse.AspNet.HttpModule, Glimpse.AspNet" />
        </httpModules>
        <httpHandlers>
            <clear />
            <add path="glimpse.axd" verb="GET" type="Glimpse.AspNet.HttpHandler, Glimpse.AspNet" />
        </httpHandlers>
        <roleManager>
            <providers>
                <clear />
            </providers>
        </roleManager>
        <customErrors mode="Off" />
        <trace enabled="false" />
        <sessionState mode="Off" />
        <httpRuntime targetFramework="4.5" requestPathInvalidCharacters="" />
        <globalization culture="ja-jp" uiCulture="ja-jp" />
        <!-- リリース時にxsltでfalseにする -->
        <compilation debug="true" />
    </system.web>
    <system.webServer>
        <validation validateIntegratedModeConfiguration="false" />
        <globalModules>
            <clear />
        </globalModules>
        <modules>
            <!-- モジュールも全消し -->
            <remove name="OutputCache" />
            <remove name="Session" />
            <remove name="UrlRoutingModule-4.0" />
            <!-- 以下デフォで読まれるモジュール名が延々と続く(system.webServer下は一括clearが使えなくて辛い)... -->
            <add name="Glimpse" type="Glimpse.AspNet.HttpModule, Glimpse.AspNet" preCondition="integratedMode" />
        </modules>
        <handlers>
            <add name="Glimpse" path="glimpse.axd" verb="GET" type="Glimpse.AspNet.HttpHandler, Glimpse.AspNet" preCondition="integratedMode" />
        </handlers>
    </system.webServer>
    <!-- おまじない(笑)セクション -->
    <system.net>
        <connectionManagement>
            <add address="*" maxconnection="1024" />
        </connectionManagement>
        <settings>
            <servicePointManager expect100Continue="false" useNagleAlgorithm="false" />
        </settings>
    </system.net>
    <!-- WebServiceでやるならPersistResultsで(当然このセクションもリリースでは消す) -->
    <glimpse defaultRuntimePolicy="PersistResults" endpointBaseUri="~/Glimpse.axd">
        <tabs>
            <ignoredTypes>
                <add type="Glimpse.AspNet.Tab.Cache, Glimpse.AspNet" />
                <add type="Glimpse.AspNet.Tab.Routes, Glimpse.AspNet" />
                <add type="Glimpse.AspNet.Tab.Session, Glimpse.AspNet" />
                <add type="Glimpse.Core.Tab.Trace, Glimpse.Core" />
            </ignoredTypes>
        </tabs>
        <runtimePolicies>
            <ignoredTypes>
                <add type="Glimpse.Core.Policy.ControlCookiePolicy, Glimpse.Core" />
                <add type="Glimpse.Core.Policy.StatusCodePolicy, Glimpse.Core" />
                <add type="Glimpse.Core.Policy.AjaxPolicy, Glimpse.Core" />
                <add type="Glimpse.AspNet.Policy.LocalPolicy, Glimpse.AspNet" />
                <add type="Glimpse.Core.Tab.Trace, Glimpse.Core" />
            </ignoredTypes>
        </runtimePolicies>
    </glimpse>
</configuration>

Web API的なサービスでもGlimpse使えるよ!ってのはもっと知ってほしいかしらん。その辺はLightNode 1.0、或いはWeb APIでのGlimpseの使い方で詳しく解説しています。

Comment (2)

匿名 : (03/26 09:00)

認証とか認可周りはどうしてるんですか??

neuecc : (03/26 11:21)

既存ソーシャルゲームに関しては、GREEのOAuth認証ですね(TwitterやGoogleのOAuth認証みたいなもの)
というのを、自家製のAsyncOAuthをカスタムしたものでやってます。
(というのもGREEのシステムが若干バギーで仕様に従ってない形で来たりするので、特別対応がどうしても必要で)

Trackback(0)
Write Comment

LightNode 1.0、或いはWeb APIでのGlimpseの使い方

こないだ、RedisクライアントのCloudStructuresを1.0にしたばかりですが、今回は大昔に作った自作Web APIフレームワークのLightNodeを1.0にしました。なんでドタバタやってるのかというと、.NET XRE(ASP.NET vNext)を様子見してたんですが、そろそろ今年一年どうしていくかの態度を決めなければならなくて、結論としては、OWINで行くことにしたからです。ちゃんちゃん。その辺の理由なんかは後ほど。

さて、Glimpseです。なにはなくともGlimpseです。イマドキでC#でウェブ作るんなら、まずはGlimpse入れましょう。絶対必須です。使ったことないんなら今すぐ使ってください。圧倒的なVisual Profiling!ボトルネックが一目瞭然。コンフィグも一覧されるので、普段気にしていなかったところも丸見え。データアクセスが何やってるかも一発で分かる。ちなみに、競合としては昔あったMiniProfilerは窓から投げ捨てましょう。ASP.NET開発はもはやGlimpse以前と以後で分けられると言っても過言ではない。

で、LightNode 1.0です。変更点はGlimpseにフル対応させたことで、ついでに細かいとこ直しまくりました、と。ともあれGlimpse対応が全てです。

で、作ってる間にGlimpseをWeb API(ASP.NET Web APIとは言ってない)系で使ったり、Owinと合わせて使ったりすることのノウハウも溜まったので、LightNodeの話というかは、そっちのことを放出したいな、というのがこの記事の趣旨ですね!

OwinでGlimpseを使う

Glimpse自体はOwinに対応していません。勿論、vNextへの対応も含めてSystem.Webへの依存を断ち切ろうとしたGlimpse v2の計画は随分前から始まっているんですが、Issueをずっと見ている限り、かなり進捗は悪く、難航しているようです。正直、いつ完了するか全く期待持てない感じで、残念ながら待っていても使えるようにはなりません。

しかし、そもそもGlimpseのシステムはただのHttpModuleとHttpHandlerで動いています。つまり、Microsoft.Owin.Host.SystemWebでホストしている限りは、Owinであろうと関係なく動きます。動くはずです。実際Glimpse.axdにアクセスすれば表示されるし、一見動いています。そしてGlimpseにはページ埋め込みの他、Standaloneでの起動が可能(Glimpse.axdでの右側)なのでそこから起動すると……

いくらOwinでページ作ってアクセスしても何も表示されません、データがHistoryに蓄積されません。これにめっちゃハマって以前は諦めたんですが、今回LightNodeをOwinに何が何でも対応させたくて改めて調べた結果、対策分かりました。原因としては、Glimpseはリクエストの完了をPostReleaseRequestStateで受け止めているんですが、Microsoft.Owin.Host.SystemWebでホストしてOwinによるリクエストハンドリングでは、完了してもPostReleaseRequestStateが呼ばれません。結果的にOwinでふつーにやってる限りではGlimpseでモニタできない。

対策としては、単純に手動でEndRequestを叩いてやればいいでしょう。Middlewareを作るなら

public Task Invoke(IDictionary<string, object> environment)
{
    return next(environment).ContinueWith((_, state) =>
    {
        ((state as HttpContext).Application["__GlimpseRuntime"] as IGlimpseRuntime).EndRequest();
    }, System.Web.HttpContext.Current);
}

ということになります。このMiddlewareを真っ先に有効にしてやれば、全てのOwinパイプラインが完了した際にEndRequestが叩かれる、という構造が出来上がります。System.Webをガッツリ使ったMiddlewareなんて気持ち悪いって?いやいや、まぁいーんですよ、そもそもGlimpseがSystem.Webでしか現状動かないんだから、ガタガタ言うでない。

さて、LightNodeのGlimpse対応DLLにはこのMiddlewareを最初から同梱してあります。LightNodeでGlimpse対応のConfigurationを書く場合は、以下のようになります。

public void Configuration(Owin.IAppBuilder app)
{
    app.EnableGlimpse();
    app.MapWhen(x => !x.Request.Path.Value.StartsWith("/glimpse.axd", StringComparison.OrdinalIgnoreCase), x =>
    {
        x.UseLightNode(new LightNodeOptions()
        {
            OperationCoordinatorFactory = new GlimpseProfilingOperationCoordinatorFactory()
        });
    });
}

まずEnableGlimpse、これが先のEndRequestを手動で叩くものになってます。次にMapWhenで、Glimpse.axdだけOwinパイプラインから外してやることで、LightNodeと共存させられます!ついでに、LigthNodeでのGlimpseモニタリングを有効にする場合はGlimpseProfilingOperationCoordinatorFactoryをOptionに渡してあげれば全部完了。

LightNode+GlimpseによるWeb APIモニタリング

何ができるようになるの?何が嬉しいの?というと、勿論当然まずはTimelineへの表示。

フィルター(Before/After)とメソッド本体がTimeline上で見えるようになります。これは中身何もないですが、勿論DatabaseやRedis、Httpアクセスなどがあれば、それらもGlimpseは全部乗っけることができるし、それらをWeb APIでも見ることができる。圧倒的に捗る。

そしてもう一つがLightNodeタブ。

一回のリクエストのパラメータと、戻り値が表示されます。API開発の辛さって、戻り値が見えない(クライアント側でハンドリングして何か表示したりするも、領域的に見づらかったりする)のが結構あるなーって私は思っていて、それがこのLightNodeタブで解消されます。ちなみにもし例外があった場合は、ちゃんと例外を表示します。

また、ExecutionのPhaseが以降はすべてExceptionになってるので、フィルターが遠ったパスも確認しやすいはずです。

Web APIのためのGlimpseコンフィグ

Web APIのためにGlimpseを使う場合、ふつーのWeb用のコンフィグだと些か不便なところがあるので、調整したほうがいいでしょう。私のお薦めは以下の感じです。

<!-- GlimpseはHUDディスプレイ表示のためなどでレスポンスを書き換えることがありますが、勿論APIには不都合です。
     デフォルトはPersistResults(結果のHistory保存のみ)にしましょう -->
<glimpse defaultRuntimePolicy="PersistResults" endpointBaseUri="~/Glimpse.axd">
    <tabs>
        <ignoredTypes>
            <!-- OWINで使うならこれらは不要でしょう、出てるだけ邪魔なので消します -->
            <add type="Glimpse.AspNet.Tab.Cache, Glimpse.AspNet" />
            <add type="Glimpse.AspNet.Tab.Routes, Glimpse.AspNet" />
            <add type="Glimpse.AspNet.Tab.Session, Glimpse.AspNet" />
        </ignoredTypes>
    </tabs>
    <runtimePolicies>
        <ignoredTypes>
            <!-- クライアントがクッキー使うとは限らないので、無視しましょう、そうしないとHistoryに表示されません -->
            <add type="Glimpse.Core.Policy.ControlCookiePolicy, Glimpse.Core" />
            <!-- 404とかもAPIならハンドリングして表示したい -->
            <add type="Glimpse.Core.Policy.StatusCodePolicy, Glimpse.Core" />
            <!-- Ajaxじゃないなら -->
            <add type="Glimpse.Core.Policy.AjaxPolicy, Glimpse.Core" />
            <!-- リモートで起動(APIならそのほうが多いよね?)でも有効にする -->
            <add type="Glimpse.AspNet.Policy.LocalPolicy, Glimpse.AspNet" />
        </ignoredTypes>
    </runtimePolicies>
</glimpse>

defaultRuntimePolicyと、そして特にControlCookiePolicyが重要です。利用シチュエーションとしてStandalone Glimpseで起動してHistoryから結果を見る、という使い方になってくるはずなので(というかWeb APIだとそうしか方法ないし)、Cookieで選別されても不便すぎるかな、ブラウザからのAjaxならともかくモバイル機器から叩かれてる場合とかね。

さて、それは別として、様々なクライアントからのリクエストが混ざって判別できないというのも、それはそれで不便です。これを区別する手段は、あります。それは、クッキーです(笑) 判別用にクッキーでID振ってやるとわかりやすくていいでしょう。例えば以下の様な感じです。

var req = WebRequest.CreateHttp("http://localhost:41932/Member/Random?seed=13");
// "glimpseid" is Glimpse's client grouping key
req.CookieContainer = new CookieContainer();
req.CookieContainer.Add(new Uri("http://localhost:41932"), new Cookie("glimpseid", "UserId:4"));

glimpseidというのがキーなので、例えばそこにユーザーIDとか振っておくと見分けがついてすごく便利になります。

こんな感じです。これはデバッグビルド時のみといった形で、クライアントサイドで埋め込んであげたいですね。

LightNodeを使う利点

というわけでGlimpseとの連携が超強力なわけですが、LightNode自体はまず言っておくと、誰にでも薦めはしません。この手のフレームワークで何より大事なのが標準に乗っかることです。C#での大正義はASP.NET Web APIです、そこは揺るぎません。その上でLightNodeの利点は「シンプルなAPIがシンプルに作れる」「Glimpseによる強力なデバッグ支援」「クライアントコード自動生成」です。特に非公開のインターナルなWeb API層向けですね。反面お薦めしないのは、RESTfulにこだわりたい人です。LightNodeは設計思想として徹底的にRESTfulを無視してるんで、準拠するつもりは1ミリもありません。例えば、インターナルなAPIでRESTfulのために1つのURIを決めるのに3日議論するとか、凄まじく馬鹿げているわけで。LightNodeは悩みを与えません、そもそもメソッド書くしかできないという制約を与えているから。

凝ったルーティングもアホくさい。インターナルなWeb APIで、モバイル機器からのアクセスを前提にすると、クライアントサイドでのAPIライブラリを書くことになりますが、ルーティングが凝っていれば凝っているほど対応が面倒くさいだけ、という。嬉しさなんて0.1ミリもない。結局、ルールはある程度固定のほうが良いんですよ。さすがにパブリックAPIなら長いものに適当に巻かれて適当に誤魔化しますが。

というわけで、どういう人に薦めるかというと「とりあえずサクッとWeb API作りたい人」「モバイルクライアントからアクセスするインターナルなWeb APIを作りたい人」ですかねー。別にパブリックなのも作れないことはないですけど、別にそこまで違和感あるURLになるわけでもないですしね。

ちなみに、MVCとの共存は可能です。例えば

public void Configuration(IAppBuilder app)
{
    app.Map("/api",  x =>
    {
        x.UseLightNode();
    });
}

といった感じにapi以下をLightNodeのパスってことにすればOK。それ以外のパスではASP.NET MVCが呼ばれます。ルートが変わるだけなので、他のコンフィグは不要です。あんまり細かくゴチャゴチャやると辛いだけなので、このぐらいにしておくのがいいですね。ちなみに、Owinで困るのはHttpModuleとの共存だったりします。実行順序もグチャグチャになるし(一応、少しはOwin側でコントロールかけられますが、辛いしね)同じようなものが複数箇所にあるというのは、普通にイけてない。これはMiddlewareのほうに寄せていきたいところ。脱HttpModule。

まとめ

あ、で、OWINな理由って言ってませんでしたっけ。なんかねー、XREは壮大すぎて危険な香りしかしないんですよ。少なくとも、今年の頭(今)に、今年に使う分のテクノロジーを仕込むには、賭けられないレベルで危なっかしい。Previewで遊びながら生暖かく見守るぐらいがちょうどいいです。まあ、アタリマエだろっていえばアタリマエ(ベータすら出てないものを実運用前提で使い出すとかマジキチである)ですけどね、だから別にXREがダメとかそういう話じゃないですよ。むしろXREはまだ評価できる段階ですらないし。

で、今は過渡期で宙ぶらりんなのが凄く困る話で、そのブリッジとしてOWINはアリかな、と。OWIN自体の未来は、まぁASP.NET 5はどうしてOWIN上に乗らなかったのかにあるように、Deadでしょう。しかし、今から来年の分(XREが実用になった世代)を仕込むには、System.Webへの依存の切り離しや、Owin的なパイプラインシステムへの適用は間違いなく重要。OWINならコーディングのノリもASP.NET 5と変わらないしコードの修正での移行も容易になる、最悪互換レイヤーを挟んで適用できるので、「今」の選択としては、消極的にアリです。

ASP.NET Web APIは、うーん、ASP.NET MVCとの統合が見えてる今、改めて選びたくない感半端ないんだよねぇ。GlimpseはASP.NET Web API対応しないの?というと、そういう話もあるにはあったようですが、色々難航していて、PullRequestで物凄く時間かけて(70レス以上!一年近く!)、それでも結局取り込まれてないんですよ。ここまで来るともはやGlimpse v2でのvNext対応でMVC統合されてるんだからそれでいいじゃん、に落ち着きそうで、恐らくもう動きはないでしょう。とか、そういう周辺のエコシステムの動きも今のASP.NET Web APIは鈍化させる状況にあるわけで、あんまポジティブにはなれないなぁ。とはいえ、現状のスタンダードなWeb API構築フレームワークとして消極的にアリ、と言わざるをえないけれど。ちなみにNancyは個人的には全くナシです、あれのどこがいいのかさっぱりわからない。

Glimpseの拡張は、ちょうど社内用拡張も全部書き換えたりして、ここ数日でめちゃくちゃ書きまくったんで、完全に極めた!うぉぉぉぉ、というわけで拡張ガイダンスはいつかそのうち書くかもしれませんし、多分書きません。つーかGlimpseちゃんと日本の世の中で使われてます?大丈夫かなー、さすがにGlimpseは圧倒的に良いので標準レベルで使われなければならないと思うのですけれど。

あー、で、LightNodeは、まあ良く出来てますよ、用途の絞り方というか課題設定が明確で、実装もきっちりしてありますし。うん、私は好きですけど(そりゃそうだ)、人に薦めるかといったら、Microsoftの方針がOwin的なオープンの流れから、やっぱり大Microsoft的なところに一瞬で戻ったりしてるんで(Hanselmanには少し幻滅している)、まぁ長いものには巻かれておきましょう。

CloudStructures 1.0 - StackExchange.Redis対応、RedisInfoタブ(Glimpse)

CloudStructures、というRedisライブラリを以前に作ってたわけなのですが(CloudStructures - ローカルとクラウドのデータ構造を透過的に表現するC# + Redisライブラリ)、2013年末にGlimpseプラグインを追加してから一切音沙汰がなかった。私お得意の作るだけ作って放置パターンか!と思いきや、ここにきて突然の大更新。APIも破壊的大変更祭り。バージョンもどどーんと上げて1.0。ほぅ……。

一番大きいのが、ベースにしてるライブラリがBookSleeveからStackExchange.Redisになりました。StackExchange.RedisはBookSleeveの後継で、そしてAzure Redis Cacheのドキュメントでもマニュアルに使用されているなど、今どきの.NETにおけるRedisクライアントのデファクトの位置にあると言ってよいでしょう。当然、移行する必要があったんですが腰が重くて……。

APIを変えた理由は、以前は「ローカルとクラウドのデータ構造を透過的に表現」というのに拘ってIAsyncCollection的に見せるのに気を配ってたんですが、Redis本来のコマンド表現と乖離があって、かなり使いづらかったんでやめたほうがいーな、と。メソッド名が変わっただけで使い方は一緒なんですが、とりあえずRedisのコマンド名が露出するようになりました。ま、このほうが全然イイですね。抽象化なんて幻想だわさ、特に名前の。

CloudStructuresの必要性

こんな得体のしれない野良ライブラリなんて使いたくねーよ、StackExchange.Redisを生で使えばいいじゃん。と、思う気持ちは至極当然でまっとうな感覚だと思います。私もそう思う。そして、単純なStringGet/Setぐらいしか使わないならそれでOKです、本当にただのキャッシュストアとして使うだけならば。しかし、本気でRedisを使い倒す、本気でRedisの様々なデータ構造を活用していこうとすると、StackExchange.Redisを生で使うのは限界が来ます。戻り値のオブジェクトへのマッピングすらないので、そこら中にSeiralize/Deserializeしなければならなくなる。ADO.NETのDbDataReaderを生で使うようなもので、そうなったら普通はなんかラップするよね?ADO.NETにはDapperのようなMicro ORMからEntity FrameworkのようなフルセットのORMまである。StackExchange.Redisが生ADO.NETを志向するならば(これは作者も言明していて、付随機能は足さない方針のようです)ならば、そこにO/R(Object/Redis)マッパーが必要なのは自然のことで、それがCloudStructuresです。

CloudStructuresが提供するのは自動シリアライズ/デシリアライズ、キーからの分散コネクション(シャーディング)、コマンドのロギング、Web.configからの接続管理、そしてGlimpse用の各種可視化プロファイラーです。元々、というか今もCloudStructuresはうちの会社でかなりヘヴィに使ってて(このことは何度か記事でも推してます、技評のグラニがC#にこだわる理由とか)、コマンドのロギングとかは執拗に拘ってます。今回はそうした長い利用経験から、やっぱイケてない部分も沢山あったので徹底的に見直しました。

シャーディングは、StackExchange.RedisはそもそもConnectionMultiplexerという形で内部で複数の台への接続を抱えられるんですが、これはどちらかというと障害耐性的な機能(Master/Slaveや障害検知時の自動昇格など)が主なので、Memcached的なクライアントサイドでの分散はBookSleeveの時と変わらず持っていません。なので引き続きシャーディングはCloudStructures側の機能として提供しています。

そもそもRedisが必要かどうかだと、んー、私としては規模に関わらず絶対に入れたほうがいいと思ってます。RDBMSの不得意なところを綺麗に補完できるので、RDBMSだけで頑張るよりも、ちょっとしたとこに使ってやると物凄く楽になると思います。導入もAzure Redis CacheやAWSのElastiCache for Redisのようにマネージドのキャッシュサービスが用意されているので、特にクラウド環境ならば簡単に導入できますしね。

使い方の基本

RedisSettingsまたはRedisGroupを保持して、各データ構造用のクラスをキー付きで作って、メソッド(全部async)を呼ぶ、です。

// 設定はスタティックに保持しといてください
public static class RedisServer
{
    public static readonly RedisSettings Default = new RedisSettings("127.0.0.1");
}
 
// こんなクラスがあるとして
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
 
// RedisStringという型を作って... (RedisSettings.Default.String<Person>("key")でも作れます)
var redis = new RedisString<Person>(RedisServer.Default, "test-string-key");
 
// コマンドをがしがし呼ぶ、何があるかはIntelliSenseで分かる
await redis.Set(new Person { Name = "John", Age = 34 });
 
// 取得もそのまま呼ぶ 
var copy = await redis.Get();
 
// Listも同じ感じ
var list = new RedisList<Person>(RedisServer.Default, "test-list-key");
await list.LeftPush(new[] { new Person { Name = "Tom" }, new Person { Name = "Mary" } });
 
var persons = await list.Range(0, 10);

難しいところはなく、割と直感的。StackExchange.Redisのままだと、特にListが辛かったりするんで、相当楽になれるかな。

メソッド名は基本的にStackExchange.Redisのメソッド名からデータ構造名のプリフィックスとAsyncサフィックスを抜いたものになってます。例えばSetAddAsync()はRedisSet.Add()になります。SetAddなんて最悪ですからね、そのまんま扱いたくない名前。Asyncをつけるかどうかはちょっと悩ましかったんですが、まぁ全部Asyncだしいっか、と思ったんで抜いちゃいました。

他の特徴としては全てのセット系のメソッドにRedisExpiryという引数を足してます。これは、SetのついでにExpireを足すって奴ですね。標準だとStringSetぐらいにしかないんですが、元々Redisは個別にExpireを呼べば自由につけれるので、自動でセットでつけてくれるような仕組みにしました。なんだかんだでExpireはつける必要があったりして、今までは毎回Task.WhenAllでまとめててたりしてたんですがすっごく面倒だったので、これで相当楽になれる、かな?

RedisExpiryはTimeSpanかDateTimeから暗黙的変換で生成されるので、明示的に作る必要はありません。

var list = new RedisList<int>(settings);
await list.LeftPush(1, expiry: TimeSpan.FromSeconds(30));
await list.LeftPush(10, expiry: DateTime.Now.AddDays(1));

こんな感じ。これは、StackExchange.RedisがRedisKey(stringかbyte[]から暗黙的に変換可能)やRedisValue(基本型に暗黙的/明示的に変換可能)な仕組みなので、それに似せてみました。違和感なく繋がるのではないかな。

コンフィグ

Web.configかapp.cofingから設定情報を引っ張ってこれます。トランスフォームとかもあるので、なんのかんのでWeb.configは重宝しますからねー、あると嬉しいんじゃないでしょうか。

<configSections>
    <section name="cloudStructures" type="CloudStructures.CloudStructuresConfigurationSection, CloudStructures" />
</configSections>
 
<cloudStructures>
    <redis>
        <group name="Cache">
            <!-- Simple Grouping(key sharding) -->
            <add connectionString="127.0.0.1,allowAdmin=true" db="0" />
            <add connectionString="127.0.0.1,allowAdmin=true" db="1" />
        </group>
        <group name="Session">
            <!-- Full option -->
            <add connectionString="127.0.0.1,allowAdmin=true" db="2" valueConverter="CloudStructures.GZipJsonRedisValueConverter, CloudStructures"  commandTracer="Glimpse.CloudStructures.Redis.GlimpseRedisCommandTracer, Glimpse.CloudStructures.Redis" />
        </group>
    </redis>
</cloudStructures>

こんな感じに定義して、

public static class RedisGroups
{
    // load from web.config
    static Dictionary<string, RedisGroup> configDict = CloudStructures.CloudStructuresConfigurationSection
        .GetSection()
        .ToRedisGroups()
        .ToDictionary(x => x.GroupName);
 
    // setup group
    public static readonly RedisGroup Cache = configDict["Cache"];
    public static readonly RedisGroup Session = configDict["Session"];
 
    static RedisGroups()
    {
        // 後述しますがGlimpseのRedisInfoを有効にする場合はここで登録する
        Glimpse.CloudStructures.Redis.RedisInfoTab.RegisiterConnection(new[] { Cache, Session });
    }
}

こんな風にstatic変数に詰めてやると楽に扱えます。

シリアライズ

CloudStructuresは基本型(int, double, string, etc)はそのまま格納し、オブジェクトはシリアライザを通します。シリアライザとして標準ではJSONシリアライザのJilを使っています。理由は、速いから。JilはJSON.NETと違って、JsonReader/Writerも提供しないし、複雑なカスタムオプションもフォールバックもありません(多少の(特にDateTime周りの)オプションはありますが)。単純にJSONをシリアライズ/デシリアライズする、もしくはdynamicで受け取る。それだけです。まぁ、CloudStructuresの用途には全然合ってる。

以前はprotobuf-netを使っていたんですが、今後はやめようと思ってます。理由は、DataMemberをつけて回るのが面倒だから、ではなくて、空配列/空文字列/nullのハンドリングが凄く大変だったり(ネストしたオブジェクトの空配列がデシリアライズしたらnullになってた、とかね……これは正直ヤバすぎた)、バージョニング(特にEnumの!)が辛かったり、型がないとデシリアライズできないのでちょっとしたDumpすらできなかったりと、実運用上クリティカルすぎる案件が多くてそろそろもう無理。

かわりに、ではないですが圧縮することを提案します。CloudStructuresは標準でGZipJsonRedisValueConverterというものも用意していまして、それに差し替えることでJSONをGZipで圧縮して格納/展開します。圧縮は、特にデカい配列を突っ込んだりするときに物凄く効きます。めちゃくちゃ容量縮みます。protobufにせよmsgpackにせよ、シリアライザは圧縮、ではないんで、バイナリフォーマットとして小さくはなっても、配列にたいしてめちゃくちゃ縮むとかそういうことは起こり得ません(勿論、別にmsgpack+GZipとか併用するのは構わないけれど)。

圧縮の欠点は圧縮なんで、圧縮/解凍にそれなりにパフォーマンスを取られること。と、いうわけでCloudStructuresではLZ4で圧縮するものも用意しました。LZ4はfastest compression algorithmということで、GZipと比べて数倍、圧縮/解凍が速い、です(ただしサイズ自体はGZipよりは縮まない)。この手の用途ではかなり適しているかなー、と。LZ4のライブラリはLZ4 for .NETを用いてます。

インストールはNuGetから入れてもらった後に、LZ4JsonRedisValueConverterに差し替えるだけ。

ふつーはそのまま生JSON、気にしたいけど色々入れたくない人はGZip、エクストリームに頑張ってみたい人はLZ4を選べばいいと思います。更にもっとやりたい人はObjectRedisValueConverterBaseを継承して、自作のRedisValueConverterを作ってみてくださいな。

Glimpseプラグイン

もはやASP.NET開発でGlimpseは絶対に欠かせません。使わないのはありえないレベル。あ、MiniProfilerはもういらないので投げ捨てましょう。というわけでCloudStructuresはGlimpse用のプラグインをしっかり用意してあります。相当気合入れて作りこんであるので、これのためにもRedis使うならCloudStructuresで触るべき、と言えます。マジで。

インストールはNuGetから本体とは別に。それとGlimpseを使う場合は、commandTracerにGlimpseRedisCommandTracerを渡しておいてあげてください。またRedisInfoで情報を出す場合、接続文字列でallowAdminをtrueにしておく必要があります。

<add connectionString="127.0.0.1,allowAdmin=true" db="0" commandTracer="Glimpse.CloudStructures.Redis.GlimpseRedisCommandTracer, Glimpse.CloudStructures.Redis" />

まず、Timeline。

コマンドの並列実行具合がしっかりタイムラインで確認できます。

Redisタブ。

コマンド名、キー名、送受信オブジェクトのダンプとサイズ、Expire時間と処理にかかった時間、そしてキーとコマンドで重複して発行してたら警告。これを見れば一回のページリクエストの中でどうRedisを使ったかが完全に分かるようになってます。不足してる情報は一切なし、とにかく全部出せる仕組みにしました。

最後にRedisInfoタブ。RedisInfoタブを使うには、最初に言ったallowAdmin=trueにすることと、もう一つ、最初に情報表示に使うRedisGroupを登録しておく必要があります。

Glimpse.CloudStructures.Redis.RedisInfoTab.RegisiterConnection(new[] { Cache, Session });

ServerInfoからCmdStat、コンフィグ、クライアント側のコンフィグやコネクションの状態を全部表示します。全部。全部。出せそうな情報は全部収集してきてます。こういうの何気に結構地味に相当大事だったりしますのよ、特にRedisサーバーの情報やコンフィグなんて普段は見ないですからね、こうして超絶カジュアルに見れるっての、かなりありがたい。

まとめ

そんなわけで凄く良くなったんで、かなりお薦めデス。ネーミングが直球なものしか付けないことの多い私にしては、CloudStructuresってライブラリ名はかなりカッコイイという点でもお薦めですね!

問題は旧CloudStructuresとの互換性が、かなり無いので既に使ってる場合は移行が大変ってことデスネ。うちはどうしたんだって?移行してないよ!どーしようかなぁ、うーん、そこはちょっとかなり悩ましい……。

まぁCloudStructuresを使うかどうかはともかくとして、RedisはC#界隈でももっとばんばん使われて欲すぃですねー、そして使うならStringGet/Setだけじゃもったいない。

Comment (2)

mat : (03/12 10:35)

Neue さん、こんにちは。
Redis ライブラリを探していて CloudStructures を見つけました。
「キャッシュストアとして使うだけならば。しかし、本気でRedisを使い倒す、本気でRedisの様々なデータ構造を活用していこうとする」というところに感銘を受けました。
利用したいのですが、利用方法がいまひとつ分かりません。
お手数をおかけしますが、動作可能サンプルプロジェクトなどありましたら GitHub にあげていただけないでしょうか?
以上、よろしくお願い致します。

neuecc : (03/16 17:43)

どうもです。
一応、CloudStructuresないにDemo.Mvcというプロジェクトがありますー。
https://github.com/neuecc/CloudStructures/tree/master/CloudStructures.Demo.Mvc

Web.configでのコンフィグ設定
https://github.com/neuecc/CloudStructures/blob/master/CloudStructures.Demo.Mvc/Web.config#L17

コード側での設定の保持
https://github.com/neuecc/CloudStructures/blob/master/CloudStructures.Demo.Mvc/RedisGroups.cs

とりあえず叩いてみた
https://github.com/neuecc/CloudStructures/blob/master/CloudStructures.Demo.Mvc/Controllers/HomeController.cs

になりますー。
ご不明な点などありましたら、どうぞどうぞ。

Trackback(0)
Write Comment

Open on GitHub - Visual StudioからGitHubのページを開くVS拡張

を、作りました。

機能は見たまんま?です。ソースコード上で右クリックすると「Open on GitHub」メニューが出るので、そこからmasterかbranchかrevisionを選ぶと、該当のGitHubのブロブページが開きます。便利。

インストールはVisual Studio Galleryからどうぞ。例によってソースコードはGitHubで公開しています。

How to make VSIX

VS拡張はドキュメントがあるんだかないんだか、一応あるんですけど、どうも取っ付きが悪いのが難点。今回はWalkthrough: Adding a Submenu to a Menuをベースに弄ってます。といってもやることは簡単なので、そんな大したことはないですが。

まず、メニュー系は全部vsctというクソ書きづらいXMLを弄って作っていきます。テンプレートは「Visual Studio Package」でウィザードで「Menu Command」を選んどくといいでしょふ、というかそれ以外だと詰む。で、vsctのうち

<Group guid="guidOpenOnGitHubCmdSet" id="ContextMenuGroup" priority="0x0600">
    <Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_CODEWIN" />
</Group>

Parentを「guidSHLMainMenu, IDM_VS_CTXT_CODEWIN」にするとエディタのコンテキストメニューに出てきます。あとはまぁ、適当にどうぞ。OpenOnGitHub.vsctOpenOnGitHubPackage.csが全て。分かれば難しくない、分かるまでがダルい。

と、ここまでが普通の感じなんですがVSCT(Visual Studio Command Table)は闇が深くて、IDM_VS_CTXT_CODEWINだとcshtmlとかjsonとかcssでは出てきません!これは別のParentを設定する必要があります。しかも、そのGUIDとかはノーヒント……。既存の拡張を観て研究してもいいんですが、本質的にはUsing EnableVSIPLogging to identify menus and commands with VS 2005 + SP1の記事にある、レジストリ弄ってEnableVSIPLoggingをオンにして、直接対象ウィンドウのGUIDとCmdIdを取得するほうがいいかと思われます。取得したIDとかの使い方はOpenOnGitHub.vsctに載ってるので興味ある人は見てくださいな。

あと、Gitの解析にlibgit2を使っているんですが、VSIXでネイティブバイナリを同梱するためにcsprojに

<Content Include="NativeBinaries\amd64\git2-91fa31f.dll">
  <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  <IncludeInVSIX>true</IncludeInVSIX>
</Content>

といったようにIncludeInVSIXをつけなきゃいけないとか、VSIX自体の署名を切らないといけない(テンプレートから作ると入ってるのでカットする)とか、細かいのをこなしていけば出来上がり!

最近のWindowsでGit

SourceTreeがゴミクズすぎて困る。ので、最近はVSのGit使ってたりします。割といいです。

コミットウィンドウが切り離せることに気付いてから、切り離して使ってます。そうするとまぁまぁコミットしやすい。Commit and Syncはなんのかんのいってベンリだし、SourceTreeよりもPushPullも気持ち早い。DiffとかがVS上で行えるのはサイキョーなので、あとはツリー表示さえあれば完璧だなぁ。ともあれ、VSのGit、いいです。見直しましょう。とにかくSourceTreeは使っててストレスで禿げるのでメインVS、サブSourceTreeぐらいの感じが今のとこ一番いい。

まとめ

ともあれOpen on GitHubはマジベンリ。うちの会社はGitHubでリポジトリ管理してるんですが、いっつも社内チャットに貼り付けるURLとか探してくるのひぢょーにダルくて、ずっと欲しかったのよね。やっと重い腰を上げて作りました。ほんとベンリ。もっと早くに作っておけば良かった。

2014年を振り返る

振り返るシリーズ第三弾。12/30日にやってるのは誕生日なので、まぁ今年もそれで、と。今年はグラニ設立2年目になったわけですが、去年のまとめでは

ここ数年は、毎年ジェットコースター状態で目まぐるしく変化していて。けれど、大きな目標からはブレないで、年々近づけている気がします。一番最初に若くない人サイドに入ったとか、新陳代謝とか言いましたが、来年はそういうことが起こる状態を作っていきたいですね。C#が、若い人がこぞって使うような言語になってればいい、と。そのためにできること。人がすぐに思い浮かべられる、メジャーなアプリケーションの創出と、C#による圧倒的な成果、C#だからこその強さ、というのを現実に示していくこと。雇用の創出、の連鎖。

なるほど。達成度でいうと、今年は残念ながら弱いかなぁ、うむむむ。そこに向けて突き進んでいるというのは変わらないのですが、来年に向けての準備といった感になってしまったかも。あとは、どちらかというと一年目の総まとめみたいな感じ。一番大きなセッションはAWS Summit Tokyo 2014での発表、AWS + Windows(C#)で構築する.NET最先端技術によるハイパフォーマンスウェブアプリケーション開発実践かな?一端の成果を示したうえで、次のステップへ、といったような。

C#

今年は大分記事数少なめになってしまってます!過去最小かも。かわりにライブラリは過去最多で作ったかもしれません。

去年に引き続き、前半はLightNodeの作成続き。OWIN上に作られたMicro RPC/REST フレームワーク。コンセプトはいいと思うし実装もかなりいいと思うし、既にプロダクションに突っ込んで稼働してるんで、ちゃんと使えるし作って良かったとは思ってます。ASP.NET MVC 6でAction FilterがOWIN風デリゲートチェーンになってるのなんかはLightNodeでは最初からそうしてるし、絶対そのほうがいいでしょドヤァ、言ったとおりでしょ!といった先見の明もある!が、しかし、コミット止まって完全に息切れしてますね(笑)

というのも、うーん、まぁ去年後半から今年前半にかけてはOWINへの傾倒もあったのですけれど、ASP.NET vNextがね……。アレによって完全にOWIN無価値になりましたから。思想的/コードのふいんき的な面では親しいところがあるので、今やるならOWINベースで書くのは良いと思ってます。そうすればvNextへの「移植」が容易になりますから。でも、移植なんですよね、そのまま持ってく(一応互換レイヤーで持ってけますが)わけではないところからして、萎える……かなりOne ASP.NET(笑)感があって、割と嫌な気分ですねー。誰かマジに来日するScott Hanselmanに突っ込んでくださいよ(私は行きません)。とはいえ良くなってる面も理解できるんで、来年は気持ちを切り替えてvNextやりますよ、はい。ちなみにLightNode自体は、vNextベースで、ちょっと違う形で生まれ変わるはずです、という計画があります、やるやる詐欺。

RespClientというPowerShell向けのRedisクライアント/コマンドレットも今年作りました。これはまぁ、たまに私自身も便利にツカッテマス。メンテはguitarrapc先生に譲りました。Redisは相変わらずモリモリ使ってまして、素晴らしいKVSだと思います。来年はやはりこれも放置気味なCloudStructuresをStackExchange.Redisに対応させないと、という……。

そして今年最大の気合の入れ方でリリースしたのがUniRx - ReactiveExtensions for Unity。絶対に必要になる、と、こそこそ作ってたんですが、実際良いもの、欠かせないものになったと思ってます。そして、成功した!と言ってもいいかなー。uFrameに同梱されるようになったとか、海外でも反響あったうえに、国内でもじわじわ話題になりだしていて、かなりいい感じです。来年もがんがん更新していきたい(ちなみに現在AssetStoreでアップデート申請中!)。また、UnityコミュニティとC#コミュニティには若干の断絶がありますが、そこも埋められたらな、といったところですね。

ちなみにUnity関連では、他にLINQ to GameObjectという小品もリリースしたりしたり。

LINQ to BigQueryというGoogle BigQuery用のライブラリも結構な大物でした、と作るの大変だった(というか面倒だった)度合い的に。BigQueryは、正直、凄い。.NETの人もAzureの人も、とりあえず使うべき。うちの会社も基本AWSですが、BigQueryだけはBigQuery。BigQueryに突っ込むためのロギング周りについても一家言できたのですが、その辺はEtwStreamという作りかけの謎プロジェクトがあるので、それが完成した時にでも、お話しましょう(実際作りきりたいとはオモッテマス)

今年はUniRx(Rx), LINQ to BigQuery(Queryable), LINQ to GameObject(LINQ to XML)を通して、改めてLINQとは何ぞやか、というのを掲示できたのも良かったかと思います。口で説明するよりモノで黙らせたほうが早いというアレソレ。

会社

いいところは、今年も非常に強力なメンバーが多くJOINしてくれた!人は会社の原動力ですからね、うちを選んでくれたことに大変感謝です。「C#」では本当に、類を見ないほど力のある会社となっているのではないかな、と。より能力を発揮してもらうような環境を作りたいですね。

さて、今年はやけにCTOの役割とは!みたいなテーマが盛り上がったところですが、私の場合どうかしらん。広告塔代わりであったり求人面であったりなんかは、十二分すぎるほど果たせたとは思います。技術選定なんかも適度に先駆的に、的確だった。少なくとも失敗はない。合間合間にガッと作ってるライブラリ郡も(会社でコソッと作る時間も少し持ってますが、基本的には家で仕上げてますよ)、戦略的に根幹をなすようにしたりで、よくやれたんじゃないかなー。

とはいえ反省点は多かったり。割と勢いだけで突っ走れた1年目と違って2年目は中々むつかしく。特に時間が細切れになるのは避けられなくて、どうも集中しきれず成果としてはかなりイマイチ。この辺は受け入れつつ細切れでも効率的に作業できるよう自分を律するしかないですかね、といった感。そんなわけで自社のプログラムにがっつり関われたかというとかなりそうでもないのが、もにょもにょ。かなり良くない。総論するとどーも歯切れ悪い感じ。来年はドヤッ!といえるようにならないとかな。

ゲーム

PS4やXbox Oneも買ったのですが、うーん。結局やっぱりあんまプレイしてないのよねー。ただまぁPS4>超えられない壁>Xbox Oneというのは痛感しました、これはキビシイ……。Kinect2も割とガッカリ系。そんなわけでvvvvvvのiOS移植が一番楽しんだのかも。

音楽

今年中頃からはずっと大森靖子聞いてましたね、ライブにも行ったし……。YouTube動画だと弾き語りの大森靖子 LIVE @ TIF2013とバンド編成の大森靖子&THEピンクトカレフ@ZeppDiverCityあたりがお薦め。エキセントリックな情報とかインターネット時代の戦略とか、うーん、まぁ、パンクですよ、パンク(適当)。

来年

テーマは「クライアントサイドとサーバーサイドをC#で統一することのメリットの実証」「さらにリアルタイムネットワークもC#で統一」「のためのヒットアプリケーションの創出」です。指向はあんま変わってないんですが、より具体的に。来年は動く年かな、といったところなので是非期待してください。

Unityのコルーチンの分解、或いはUniRxのMainThreadDispatcherについて

この記事はUnity Advent Calendar 2014のための記事になります。昨日はkomiyakさんのUnity を使いはじめたばかりの頃の自分に伝えたい、Unity の基本 【2014年版】でした。いやー、これはまとまってて嬉しい情報です。ところでカレンダー的には穴開けちゃってます(遅刻遅延!)、すみません……。

さて、今回の内容ですが、私の作っているUniRxというReactive Programming(バズワード of 2014!)のためのライブラリを、最近ありがたいことに結構使ってみたーという声を聞くので、Rxの世界とUnityの世界を繋ぐ根幹である、MainThreadDispatcherと、その前準備に必要なコルーチンについて書きます。

Coroutine Revisited

コルーチンとはなんぞや。なんて今更ですって!はい。とりあえず、Unityは基本的にシングルスレッドで動いています。少なくともスクリプト部分に関しては。Unityのコルーチンは、IEnumeratorでyield returnすると、その次の処理を次フレーム(もしくは一定秒数/完了後などなど)に回します。あくまでシングルスレッド、ということですね。挙動について。簡単な確認用スクリプトを貼っつけて見てみると……

void Start()
{
    Debug.Log("begin-start:" + Time.frameCount);
    StartCoroutine(MyCoroutine());
    Debug.Log("end-start" + Time.frameCount);
}
 
IEnumerator MyCoroutine()
{
    Debug.Log("start-coroutine:" + Time.frameCount);
 
    yield return null;
    Debug.Log("after-yield-null:" + Time.frameCount);
 
    yield return new WaitForSeconds(3);
    Debug.Log("end-coroutine:" + Time.frameCount);
}

呼ばれる順番とframeCountを考えてみようクイズ!意外と引っかかるかもしれません。答えのほうですが……

begin-start:1
start-coroutine:1
end-start:1
after-yield-null:2
end-coroutine:168

となります。最後の秒数のフレームカウントはどうでもいいとして、start-coroutineが呼ばれるのはend-startの前ってのがちょっとだけヘーってとこかしら。IEnumerator自体はUnity固有の機能でもなく、むしろC#の標準機能で、通常は戻り値を持ってイテレータを生成するのに使います(Pythonでいうところのジェネレータ)

// 偶数のシーケンスを生成
IEnumerable<int> EvenSequence(int from, int to)
{
    for (int i = from; i <= to; i++)
    {
        if (i % 2 == 0)
        {
            yield return i;
        }
    }
}
 
void Run()
{
    var seq = EvenSequence(1, 10);
 
    // シーケンスはforeachで消費可能
    foreach (var item in seq)
    {
        Debug.Log(item);
    }
 
    // あるいはEnumeratorを取得し回す(foreachは↓のコードを生成する)
    // Unityでのコルーチンでの利用され方はこっちのイメージのほうが近い
    using (var e = seq.GetEnumerator())
    {
        while (e.MoveNext())
        {
            Debug.Log(e.Current);
        }
    }
}

Unityのコルーチンとしてのイテレータの活用法は、戻り値を原則使わず(宣言がIEnumerator)、yield returnとyield returnの間に副作用を起こすために使うということですね。これはこれで中々ナイスアイディアだとは思ってます。

言語システムとしてはC#そのままなので、誰かがIEnumeratorを消費しているということになります。もちろん、それはStartCoroutineで、呼んだ瞬間にまずはMoveNext、その後はUpdateに相当するようなタイミングで毎フレームMoveNextを呼び続けているようなイメージ。

擬似的にMonoBehaviourで再現すると

public class CoroutineConsumer : MonoBehaviour
{
    public IEnumerator TargetCoroutine; // 何か外からセットしといて
 
    void Update()
    {
        if (TargetCoroutine.MoveNext())
        {
            var current = TargetCoroutine.Current;
            // 基本的にCurrent自体はそんな意味を持たないで次フレームに回すだけ
            if (current == null)
            {
                // next frame
            }
            // ただしもし固有の何かが返された時はちょっとした別の挙動する
            if (current is WaitForSeconds)
            {
                // なんか適当に秒数待つ(ThreadをSleepするんじゃなく挙動的には次フレームへ)
            }
            else if (current is WWW)
            {
                // isDoneになってるまで適当に待つ(ThreadをSleepするんじゃなく挙動的には次フレームへ)
            }
            // 以下略
        }
    }
}

こんな感じでしょうか!yield returnで返す値が具体的にUnityのゲームループにおいてどこに差し込まれるかは、UnityのマニュアルのScript Lifecycle Flowchartの図を見るのが分かりやすい。

nullが先頭でWaitForEndOfFrameは末尾なのね、とか。yield returnで返して意味を持つ値はYieldInstruction、ということになっているはずではあるんですが、実際のとこWWWはYieldInstructionじゃないし、YieldInstruction自体はカスタマイズ不能で自分で書けるわけじゃないんで(イマイチすぎる……)なんだかなぁー。Lifecycle Flowchartに書かれていない中でyield可能なのはAsyncOperationかな?

もしイテレータの挙動について更に詳しく知りたい人は、私の以前書いたスライドAn Internal of LINQ to Objectsの14Pを参照してくださいな。

UniRx.FromCoroutine

というわけかで(一旦)コルーチンの話はおしまい。ここからはUniRxの話。UniRxについてはneue cc - A Beginners Guide to Reactive Extensions with UniRxあたりをどうぞ。UniRxはFromCoroutineメソッドにより、コルーチンをUniRxの基盤インターフェースであるIObservable<T>に変換します。

// こんなのがあるとして
IEnumerator CoroutineA()
{
    Debug.Log("a start");
    yield return new WaitForSeconds(1);
    Debug.Log("a end");
}
 
// こんなふうに使える
Observable.FromCoroutine(CoroutineA)
    .Subscribe(_ => Debug.Log("complete"));
 
// 戻り値のあるバージョンがあるとして
IEnumerator CoroutineB(IObserver<int> observer)
{
    observer.OnNext(100);
    yield return new WaitForSeconds(2);
    observer.OnNext(200);
    observer.OnCompleted();
}
 
// こんなふうに合成もできる
var coroutineA = Observable.FromCoroutine(CoroutineA);
var coroutineB = Observable.FromCoroutine<int>(observer => CoroutineB(observer));
 
// Aが終わった後にBの起動、Subscribeには100, 200が送られてくる
var subscription = coroutineA.SelectMany(coroutineB).Subscribe(x => Debug.Log(x));
 
// Subscribeの戻り値からDisposeを呼ぶとキャンセル可能
// subscription.Dispose();

IObservable<T>になっていると何がいいかというと、合成可能になるところです。Aが終わった後にBを実行する、Bが失敗したらCを実行する、などなど。また、戻り値を返すことができるようになります。そして、コルーチンに限らず、あらゆるイベント、あらゆる非同期がIObservable<T>になるので、全てをシームレスに繋ぎ合わせることができる。そこが他のライブラリや手法と一線を画すRxの強みなんです、が、長くなるのでここでは触れません:)

また、MonoBehaviour.StartCoroutineを呼ばなくてもコルーチンが起動しています。これは結構大きな利点だと思っていて、というのも、コルーチンを使うためだけにMonoBehaviourにする必要がなくなる。やはり普通のC#クラスのほうが取り回しが良いので、MonoBehaviourにする必要がないものはしないほうがいい。けれど、コルーチンは使いたい。そうした欲求に応えてくれます。

更にFromCoroutine経由にするとEditor内部では通常は動かせないコルーチンを動かすことができます!(これについては後で説明します)

といった応用例はそのうちやるということで、とりあえずFromCoroutineの中身を見て行きましょう。

// Func<IEnumerator>はメソッド宣言的には「IEnumerator Hoge()」になる
public static IObservable<Unit> FromCoroutine(Func<IEnumerator> coroutine, bool publishEveryYield = false)
{
    return FromCoroutine<Unit>((observer, cancellationToken) => WrapEnumerator(coroutine(), observer, cancellationToken, publishEveryYield));
}
 
// ↑のはWrapEnumeratorを介してこれになっている
public static IObservable<T> FromCoroutine<T>(Func<IObserver<T>, CancellationToken, IEnumerator> coroutine)
{
    return Observable.Create<T>(observer =>
    {
        var cancel = new BooleanDisposable();
 
        MainThreadDispatcher.SendStartCoroutine(coroutine(observer, new CancellationToken(cancel)));
 
        return cancel;
    });
}
 
// WrapEnumeratorの中身は(オェェェェ
static IEnumerator WrapEnumerator(IEnumerator enumerator, IObserver<Unit> observer, CancellationToken cancellationToken, bool publishEveryYield)
{
    var hasNext = default(bool);
    var raisedError = false;
    do
    {
        try
        {
            hasNext = enumerator.MoveNext();
        }
        catch (Exception ex)
        {
            try
            {
                raisedError = true;
                observer.OnError(ex);
            }
            finally
            {
                var d = enumerator as IDisposable;
                if (d != null)
                {
                    d.Dispose();
                }
            }
            yield break;
        }
        if (hasNext && publishEveryYield)
        {
            try
            {
                observer.OnNext(Unit.Default);
            }
            catch
            {
                var d = enumerator as IDisposable;
                if (d != null)
                {
                    d.Dispose();
                }
                throw;
            }
        }
        if (hasNext)
        {
            yield return enumerator.Current; // yield inner YieldInstruction
        }
    } while (hasNext && !cancellationToken.IsCancellationRequested);
 
    try
    {
        if (!raisedError && !cancellationToken.IsCancellationRequested)
        {
            observer.OnNext(Unit.Default); // last one
            observer.OnCompleted();
        }
    }
    finally
    {
        var d = enumerator as IDisposable;
        if (d != null)
        {
            d.Dispose();
        }
    }
}

WrapEnumeratorの中身が長くてオェェェって感じなんですが何やってるかというと、元のコルーチンを分解して、Rx的に都合のいい形に再構築したコルーチンに変換してます。都合のいい形とは「キャンセル可能」「終了時(もしくは各yield時)にObserver.OnNextを呼ぶ」「全ての完了時にObserver.OnCompletedを呼ぶ」「エラー発生時にObserver.OnErrorを呼ぶ」を満たしているもの。コルーチン自体がC#の標準機能のままで、なにも特別なことをしていないなら、別に自分で回す(enumerator.MoveNextを手で呼ぶ)ことも、何も問題はない、わけです。

そんなラップしたコルーチンを動かしているのがMainThreadDispatcher.SendStartCoroutine。今のMainThreadDispatcher.csは諸事情あって奇々怪々なんですが、SendStartCoroutineのとこだけ取り出すと

public sealed class MainThreadDispatcher : MonoBehaviour
{
    // 中略
 
    /// <summary>ThreadSafe StartCoroutine.</summary>
    public static void SendStartCoroutine(IEnumerator routine)
    {
#if UNITY_EDITOR
        if (!Application.isPlaying) { EditorThreadDispatcher.Instance.PseudoStartCoroutine(routine); return; }
#endif
 
        if (mainThreadToken != null)
        {
            StartCoroutine(routine);
        }
        else
        {
            Instance.queueWorker.Enqueue(() => Instance.StartCoroutine_Auto(routine));
        }
    }
 
    new public static Coroutine StartCoroutine(IEnumerator routine)
    {
#if UNITY_EDITOR
        if (!Application.isPlaying) { EditorThreadDispatcher.Instance.PseudoStartCoroutine(routine); return null; }
#endif
 
        return Instance.StartCoroutine_Auto(routine);
    }
}

if UNITY_EDITORのところは後で説明するのでスルーしてもらうとして、基本的にはInstance.StartCoroutine_Autoです。ようはMainThreadDispatcherとは、シングルトンのMonoBehaviourであり、FromCoroutineはそいつからコルーチンを起動しているだけなのであった。なんだー、単純。汚れ仕事(コルーチンの起動、MonoBehaviourであること)をMainThreadDispatcherにだけ押し付けることにより、それ以外の部分が平和に浄化される!

コルーチンの起動が一極集中して、それで実行効率とか大丈夫なの?というと存外大丈夫っぽいので大丈夫。実際、私の会社ではこないだ一本iOS向けにゲームをリリースしましたがちゃんと動いてます。しかしそうなるとStartCoroutineはMonoBehaviourのインスタンスメソッドではなく、静的メソッドであって欲しかった……。

その他、SendStartCoroutineはスレッドセーフ(他スレッドから呼ばれた場合はキューに突っ込んでメインスレッドに戻ってから起動する)なのと、UnityEditorからの起動を可能にしています(EditorThreadDispatcher.Instance.PseudoStartCoroutine経由で起動する)。なので、普通にStartCoroutineを呼ぶ以上のメリットを提供できているかな、と。

UnityEditorでコルーチンを実行する

Editorでコルーチンを動かせないのは存外不便です。WWWも動かせないし……。UniRxではFromCoroutine経由で実行すると、内部でMainThreadDispatcher.SendStartCoroutine経由になることにより、Editorで実行できます。使い方は本当にFromCoroutineしてSubscribeするだけ、と、通常時のフローとまるっきり一緒です。ここで毎回エディターの時は、通常の時は、と書き分けるのはカッタルイですからね。汚れ仕事はMainThreadDispatcherが一手に引き受けています。そんな汚れ仕事はこんな感じの実装です。

class EditorThreadDispatcher
{
    // 中略
 
    ThreadSafeQueueWorker editorQueueWorker= new ThreadSafeQueueWorker();
 
    EditorThreadDispatcher()
    {
        UnityEditor.EditorApplication.update += Update;
    }
 
    // 中略
 
    void Update()
    {
        editorQueueWorker.ExecuteAll(x => Debug.LogException(x));
    }
 
    // 中略
 
    public void PseudoStartCoroutine(IEnumerator routine)
    {
        editorQueueWorker.Enqueue(() => ConsumeEnumerator(routine));
    }
 
    void ConsumeEnumerator(IEnumerator routine)
    {
        if (routine.MoveNext())
        {
            var current = routine.Current;
            if (current == null)
            {
                goto ENQUEUE;
            }
 
            var type = current.GetType();
            if (type == typeof(WWW))
            {
                var www = (WWW)current;
                editorQueueWorker.Enqueue(() => ConsumeEnumerator(UnwrapWaitWWW(www, routine)));
                return;
            }
            else if (type == typeof(WaitForSeconds))
            {
                var waitForSeconds = (WaitForSeconds)current;
                var accessor = typeof(WaitForSeconds).GetField("m_Seconds", BindingFlags.Instance | BindingFlags.GetField | BindingFlags.NonPublic);
                var second = (float)accessor.GetValue(waitForSeconds);
                editorQueueWorker.Enqueue(() => ConsumeEnumerator(UnwrapWaitForSeconds(second, routine)));
                return;
            }
            else if (type == typeof(Coroutine))
            {
                Debug.Log("Can't wait coroutine on UnityEditor");
                goto ENQUEUE;
            }
 
        ENQUEUE:
            editorQueueWorker.Enqueue(() => ConsumeEnumerator(routine)); // next update
        }
    }
 
    IEnumerator UnwrapWaitWWW(WWW www, IEnumerator continuation)
    {
        while (!www.isDone)
        {
            yield return null;
        }
        ConsumeEnumerator(continuation);
    }
 
    IEnumerator UnwrapWaitForSeconds(float second, IEnumerator continuation)
    {
        var startTime = DateTimeOffset.UtcNow;
        while (true)
        {
            yield return null;
 
            var elapsed = (DateTimeOffset.UtcNow - startTime).TotalSeconds;
            if (elapsed >= second)
            {
                break;
            }
        };
        ConsumeEnumerator(continuation);
    }
}

ようは、UnityEditor.EditorApplication.updateでジョブキューを回しています。コルーチン(Enumerator)を手動で分解して、EditorApplication.updateに都合の良い形に再編しています。yield return nullがあったらキューに突っ込んで次のupdateに回すことで、擬似的にStartCorotineを再現。WaitForSecondsだったらリフレクションで内部の秒数を取ってきて(ひどぅい)ぐるぐるループを展開。などなど。

仕組み的には単純、なんですが結構効果的で便利かな、と。ユーザーは全くそれを意識する必要がないというのが一番いいトコですね。

ちなみにアセットストアからダウンロードできるバージョンでは、まだこの仕組みは入ってません(すびばせん!)。GitHubの最新コードか、あとは、ええと、近いうちにアップデート申請しますので来年には使えるようになっているはずです。。。

まとめ

コルーチンをコルーチンたらしめているのは消費者であるStartCoroutineであって、IEnumerator自体はただのイテレータにすぎない。なので、分解も可能だし、他の形式に展開することもできる。

UniRx経由でコルーチンを実行すると「色々なものと合成できる」「(複数の)戻り値を扱える」「キャンセルが容易」「MonoBehaviourが不要」「スレッドセーフ」「エディターでも実行可能」になる。いいことづくめっぽい!Reactive Programmingの力!そんな感じに、UniRxはなるべくシームレスにRxの世界とUnityの世界を繋げるような仕組みを用意しています。是非ダウンロードして、色々遊んでみてください。

Comment (3)

AK2S : (02/19 12:07)

こんにちは! UniRxを使わせていただきました!
ちょっとしたご質問になります。

Observable.FromCoroutine(coroutine);
上記を使用した際に、
—————————————
int n = 0;
bool ret = false;
IEnumerator CoroutineA(int nNum, bool isRet)
{
yield return new WaitForSeconds(1.0f);
this.n = nNum;
this.ret = isRet;
}
—————————————
このようなIEnumeratorを引数(coroutine)に渡したいと思っています。

しかし、どのようにIEnumeratorを渡して良いのか、そもそも引数を前提とした作りになっているのか解りませんでしたのでご質問させて頂きました。

neuecc : (02/24 23:04)

どうもどうも。
返信遅れてすみません(気付いてなかった!)
引数がある場合は
Observable.FromCoroutine(() => CoroutineA(100, true))
のようにすればOKです。

AK2S : (03/02 15:24)

ご返信ありがとうございます!

なるほど、そういう使い方をすれば宜しいのですね。
勉強になります_(._.)_

また何か不明点があったら、コメントさせて頂きます(´∇`)ノ

Trackback(0)
Write Comment

VS2015+RoslynによるCodeRefactoringProviderの作り方と活用法

この記事はC# Advent Calendar 2014のための記事になります。私は去年のAdvent Calendarでは非同期時代のLINQというものを書いていました、うん、中々良い記事であった(自分で言う)。今年のテーマはRoslynです。

先月にVS2015のRoslynでCode Analyzerを自作する(ついでにUnityコードも解析する)という記事を書きましたが、VS2015 PreviewではRoslynで作る拡張にもう一つ、Code Refactoringがあります。こちらも簡単に作れて、中々ベンリなので(前にVS2015のRoslynは以前から後退して「あんま大したことはできない」と言いましたが、それはそれでかなり役立ちです)、是非作っていきましょう。Code Analyzerと同じく非常に簡単に作れるのと、テンプレートがよくできていてちゃんとガイドになってるので、すんなり入れるかと思います。なお、こちらはCode Analyzerと違いNuGet配布やプロジェクト単位での参照は不可能、VSIXのみ。そこはちょっと残念……。

Code Refactoring

下準備としてはCode Analyzerの時と同じくVisual Studio 2015 Previewのインストールの他に、Visual Studio 2015 Preview SDK.NET Compiler Platform SDK Templates、そして.NET Compiler Platform Syntax Visualizerを入れてください。

さて、まずテンプレートのVisual C#→Extensibilityから「Code Refactoring(VSIX)」を選びます。とりあえずこのテンプレート(がサンプルになってます)をCtrl+F5で実行しましょう。これで立ち上がるVSは通常のVSに影響を及ぼさず自作拡張がインストールされる特殊なインスタンスになってます(devenv.exeを引数「/rootsuffix Roslyn」で立ち上げてる、のがDebugのとこで確認できる)。というわけで、このテンプレートの拡張によりクラス名をCtrl+.することにより

クラス名が逆になる、という(実にどうでもいい)機能拡張が追加されました!と、いうわけで、Code RefactoringはCode Analyzerと同じく「Light Bulb」によるCode Actionが実装可能になります。Code AnalyzerはDiagnosticが起点でしたが、こちらは、指し示された位置を起点にコードの削除/追加/変更を行えるという感じ。「リファクタリング」というとコード修正のイメージが個人的には強いんですが、RoslynのCode Refactoringはどちらかというと「コード生成」に使えるな、という印象です。例えばプロパティを選択してCode RefactoringでINotifyPropertyChangedのコードに展開してしまうとか。大量に作る場合、一個一個コードスニペットで作るより、そっちのほうが速く作れそうですよね?など、色々使い手はあるでしょふ。結構可能性を感じるし、良い機能だと思っています(それR#で今までも出来たよ!とかそれEclipseで既に!とか言いたいことはあるかもしれませんが!)

ただまあ、Code Refactoringって名前は好きじゃない。けれど、じゃあ何がいいかっていうと、なんでしょうねぇ。Code Generate、ジェネレートだけじゃないから、まぁCode Actionかなぁ。AnalyzerもCode Actionだから区別付かなくて嫌だって可能性もあるか、うーん、うーん、ま、いっか……。

ArgumentNullExceptionProvider

サンプルコードがたった1ファイルのように、作るのはとても簡単です。CodeRefactoringProviderを継承してComputeRefactoringsAsyncを実装する、だけ。適当にシンタックスツリーを探索して、もしLight Bulbを出したければcontext.RegisterRefactoringにCodeActionを追加する、と。

というわけで早速何か一個作ってみましょう。実用的なのがいいなぁ、ということでnullチェック、if(hoge == null) throw new ArgumetNullException(); というクソ面倒くさい恒例のアレを自動生成しよう!絶対使うし、あるとめちゃくちゃ捗りますものね!

上の画像のが完成品です。便利そう!便利そう!

さて、まずはLight Bulbをどこで出したいか。メソッドの引数で出すんで、引数を選択してたらそれは対象にしたいかなぁ?あと、一々選択するのも面倒だから、メソッド名でも出しましょうか。ふむ、とりあえず実装が簡単そうなメソッド名だけで行きましょう。何事も作る時は単純なところから広げていくのが一番、特に初めてのものはね。

// ArgumentNullExceptionProviderという名前でプロジェクト作ったらクラス名が酷いことに、その辺はちゃんと調整しましょふ
[ExportCodeRefactoringProvider(ArgumentNullExceptionProviderCodeRefactoringProvider.RefactoringId, LanguageNames.CSharp), Shared]
internal class ArgumentNullExceptionProviderCodeRefactoringProvider : CodeRefactoringProvider
{
    public const string RefactoringId = "ArgumentNullExceptionProvider";
 
    public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
    {
        // とりあえずコード全体を取る(これはほとんど定形)
        var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
 
        // SemanticModel(コードをテキストとしてではなく意味を持ったモデルとして取るようにするもの、これもほぼ必須)
        var model = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
 
        // context.Spanが選択位置、ということで選択位置のコードを取る(この辺もほぼ定形かな)
        // もし選択範囲に含まれてるものから一部のものを取り出す、とかならroot.DescendantNodes(context.Span).OfType<XxxSyntax>() という手とか色々
        var node = root.FindNode(context.Span);
 
        // メソッド定義じゃなかったら無視
        var methodDecl = node as MethodDeclarationSyntax;
        if (methodDecl == null) return;
 
        // コード生成作る
        var action = CodeAction.Create("Generate ArgumentNullException", c => GenerateArgumentNullException(context.Document, model, root, methodDecl, c));
 
        // とりあえず追加
        context.RegisterRefactoring(action);
    }
 
    async Task<Document> GenerateArgumentNullException(Document document, SemanticModel model, SyntaxNode root, MethodDeclarationSyntax methodDecl, CancellationToken cancellationToken)
    {
        // あとで書く:)
        return document;
    }
}

まずはこんなとこですね、ようはサンプルからClassDeclarationSyntaxをMethodDeclarationSyntaxに変えただけ + CodeActionを作るところの引数にSemanticModelとrootのSyntaxNodeを足してあります。この辺はほとんど定形で必要になってくるので、とりあえず覚えておくといいでしょう。さて、一旦こいつで実行してみて、ちゃんとLight Bulbが思ったところに出るか確認してから次に行きましょー。続いて本題のコード生成部分。

Task<Document> GenerateArgumentNullException(Document document, SemanticModel model, SyntaxNode root, MethodDeclarationSyntax methodDecl, CancellationToken cancellationToken)
{
    // 引数はParameterListから取れる。
    // この辺はVSでMethodDeclarationSyntaxをF12で飛んで、metadataからそれっぽいのを探しだすといいんじゃないかな?
    // ドキュメントがなくてもVisual StudioとIntelliSenseがあれば、なんとなく作れてしまうのがC#のいいところだからね!
    var parameterList = methodDecl.ParameterList;
 
    // ただのType(TypeSyntax)はコード上のテキスト以上の意味を持たない、
    // そこからstructかclassか、など型としての情報を取るにはSemanticModelから照合する必要がある
    var targets = parameterList.Parameters
        .Where(x =>
        {
            var typeSymbol = model.GetTypeInfo(x.Type).Type;
            return typeSymbol != null && typeSymbol.IsReferenceType;
        });
 
    // C#コードを手組みするのは(Trivia対応とか入れると)死ぬほど面倒なのでParseする
    var statements = targets.Select(x =>
    {
        var name = x.Identifier.Text;
        // String Interpolationベンリ(ただし文法はまだ変更される模様……)
        return SyntaxFactory.ParseStatement("if (\{name} == null) throw new ArgumentNullException(nameof(\{name}));");
    }).ToArray();
 
    // 追加、メソッドBodyはBody以下なのでそこの先頭に(AddStatementsだと一番下に置かれてしまうのでダメ、nullチェックは「先頭」にしたい)
    var newBody = methodDecl.Body.WithStatements(methodDecl.Body.Statements.InsertRange(0, statements));
 
    // 入れ替え
    var newRoot = root.ReplaceNode(methodDecl.Body, newBody);
    var newDocument = document.WithSyntaxRoot(newRoot);
 
    return Task.FromResult(newDocument);
}

SemanticModelがキーです。SyntaxTreeから取ってきただけのものは、何の情報も持ってません、ほんとただのコード上の字面だけです。今回で言うとnullチェックしたいのは参照型だけですが、それを識別することが出来ません。そこからTypeInfoを取り出すことができるのがSemanticModelになります。例えば「Dictionary」から「System.Collections.Generic.Dictionary」といったフルネームを取り出したりなど、とかくコード操作するには重要です。今回はこれでIsReferenceTypeを引き出しています。

あとは、コードの手組みは辛すぎるのでSyntaxFactory.ParseXxxを活用して済ませちゃうのは楽です。その後は、rootからReplaceと、documentから丸っと差し替えなどは定形ですね。あ、そうそう、あとnameofはC# 6.0の新機能です。ベンリベンリ。

では、実際使ってみると……。

// これが
static void Hoge(string[] a, string b, Dictionary<int, int> huga, int tako)
{
}
 
// こうなる、あ、れ……?
static void Hoge(string[] a, string b, Dictionary<int, int> huga, int tako)
{
if (a == null) throw new ArgumentNullException(nameof(a));if (b == null) throw new ArgumentNullException(nameof(b));if (huga == null) throw new ArgumentNullException(nameof(huga));}

はい。ちゃんと機能するコードができてはいます。が、なんじゃこりゃーーーーー。なんでかっていうとTrivia(空白とか改行)が一切考慮されてないから、なんですね。Code Refactoringを作る上でメンドウクサイのは、こうしたTriviaへの考慮です。置換なら既存のTriviaをそのまま使えるんですが、コード追加系だと、自分でTrivia入れたり削ったりの調整しないと見れたもんじゃなくなります。というわけで、こっから先が地獄……。まぁ、頑張りましょう。さすがにそのままだと使えないので、調整しましょう。

まずは改行です。改行は結構簡単で、(大抵の場合)TrailingTrivia(後方のTrivia(空白や改行など))にCRLFを仕込むだけ。

// statementsのところをこう変更すると
var statements = targets.Select(x =>
{
    var name = x.Identifier.Text;
    var statement = SyntaxFactory.ParseStatement("if (\{name} == null) throw new ArgumentNullException(nameof(\{name}));");
    // TrailingTriviaは行末、というわけで改行を仕込む
    return statement.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed);
})
.ToArray();
 
        // 置換結果はこうなります、だいぶ良くなった!
        static void Hoge(string[] a, string b, Dictionary<int, int> huga, int tako)
        {
if (a == null) throw new ArgumentNullException(nameof(a));
if (b == null) throw new ArgumentNullException(nameof(b));
if (huga == null) throw new ArgumentNullException(nameof(huga));
        }

だいぶいい線いってますね!このぐらいできれば、あとは実行後にCtrl+K, D(ドキュメントフォーマット)押してね、で済むんで全然妥協ラインです。が、もう少し完璧にしたいならインデントも挟みましょうか。インデントの量は直前の{から引っ張ってきて調整してみましょふ。

// WithLeadingTriviaの追加
var statements = targets.Select(x =>
{
    var name = x.Identifier.Text;
    var statement = SyntaxFactory.ParseStatement("if (\{name} == null) throw new ArgumentNullException(nameof(\{name}));");
 
    // LeadingTriviaに「{」のとこのインデント + 4つ分の空白を入れる
    return statement
        .WithLeadingTrivia(methodDecl.Body.OpenBraceToken.LeadingTrivia.Add(SyntaxFactory.Whitespace("    ")))
        .WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed);
})
.ToArray();
 
        // 置換結果はこうなります、完璧!
        static void Hoge(string[] a, string b, Dictionary<int, int> huga, int tako)
        {
            if (a == null) throw new ArgumentNullException(nameof(a));
            if (b == null) throw new ArgumentNullException(nameof(b));
            if (huga == null) throw new ArgumentNullException(nameof(huga));
        }

こんなところですね。さて、勿論このコードはOpenBraceTokenが改行された位置にあることを前提にしているので、後ろに{を入れるスタイルのコードには適用できません。また、空白4つをインデントとして使うというのが決め打ちされています。また、なんども実行しても大丈夫なように既にthrow new ArgumentNullExceptionが記述されてる引数は無視したいよねえ、などなど、完璧を求めるとキリがありません。キリがないということは、適当なところでやめておくのが無難ということです、適度な妥協大事!

フォーマットする

とはいえ、フォーマットはもう少しきちんとやりたいところです。実は簡単にやる手段が用意されていて、.WithAdditionalAnnotations(Formatter.Annotation) を呼ぶことで、その部分だけフォーマットがかかります。正確にはフォーマットが可能になるタイミングでフォーマットがかかるようになります、どういうことかというと、例えばインデントのフォーマットは前後のコード情報がなければかけることは出来ません。このコード例でいうとif()…は前後空白もない完全一行だけなのでフォーマットもなにもできない。なのでAnnotationのついたコード片がフォーマット可能なドキュメントにくっついたタイミングで自動でかかるようになります。AnnotationはRoslynの構文木内でのみ使われるオプション情報とでも思ってもらえれば。

var statements = targetParameters
    .Select(x =>
    {
        var name = x.Identifier.Text;
        var statement = SyntaxFactory.ParseStatement("if (\{name} == null) throw new ArgumentNullException(nameof(\{name}));");
 
        // Formatter.Annotationつけてフォーマット
        return statement
            .WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed)
            .WithAdditionalAnnotations(Formatter.Annotation);
    })
    .ToArray();

これは簡単でイイですね!!!なのでTriviaの付与は、最低限のCRLFぐらいを部分的に入れるだけでOK。これは、これからもめちゃくちゃ多用するのではかと思われます。他にAnnotationにはSimplifier.Annotationなどが用意されてます。

フルコード

最初に妥協した(?)引数を選択してたらそれも対象に、ってコードも入れましょうか。というのを含めたフルコードは以下になります。

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using System.Collections.Generic;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
 
namespace Grani.RoslynTools
{
    [ExportCodeRefactoringProvider(GenerateArgumentNullExceptionCodeRefactoringProvider.RefactoringId, LanguageNames.CSharp), Shared]
    internal class GenerateArgumentNullExceptionCodeRefactoringProvider : CodeRefactoringProvider
    {
        public const string RefactoringId = "GenerateArgumentNullException";
 
        public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
        {
            // とりあえずコード全体を取る(これはほとんど定形)
            var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
 
            // SemanticModel(コードをテキストとしてではなく意味を持ったモデルとして取るようにするもの、これもほぼ必須)
            var model = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
 
            // context.Spanが選択位置、ということで選択位置のコードを取る(この辺もほぼ定形かな)
            var node = root.FindNode(context.Span);
 
            // パラメータリストの場合はそれだけ、クラス名だったらその定義の全部の引数を取る
            MethodDeclarationSyntax methodDecl;
            IEnumerable<ParameterSyntax> selectedParameters;
            if (node is MethodDeclarationSyntax)
            {
                methodDecl = (MethodDeclarationSyntax)node;
                selectedParameters = methodDecl.ParameterList.Parameters;
            }
            else
            {
                // 単品選択のばやい(名前の部分選択でParameterSyntax、型の部分選択でParentがParameterSyntax)
                if (node is ParameterSyntax || node.Parent is ParameterSyntax)
                {
                    var targetParameter = (node as ParameterSyntax) ?? (node.Parent as ParameterSyntax);
                    // 親方向にMethodDeclarationSyntaxを探す
                    methodDecl = targetParameter.Ancestors().OfType<MethodDeclarationSyntax>().FirstOrDefault();
                    selectedParameters = new[] { targetParameter };
                }
                else
                {
                    // 選択範囲から取り出すばやい
                    var parameters = root.DescendantNodes(context.Span).OfType<ParameterSyntax>().ToArray();
                    if (parameters.Length == 0) return;
                    methodDecl = parameters[0].Ancestors().OfType<MethodDeclarationSyntax>().FirstOrDefault();
                    selectedParameters = parameters;
                }
            }
            if (methodDecl == null) return;
 
            // ただのType(TypeSyntax)はコード上のテキスト以上の意味を持たない、
            // そこからstructかclassか、など型としての情報を取るにはSemanticModelから照合する必要がある
            var replaceTargets = selectedParameters
                .Where(x =>
                {
                    var typeSymbol = model.GetTypeInfo(x.Type).Type;
                    // ジェネリック型で型引数がclassでstructでもない場合はIsXxxが両方false、これはif(xxx == null)の対象にする
                    return typeSymbol != null && typeSymbol.IsReferenceType || (!typeSymbol.IsReferenceType && !typeSymbol.IsValueType);
                })
                .ToArray();
 
            if (replaceTargets.Length == 0) return;
 
            // コード生成作る(nameof利用の有無で2つ作ってみたり)
            var action1 = CodeAction.Create("Generate ArgumentNullException", c => GenerateArgumentNullException(context.Document, root, methodDecl, replaceTargets, true, c));
            var action2 = CodeAction.Create("Generate ArgumentNullException(unuse nameof)", c => GenerateArgumentNullException(context.Document, root, methodDecl, replaceTargets, false, c));
 
            // 追加
            context.RegisterRefactoring(action1);
            context.RegisterRefactoring(action2);
        }
 
        Task<Document> GenerateArgumentNullException(Document document, SyntaxNode root, MethodDeclarationSyntax methodDecl, ParameterSyntax[] targetParameters, bool useNameof, CancellationToken cancellationToken)
        {
            // nameof版と非nameof版を用意
            var template = (useNameof)
                ? "if ({0} == null) throw new ArgumentNullException(nameof({0}));"
                : "if ({0} == null) throw new ArgumentNullException(\"{0}\");";
 
            var statements = targetParameters
                .Select(x =>
                {
                    // C#コードを手組みするのは(Trivia対応とか入れると)死ぬほど面倒なのでParseする
                    var name = x.Identifier.Text;
                    var statement = SyntaxFactory.ParseStatement(string.Format(template, name));
 
                    // Formatter.Annotationつけてフォーマット
                    return statement
                        .WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed)
                        .WithAdditionalAnnotations(Formatter.Annotation);
                })
                .ToArray();
 
            // 追加
            var newBody = methodDecl.Body.WithStatements(methodDecl.Body.Statements.InsertRange(0, statements));
 
            // 入れ替え
            var newRoot = root.ReplaceNode(methodDecl.Body, newBody);
            var newDocument = document.WithSyntaxRoot(newRoot);
 
            return Task.FromResult(newDocument);
        }
    }
}

まずLight Bulbを出すためのチェックがFindNodeだけでは済まなくなるので、root.DescendantNodes(context.Span)が活躍します。あとは、もう、色々もしゃもしゃと。とにかくコーナーケース探していくとかなり面倒くさかったり。例えば、引数の型の部分を選択した時と、名前の部分を選択した時の対処、などなど……。しょうがないけれどね。

それと、もしメソッドの中に参照型の引数がなかったらLight Bulbを出さないようにするため、replaceTargetsの生成をRegisterRefactoringの前に変更しています。それと、ジェネリックな型引数の場合で制約がついてない状態も生成対象に含めるよう微調整。といったような、この辺の細かい調整はある程度出来上がってからやってくのが良いでしょうねー。

Portable?

ところで、CodeRefactoringにせよAnalyzerにせよ、テンプレートではコア部分はPCLで生成されています。ということは、実は、System.IOとか使えない。これ、ちょっと色々な邪道な操作したい時に不便なんですよ……。ていうかVisual Studio前提なんだからPCLである必要ないじゃん!いみわからない、なんでもPCLって言っておけばいいってもんでもないでしょう!あー、もう私はPCL大嫌いだよぅー。しかもPCLで生成されたプロジェクトは普通のプロジェクトタイプには簡単には戻せないんですよ、うわぁ……。

まあcsprojを手で書き換えればできます。やりかたは

<!-- こいつを消す -->
<ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
 
<!-- こいつを消す -->
<TargetFrameworkProfile>Profile7</TargetFrameworkProfile>
 
<!-- こいつを消して -->
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
<!-- かわりにこれを追加 -->
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

なんだかなぁー。

ビルトイン拡張

VS2015ではビルトインで幾つかCode Refactoringが入っています。が、ノーヒント(Ctrl+.を押すまではLight Bulbが出ない)なので、宝探し状態です!例えばプロパティを選択すると「Generate Constructor」が出てきたり。まぁ、これはそのうちリストが公開されるでしょう。

まとめ

拡張を作るのは簡単!うーん、んー、簡単?まぁ簡単!少なくとも今までよりは比較的遥かに簡単カジュアルに作れるようになりました。こうした自動生成って、一般的なものだけではなく、プロジェクト固有で必要になるものもあると思います。例えばうちの会社ではシリアライザにprotobuf-netを使っているので、クラスのプロパティの各属性にDataMemberをつけて回る必要がある。ちゃんとOrderの順番を連番にして。でも手作業は面倒、そこで、CodeRefactoring。

こんな風にプロパティを選択してCtrl+.でほいっと生成できる。このコードはちゃんと属性の振り直しとかも配慮してある(場合によって追加、場合によって置き換え、とかのコードを書くのはやっぱり結構面倒!フルコードはneuecc/AddDataMemberWithOrderCodeRefactoringProvider.csに置いておきます、使いたい方 もしくは CodeRefactoring作りの参考にどうぞ)

こうしたプロジェクト固有の必要な自動生成のためのコードがサクッと作れるようになったのは地味に革命的かなぁ、なんて思ってます。どんどん活用されていくといいですね、少なくともうちの会社(グラニ)では社内用に皆で色々作って配布していこうかなあと計画しています(ところでグラニはまだまだアクティブにエンジニアの求人してます!最先端のC#に興味のある方は是非是非どうぞ、「今から」VS2015を使い倒しましょう)。

AnalyzerもCode Refactoringも、すっごくミニマムな仕様に落ち着いていて、限りなく削っていった形なんだろうなぁ、と想像します。すごく小さくて、でも、すごく使い手がある。いい落とし所だと思ってます。あとはもう活用するもしないも全て自分達次第。とにかくまずは、是非触ってみてください。可能性を感じましょう。

kpm wrapでVS2015 Previewでもcsprojをkprojで参照する

この記事は土壇場駆け込みで爆誕したASP.NET Advent Calendar 2014の3日目の記事です。昨日はASP.NET Web API で Jil JSON Serializer を使ってみるでした。Jilいいよね、うん、使われるようになるといいと思ってます。

さて、次のASP.NET(ASP.NET 5/vNext)は、もうみんな薄々気付いていると思いますが、Web APIとかOwinとかは結局vNextにおいて……。口には出せなくても!絶対にオフィシャルにアナウンスされることはなくても!ふむ、まぁ、その辺は置いておきましょう、はい。そんな不穏なことはね!ね!というわけでそんなことは全く関係なく今回はkpm wrapの話です。何それ、というと、多分、今の間しか使うことはないと思います。いや、むしろ今の間だからこそ直接使う人はそんないないと思います。つまり、誰にとっても全く役に立たないお話。

失われたプロジェクト参照

なんとASP.NET 5(旧vNext、ちなみにMVCは6なので割と紛らわしいネーミングじゃ)のプロジェクトでは、csprojが参照できません。へー、どーいうことなのー?というとこーいうことです。

参照設定でプロジェクト選んでも参照できない!何がnot supportedだよ、クソが!じゃあどうするか、というと、ASP.NET 5 Class Libraryという新設されたK専用のクラスライブラリプロジェクトを選べば作れる。ほぅ……、そんなポータブルじゃなさすぎるクラスライブラリ作ってたまるか、何が嬉しくてクラスライブラリをウェブ専用(っぽく)しなきゃならんのだ!

別の道としてはNuGet経由でdllを渡すとか、そーいうアプローチが推奨されてて、それは一見良いように見えて全然よくないです。csprojを参照してソースコードと直にひっつくってのはすごく大事なんです。クラスライブラリもF12でソースにダイレクトに飛べる環境を作るのはめちゃくちゃ大事なんです。NuGetでdllで分離して理想的!だとかそんなお花畑理論に乗っかるきは毛頭ない。

kpm wrap

そんなわけでものすごく憤慨して、Kマジダメだわー、ありえないわー、センスないわー、ぐらいに思ってたりなかったりしたんですが、さすがに突っ込まれる。さすがに気づく。というわけで、最新のKRuntimeは、ちゃんとkprojでもcsprojを参照できるようになっています。また、一定の手順を踏めばVS2015でも参照が可能になります。さすがに次のバージョンのVS2015では(アルファ?ベータ?)対応してくると思うので、短い寿命のお話、或いはそういう風な仕組みになっているのねのお話。

まず、aspnet/HomeからDownload ZIPしてkvm.cmdを拾ってきます。こっから先はcmdで叩いていきます。まずkvm listすると1.0.0-beta1が入ってるのを確認できるはず。VS2015 Preview同梱がそれなんですね。というわけで、1.0.0-beta2を入れましょう。「kvm install latest」コマンドだと恐らく最新のmasterバージョンになってしまってよろしくないので、バージョン指定しましょう。MyGet - aspnetvnextを見るとバージョン確認できるので、そこから任意にバージョン指定でいれましょう。しかしそれでも404 NotFoundが返ってきてうまく入れられないことがあります!その場合は上のURLのところからKRE-CLR-x86のアイコンをクリックすれば生のnupkgが拾えるのでそいつを手で解凍して↓の場所に配置しましょう、nupkgをzipに変えるだけですがちゃんとNuGet.exeで展開してもいいです(どうでもいい)

あとは「%UserProfile%.kre\packages\」のbeta2のbinのとこにkpmが転がってるので、そいつでkpm wrapコマンドを叩けばOK。あとすると

kpm wrap "c:\ToaruWebApp\src\ToaruClassLibrary"
 
Wrapping project 'ToaruClassLibrary' for '.NETFramework,Version=v4.5'
  Source c:\ToaruWebApp\src\ToaruClassLibrary\ToaruClassLibrary.csproj
  Target c:\ToaruWebApp\wrap\ToaruClassLibrary\project.json
  Adding bin paths for '.NETFramework,Version=v4.5'
    Assembly: ../../src/ToaruClassLibrary/obj/{configuration}/ToaruClassLibrary.dll
    Pdb: ../../src/ToaruClassLibrary/obj/{configuration}/ToaruClassLibrary.pdb

というありがたいメッセージによりラッピングが完了します。あとはGUIからAdd Referenceすれば……、まぁ当然not supportedと怒られます。が、project.jsonを手編集すればいけるようになります!dependenciesに

"ToaruClassLibrary": "1.0.0.0"

とでも足してやれば(ちゃんとIntelliSenseも効いてる)あら不思議、謎の空っぽいASP.NETライブラリが追加されてリファレンスにもきちんと追加されてコードでも参照できるようになる(ようになる時もある、なんかむしろあんまうまく行かないことのほうが多いので、なんか別の条件というか再現手順間違えてるかも……とりあえず動かなかったらしょーがないということで!)

仕組み?

wrapコマンドを実行するとglobal.jsonが

{
  "sources": [
    "src",
    "test",
    "wrap"
  ]
}

になってます。global.jsonについてはmiso_soup3 Blog - ASP.NET 5 について一部に詳しく書いてありますが、プロジェクトを探すためのルートですね。で、増えたのはwrapで、wrapフォルダにこの場合だとToaruClassLibraryというASP.NET 5クラスライブラリプロジェクトができています。wrapコマンドにより生成される実体はこいつで、こいつのproject.jsonは

{
  "version": "1.0.0.0",
  "frameworks": {
    "net45": {
      "wrappedProject": "../../src/ToaruClassLibrary/ToaruClassLibrary.csproj",
      "bin": {
        "assembly": "../../src/ToaruClassLibrary/obj/{configuration}/ToaruClassLibrary.dll",
        "pdb": "../../src/ToaruClassLibrary/obj/{configuration}/ToaruClassLibrary.pdb"
      }
    }
  }
}

何が何なのかは、十分想像できそうですね。

というわけで、KのウェブプロジェクトがASP.NET 5 クラスライブラリしか参照できないという原則に変化はありません。ただしkpm wrapコマンドを叩くことでcsprojのdllから参照を作ってくれます。まぁ、dllということでビルドしないと反映されないじゃん!とかありますが、とりあえず一応実用上は問題ないレベルにまではなっている、かな……?(もしSubModuleとかで参照されてる共通ライブラリのcsprojが更新されたとして、各自のローカルで明示的にそれをリビルドしないと変更反映されないことになって不便そうだなあ、とか辛そうな点は幾らでも探せますけね)

まとめ

まぁ、VS上だとすっごく不安定で、動いたり動かなかったりって感じなんで、現状あんま実用性はない、かな……。とりあえず、次のバージョンぐらいではcsprojの参照も行けるようになった、という確認が取れた、というだけで十二分です。ASP.NET 5は仕組みがやりすぎに複雑で、VSとの統合もうまくいってるんだかいってないんだか(例えばVS2015でついにできるようになったウォッチウィンドウ上でのラムダ式が何故かASP.NETプロジェクトでは効かない、とか)ってところですが、まぁリリース版にはその辺も解決されるでしょう、と思いたい!

さて、明日のASP.NET Advent Calendar 2014はDapperの話のようです。Dapperは私もヘヴィに使ってますからね!楽しみです(ついちょっと前まで埋まってなかったんですがギリギリ繋がったようでホッ)

VS2015のRoslynでCode Analyzerを自作する(ついでにUnityコードも解析する)

Visual Studio 2015 Previewが発表されました!この中にはC# 6.0やRoslynも含まれていて、今から試すことができます。C#の言語機能は他の人が適当にまとめてくれるので私はノータッチということで、新機能であるRoslynで拡張を作っていきましょう。

Roslynによる拡張は、ン年前に最初のPreviewが出た時は、Visual Studioの解析エンジン自体がRoslynになるから簡単にアレもコレも出来るぜ!と夢いっぱいのこと言ってましたが、実のところ最終的に現在(VS2015 Preview)ではかなり萎んでしまいました。「Code Refactoring」と「Diagnostic with Code Fix」だけです。何ができるかは、まぁ名前から察しということで、あんま大したことはできないです。がっくし。とはいえ、しかし全然使いドコロはあるし簡単に作れはするので、とにかく見て行きましょう。

下準備としてVS2015 Previewのインストールの他に、Visual Studio 2015 Preview SDK.NET Compiler Platform SDK Templates、そして.NET Compiler Platform Syntax Visualizerを入れてください。

Diagnostic with Code Fix

今回は「Diagnostic with Code Fix」を作ります。まずテンプレートのVisual C#→Extensibilityから「Diagnostic with Code Fix(NuGet + VSIX)」を選んでください。NuGet + VSIXというのが面白いところなんですが、とりあえずこのテンプレート(はサンプルになってます)をビルドしましょう(Testプロジェクトは無視していいです)。そして、ReferencesのAnalyzers(ここがVS2015から追加されたものです!)からAdd Analyzerを選び、さっきビルドしたdllを追加してみてください。

するとコード解析が追加されて、クラス名のところにQuick Fixが光るようになります。

サンプルコードのものはMakeUpperCaseということで、クラス名に小文字が含まれていたら警告を出す&全部大文字に修正するQuickFixが有効になります。

つまりDiagnostic with Code Fixは、よーするに今までもあったCode Analysis、FxCopです。ただし、Roslynによって自由に解析でき、追加できます。また、ReferencesのAnalyzersに追加できるということで、ユーザーのVisual Studio依存ではなく、プロジェクト内に直接含めることができます。追加/インストールはdllをNuGetで配ることが可能(だからVSIX + NuGetなんですね、もちろんVSIXでも配れます)。より気軽に、よりパワフルにコード解析が作れるようになったということで、地味に中々革命的に便利なのではないでしょうか?

このまま、そのサンプルコードのMakeUpperCaseの解説、をしてもつまらないので、続けて実用的(?)なものを一個作りました。

namespaceの修正

うちの会社ではUnityを使ってモバイルゲーム開発を行っていますが、LINQもガリガリ使います。その辺のことはLINQ to GameObjectによるUnityでのLINQの活用にも書いたのですが、困ったことに標準UnityではLINQ to Objectsを使うとAOTで死にます。Unity + iOSのAOTでの例外の発生パターンと対処法で書いたように対処事態は可能なんですが、最終的に標準LINQを置き換える独自実装をSystem.LinqExネームスペースに用意することになりました。で、それを使うには「using System.LinqEx;」する必要があります。「using System.Linq;」のかわりに。むしろ「using System.Linq;」はAOTで死ぬので禁止したいし、全面的に「using System.LinqEx;」して欲しい。すみやかに。どうやって……?

そこでDiagnostic with Code Fixなんですね。既存コードの全てに検査をかけることもできるし(ソリューションエクスプローラーから対象プロジェクトを右クリックしてAnalyze→Run Code Analysis)、書いてる側からリアルタイムに警告も出せるし、ワンポチでSystem.LinqExに置き換えてくれる。このぐらいなら全ファイルから「using System.Linq;」を置換すりゃあいいだけなんですが、リアルタイムに警告してくれるとうっかり忘れもなくなるし(CIで警告すればいいといえばいいけど、その前に自分で気づいて欲しいよね)、もっと複雑な要件でも、RoslynでSyntaxTreeを弄って置き換えるので、テキスト置換のような誤爆の可能性があったり、そもそも複雑で警告/置換不能、みたいなことがなくなるので、とても有益です。

というわけで「using System.Linq;」を見つけたら「using System.LinqEx;」に書き換える拡張を作りましょう!(うちの会社にとっては)実用的で有益で、かつ、はぢめての拡張のテーマとしてもシンプルで作りやすそうでちょうどいいですね!

DiagnosticAnalyzer

コード解析はDiagnosticAnalyzer、コード置換はCodeFixProviderが担当します。必要なファイルはこの2ファイルだけ(シンプル!)、コード置換が不要ならDiagnosticAnalyzerだけ用意すればOK。というわけで、以下がDiagnosticAnalyzerのコードです。

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
 
namespace UseLinqEx
{
    [DiagnosticAnalyzer(LanguageNames.CSharp)]
    public class UseLinqExAnalyzer : DiagnosticAnalyzer
    {
        // この辺はテンプレートのままに適当に書き換え
        public const string DiagnosticId = "UseLinqEx";
        internal const string Title = "System.Linq is unsafe in Unity. Must use System.LinqEx.";
        internal const string MessageFormat = "System.Linq is unsafe in Unity. Must use System.LinqEx."; // 同じの書いてる(テキトウ)
        internal const string Category = "Usage"; // Categoryの適切なのってナンダロウ
 
        internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
 
        // namespaceを引っ掛ける
        public override void Initialize(AnalysisContext context)
        {
            // なにをRegisterすればいいのか問題、テンプレではRegisterSymbolActionですが、
            // SymbolActionにはなさそうだなー、と思ったら他のRegisterHogeを使いましょう
            // ここではRegisterSyntaxNodeActionでSyntaxKind.UsingDirectiveを呼びます
            // SyntaxKindの判定はRoslyn Syntax Visualizerに助けてもらいましょう
            context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.UsingDirective);
        }
 
        static void Analyze(SyntaxNodeAnalysisContext context)
        {
            // Nodeの中身はSyntaxKindで何を選んだかで変わるので適宜キャスト
            var syntax = (UsingDirectiveSyntax)context.Node;
            if (syntax.Name.NormalizeWhitespace().ToFullString() == "System.Linq")
            {
                var diagnostic = Diagnostic.Create(Rule, syntax.GetLocation());
                context.ReportDiagnostic(diagnostic);
            }
        }
    }
}

SupportedDiagnosticsより上のものは見た通りのコンフィグなので、まぁ見たとおりに適当に弄っておけばいいでしょう。コード本体はInitializeです。ここで、対象のノードの変更があったら起こすアクションを登録します。で、まずいきなり難しいのは、何をRegisterすればいいのか!ということだったりして。そこで手助けになるのがSyntax Visualizerです。入れましたか?入れましたよね?View -> Other Window -> Roslyn Syntax Visualizerを開くと、あとはエディタ上で選択している箇所のSyntaxTreeを表示してくれます。例えば、今回の対象であるusingの部分を選択すると「using System.Linq;」は……

と、いうわけで、たかがusingの一行ですが、めっちゃいっぱい入ってます。Node(でっかいの), Token(こまかいの), Trivia(どうでもいいの)というぐらいに覚えておけばいいでしょう(適当)。さて、というわけでusingの部分はUsingDirectiveであることが大判明しました。これ以外にもとにかくSyntaxTreeの操作は、何がどこに入ってて何を置換すればいいのかを見極める作業が必要なので、Syntax Visualizerはマストです。めっちゃ大事。めっちゃ助かる。超絶神ツール。

あとは、まぁ、見たまんまな感じで、これで警告は出してくれます。WarningじゃなくてErrorにしたいとか、Infoにしたいとかって場合はRuleからDiagnosticSeverityを変えればOK。

CodeFixProvider

続いてCodeFixProviderに行きましょう。まずはコード全体像を。

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
 
namespace UseLinqEx
{
    [ExportCodeFixProvider("UseLinqExCodeFixProvider", LanguageNames.CSharp), Shared]
    public class UseLinqExCodeFixProvider : CodeFixProvider
    {
        public sealed override ImmutableArray<string> GetFixableDiagnosticIds()
        {
            // このDiagnosticIdでAnalyzerと起動するCodeFixProviderが紐付けられてる
            return ImmutableArray.Create(UseLinqExAnalyzer.DiagnosticId);
        }
 
        public sealed override FixAllProvider GetFixAllProvider()
        {
            return WellKnownFixAllProviders.BatchFixer;
        }
 
        public sealed override async Task ComputeFixesAsync(CodeFixContext context)
        {
            // ドキュメントのルート
            var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
 
            var diagnostic = context.Diagnostics.First(); // 警告だしてるとこ
            var diagnosticSpan = diagnostic.Location.SourceSpan; // の、ソース上の位置みたいなの
 
            // ↑を使って、目的のモノを見つける独自コードを書く!
            // 何が何だか分からないので、ウォッチウィンドウで手探るに書きまくって探し当てるといいでしょふ
 
            // "UsingDirectiveSyntax UsingDirective using System.Linq;" が見つかる
            var usingDirective = root.FindNode(diagnosticSpan);
 
            // で、作って登録する
            var codeAction = CodeAction.Create("ReplaceTo System.LinqEx", c => ReplaceToLinqEx(context.Document, root, usingDirective, c));
            context.RegisterFix(codeAction, diagnostic);
        }
 
        static Task<Document> ReplaceToLinqEx(Document document, SyntaxNode root, SyntaxNode usingDirective, CancellationToken cancellationToken)
        {
            // たんなるusingDirectiveでも、中にはキーワード・スペース、;や\r\nが含まれているので、
            // 純粋に新しいusingを作って置換するだけだと、付加情報がうまく置換できない可能性が高い
            // ので、(面倒くさくても)既存ノードからReplaceしていったほうが無難
            var linqSyntax = usingDirective.DescendantNodes().OfType<IdentifierNameSyntax>().First(x => x.ToFullString() == "Linq");
            var linqEx = usingDirective.ReplaceNode(linqSyntax, SyntaxFactory.IdentifierName("LinqEx"));
 
            // ルートのほうにリプレースリプレース
            var newRoot = root.ReplaceNode(usingDirective, linqEx);
            var newDocument = document.WithSyntaxRoot(newRoot); // ルート差し替えでフィニッシュ
 
            return Task.FromResult(newDocument);
        }
    }
}

ここでの作業は、変更対象のノードを見つけることと、差し替えることです。ノードを見つけるための下準備に関しては、とりあえずサンプルコードのまんま(diagnostic/diagnosticSpan)でいいかな、と。そこから先は独自に探し出す必要があります。今回はUsingDirectiveを見つけたかったんですが、幸いルートからのFindNode一発で済みました、楽ちん。あとは置換するだけです。

置換に関しては、コード上に書いたように、大きい単位で新しいSyntaxNodeを作って差し替える、のはやめたほうがいいです。そうするとトリビアを取りこぼす可能性が高く、うまく修正かけられなかったりします。面倒くさくても、置き換えたいものをピンポイントに絞って置換かけましょう。ノードを探索するにはLINQ to XMLスタイルでのDescendantsやAncestors、ChildNodesとかがあります。LINQ to SyntaxTreeってところで、この辺はまさにLINQ to XMLとは何であるのか。ツリー構造に対するLINQ的操作のリファレンスデザインだと捉えることができるって感じですね。

さて、置換といっても、Roslynのコードは全てイミュータブル(不変)なので、戻り値をうまく使ってルートに伝えていく必要があります。Replace一発では済まないのです。これは面倒くさいんですが、まぁ慣れればこんなものかなー、と思えるでしょう、多分きっと。

ともあれ、これで出来上がりました!ちなみにデバッグはVsixプロジェクトをデバッグ実行すれば、拡張ロード済みの新しいVSが立ち上がる&アタッチされているので、サクッとデバッグできます。これは相当楽だし助かる(いかんせん慣れないRoslynプログラムは試行錯誤しまくるので!)。また、生成物に関してはAnalyzersにdllを手配置もいいですが、ビルドプロジェクト自体に.nupkg生成が含まれているので、そいつを使ってもいいでせう。その辺のことはテンプレートに入ってるReadMe.txtに書いてあるので一回読んでおくといいかな。

Unityで使う

新しいVSが出ると拡張が対応してくれるか、が最大の懸念になるのですが、なんとVisual Studio Tools for Unity(VSTU/旧UnityVS)は初日から対応してくれました!まさにMicrosoft買収のお陰という感じで、非常に嬉しい。遠慮無くVisual Studio 2015 Preview Tools for Unityを入れましょう。VSTUについてはVisual Studio Tools for Unity(UnityVS) - Unity開発におけるVisual Studioのすすめを見てね。

基本的にはUnityのプロジェクトにも全く問題なくAnalyzerを追加できて解析できます。素晴らしい!んですが、問題が一点だけあります。それはVSTUはUnity側に何か変更があった時に.csprojを自動生成するんですが、その自動生成によってせっかく追加したAnalyzerも吹っ飛びます。Oh……。

という時のためにVSTUはProject File Generationという仕組みを用意してくれています。これによってプロジェクトとソリューションの自動生成をフックできます(ちなみに実例として、うちの会社ではソリューションにサーバーサイドとか色々なプロジェクトをぶら下げてるのでソリューション自動生成を抑制したり、Unityプロジェクト側にT4テンプレートを使った自動生成コードを入れているので、VSTUのcsprojの自動生成時に.ttファイルを復元してやったり、とか色々な処理を入れてます)

今回は自動生成で消滅するAnalyzerを復元してやる処理を書きましょう。Editor拡張として作るので、Editorフォルダ以下にProjectFileHook.csを追加し、以下のコードを追加。

using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using UnityEditor;
 
[InitializeOnLoad]
public class ProjectFileHook
{
    // necessary for XLinq to save the xml project file in utf8
    private class Utf8StringWriter : StringWriter
    {
        public override Encoding Encoding
        {
            get { return Encoding.UTF8; }
        }
    }
 
    static ProjectFileHook()
    {
        SyntaxTree.VisualStudio.Unity.Bridge.ProjectFilesGenerator.ProjectFileGeneration += (string name, string content) =>
        {
            // ファイルがない場合はスルー(初回生成時)
            if (!File.Exists(name)) return content;
 
            // 現在のcsprojをnameから引っ張ってきてAnalyzerを探す
            var currentContent = XDocument.Load(name);
            var ns = currentContent.Root.Name.Namespace;
            var analyzers = currentContent.Descendants(ns + "Analyzer").ToArray();
 
            // content(VSTUが生成した新しいcsprojにAnalyzerを注入)
            var newContent = XDocument.Parse(content);
            newContent.Root.Add(new XElement(ns + "ItemGroup", analyzers));
 
            // したのを返す
            using (var sw = new Utf8StringWriter())
            {
                newContent.Save(sw);
 
                return sw.ToString();
            }
        };
    }
}

nameにファイルパス、contentにVSTUが生成した新しいcsprojのテキストが渡ってくるので、それを使ってモニョモニョ処理。csprojはXMLなので、LINQ to XML使ってゴソゴソするのが楽ちんでしょう。

これでUnityでもRoslynパワーを100%活かせます!やったね!

まとめ

あんだけ盛大に吹聴してたわりには、コード解析とリファクタリングだけかよ……、という感はなきにしも非ずですが、そのかわりすっごく簡単に作れる、追加できる仕組みを用意してくれたのは評価できます(えらそう)。かなり便利なので、早速是非是非遊んでみるといいんじゃないかな、とオモイマス。

ところで今回の例、CodeFixProviderはナシにしてAnalyzerだけにして、AnalyzerのレベルをWarningではなくDiagnosticSeverity.Errorにすることで、「LINQ禁止」を暗黙のルールじゃなくコンパイル不可能レベルで実現できます。拡張メソッドを明示的に呼び出せば回避できますが、ルールにプラスしてEnumerableの静的メソッドも殺せば、もう完全に死亡!恐ろしい恐ろしい。あ、勿論やらないでくださいね!

LINQ to GameObjectによるUnityでのLINQの活用

Unityで、LINQは活用されているようでされていないようで、基本的にはあまりされていない気配を非常に感じます。もったいない!というわけじゃないんですが、以前に私の勤務先と別の会社さんとで勉強会/交流会したのですが、そこで作ったスライドがあるので(若干手直ししたものを)公開します。LINQについて全くの初心者の人向けにLINQの良さを説明しようー、みたいな感じです、でもちょびっとだけ踏み込んだ内容もね、みたいな。勉強会自体は5月ぐらいにやったので、ずいぶんと公開まで開いてしまった……。

LINQ in Unity from Yoshifumi Kawai

その私の勤務先(まどろっこしい言い方だ……)グラニでは会社間での勉強会は大歓迎なので、もし、やりたい!という人がいらっしゃいましたら是非是非私のほうまでー。オフィスは六本木にあるのでその周囲ほげkmぐらいまでなら出張りますです(他のオフィスを見てみたい野次馬根性)。私の持ちネタとしてはC#, LINQ, Rxぐらいならなんでもどこまでもいけます。

さて、そんなわけでLINQは有益です、という話なんですが(AOT問題はスライドに書きましたが頑張れば解決できる!)、LINQを活用するためにはデータソースを見つけなきゃいけません。逆にデータソースさえ見つかれば幾らでも使えます。今回(本題)着目したのはGameObjectのヒエラルキーで、これを丸々とLINQと親和性の高い形で探索/操作できるアセット「LINQ to GameObject」を作りました。GitHubでソースコード、Unity Asset StoreではFREEで公開しています。

実際のトコロ、多分、みんな絶対に作って手元に持ってるちょっとしたユーティリティ、です。これもまた一つの俺々ユーティリティ。ちょっと違うところがあるとしたら、このライブラリは、ツリー階層構造へのLINQスタイルの操作ということで、LINQ to XMLからインスパイアされていて、API体系を寄せています。既に実績があり、そして実際に良さが保証されている(LINQ to XMLは非常に良いのです!問題はXMLを操作する機会がJSONに奪われてしまって近年少なくなってしまっただけでLINQ to XML自体は非常に良い!)APIとほぼ同一なので、ある程度のクオリティが担保されている、ということでしょうか。

ツリー探索とLINQ

探索のAPIは図にすると、以下の様な形になっています。

起点を元に親方向(Parent/Ancestors)か、子孫方向(Child/Children/Descendants)か、兄弟方向(ObjectsBeforeSelf/ObjectsAfterSelf)かに並んでいるGameObjectをIEnumerable<GameObject>として列挙します。

// 以下の様な形に抽出される
origin.Ancestors();   // Container, Root
origin.Children();    // Sphere_A, Sphere_B, Group, Sphere_A, Sphere_B
origin.Descendants(); // Sphere_A, Sphere_B, Group, P1, Group, Sphere_B, P2, Sphere_A, Sphere_B
origin.ObjectsBeforeSelf(); // C1, C2
origin.ObjectsAfterSelf();  // C3, C4

これはXPath(今となっては懐かしい響き!)の「軸」と同じもので、考えられる全ての列挙方向/方法を満たしています。特徴的なのは全てがIEnumerable<GameObject>になることで、LINQ to Objectsとシームレスに繋がり、フィルタやコレクションの変形を連続して行うことができます。

// 子孫方向のゲームオブジェクトの近いものトップ5を配列に固める(ただforeachするだけなら配列にしなくていい/しないほうがいいよ!)
var nearObjects = origin.Descendants()
    .OrderBy(x => (x.transform.position - this.transform.position).sqrMagnitude)
    .Take(5)
    .ToArray();
 
// 子孫方向の全ゲームオブジェクトのうちtagが"foobar"のオブジェクトを破壊
origin.Descendants().Where(x => x.tag == "foobar").Destroy();
 
// 自分を含む子ノードからBoxCollider2Dを抽出
var colliders = origin.ChildrenAndSelf().OfComponent<BoxCollider2D>();
 
// 全ての方法を組み合わせで満たせる、例えば兄弟方向に下のノードの全子孫はObjectsAfterSelf + Descendants
// これは ObjectsAfterSelf().SelectMany(x => x.Descendants()) のシンタックスシュガー
origin.ObjectsAfterSelf().Descendants();

SelectしてOrderByして、あれやこれや、なども自由に幾らでも行えます。また、繋がった状態での定形操作ということで、LINQ to GameObjectでは更にIEnumerable<GameObject>に対してDestoryとOfComponentという拡張メソッドを用意しています。それとちなみに性能面でも余計な中間コレクションを作らないため、(理屈上は)優位です。この理屈上ってのが、まぁ、あんまり踏み込みません:)

階層へのオブジェクトの追加

ところで利用法ですが、全てのメソッドはGameObjectへの拡張メソッドとして実装しています!暴力的に大雑把に!なので

using Unity.Linq;

としてもらえれば、全部のメソッドがにょきにょきっと生えます。メソッド一覧はリファレンスにあります。

オブジェクトを追加する、というのは階層を意識して追加していくわけですが、素でやるとparentにアタッチしてsiblingを弄って、というのは非常にカッタルイ話で絶対にみんな何とかゆーてぃりてぃを持っているとは思うのですが、LINQ to GameObjectにもあります。これも同様にLINQ to XMLと同じAPIを採用し、全ての方向/方法を網羅しています。

var root = GameObject.Find("root"); 
var cube = Resources.Load("Prefabs/PrefabCube") as GameObject; 
 
// Addは子の末尾に追加
// Parentの設定の他にレイヤーの統一とlocalPosition/Scale/Rotationを調整します
// 追加された子はCloneされていて、戻り値はそのCloneされたものを返します
var clone = root.Add(cube);
 
// 兄弟方向、自分の下に追加
// オブジェクトを追加する際は配列で渡せば複数一気に追加され、クローンされたオブジェクトをListで受け取れます
var clones = root.AddAfterSelf(new[] { cube, cube, cube });  
 
// 他にAddFirst(子の先頭に追加)とAddBeforeSelf(兄弟方向、自分の上)がある
// 追加の向きとしてはこれで全パターンでしょう!
 
// ついでに(?)Destoryの拡張メソッドもあり
// nullかどうかのチェック + 一旦階層から外してDestoryします
root.Destroy();

Addなんていう超汎用的くさい名前をGameObjectへの拡張メソッドにするってのがすっごく極悪なんですが、まぁいっか、みたいな。いいんですかね、いや、いいでしょう、はい、多分、うん。

LINQ to XMLはツリーへのLINQ的操作のリファレンスデザイン

LINQ to BigQuery - C#による型付きDSLとLINQPadによるDumpと可視化で、LINQの定義を

LINQがLINQであるためにはクエリ構文はいらない。Query Providerもいらない。LINQ to XMLがLINQなのは何故?Parallel LINQがLINQであるのは何故?Reactive ExtensionsがLINQであるのは何故?linq.jsがLINQであるのは何故?そこにあるのは……、空気と文化。

LINQと名乗ること自体はマーケティングのようなもので、形はない。使う人が納得さえすれば、LINQでしょう。そこにルールを求めたがる人がいても、ないものはないのだから規定しようがないよ?LINQらしく感じさせる要素をある程度満たしてればいい。FuncもしくはExpressionを使ってWhereでフィルタしSelectで射影する(そうすればクエリ構文もある程度は使えるしね)。OrderBy系の構文はOrderBy/OrderByDescending/ThenBy/ThenByDescendingで適用される。基本的な戻り値がシーケンスっぽい何かである。うん、だんだん満たせてくる。別に100%満たさなくても、70%ぐらい満たせばLINQらしいんだよ。

極論言えば私がLINQだって言ってるんだからLINQなのですが(何か文句ある?)、多くの人には十分納得してもらえると考えています

と、かなり乱暴な感じに「勝手に」定義しましたが(つまり今回のLINQ to GameObjectも私がLINQだって言ってるんだからLINQなのだ!)、実際、LINQ to GameObjectからはLINQらしさを感じ取れるんじゃないかと思います。何故か?当然理由はあるし、そうなるように意識してデザインしてます。

LINQ to XMLとは何であるのか。ツリー構造に対するLINQ的操作のリファレンスデザインだと捉えることができます。ツリー構造はLINQになる、そのガイドライン。LINQ to Objectsと非常に相性の良い探索の抽象化。もちろん、それ自体はXML向けだけど、「軸」を意識すればJSONにも適用できるし、そして、LINQ to GameObjectにも適用できた。

もしツリーを見かけたら、そこにLINQがなかったら、同じように作ることができるし、そうすればLINQの全てのメリットを甘受できる。データソースを発見していくこと。これは視点の問題で、そう捉えれば見えるようになる。それがLINQをただ漠然と使うことから一歩踏み出せるんじゃないのかな。

ユニットテスト

今回はじめてUnity Test Toolsをちょろっとだけ使ってみました。UniRxではファイルをリンクとしてコピーして普通の.NET上、Visual Studio上のMSTestで動かすという荒っぽいことをしてて、それはそれで楽ちんでいいんですけど、GameObjectへの操作とかUnityEngineに依存するものはさすがに無理で、今回のライブラリは100%それなので困ったなー、と。で、そこで、Unity Test Toolsの出番だったわけですね。

うん、まあ、普通ですね!いや、普通で、普通に悪くないです。ロジックのテストには全然いい。便利だよ。で、アサーションは普通にNUnitなんですが、私はAssert.AreEqualとかAssert.Thatとか嫌いなのです!嫌いなのでふつーのC#用にはChaining Assertionというライブラリを作って/公開しているんですが、それのUnity版を用意しました。

AssetStoreに投稿するほどのものかなーってことで、とりあえずLINQ to GameObjectのリポジトリ内にあるだけなんですが、気になる人はEditor/ChainingAssertion.csをどうぞ。この.csファイル一個だけです。これで

[Test]
public void Children()
{
    Origin.Children().Select(x => x.name)
        .IsCollection("Sphere_A", "Sphere_B", "Group", "Sphere_A", "Sphere_B");
 
    Origin.Children("Sphere_B").Select(x => x.name)
        .IsCollection("Sphere_B", "Sphere_B");
 
    Origin.ChildrenAndSelf().Select(x => x.name)
        .IsCollection("Origin", "Sphere_A", "Sphere_B", "Group", "Sphere_A", "Sphere_B");
 
    Origin.ChildrenAndSelf("Sphere_A").Select(x => x.name)
        .IsCollection("Sphere_A", "Sphere_A");
}

みたいな感じにテスト書けます/書きました。

まとめ

LINQは良い。LINQを使うにはIEnumerableが必要。LINQ to GameObjectはそのIEnumerableを作り出すので、UnityでよりLINQを活用できる!ので使いましょう。

(ところでObjectsAfterSelfはAfterSelfのほうが良いですね……すっごく失敗した……次のバージョンで変更するかも、というかします、はい……)。

あとLINQ to XMLの大きな要素として関数型構築があるんですが、勿論LINQ to GameObjectにも用意しました!ただ、実用性は(LINQ to XMLと違ってUnityの性質上)ビミョーなので、ここでは紹介しません。とりあえずとにかく、「ツリーを上下左右に探索できて」「ツリーの上下左右に追加できて」「関数型構築できれば」ツリーへのLINQ。と言えます、きっと。

それとUniRxなんですが、スライドで少し触れましたが、uFrameというUnity用のフレームワークに採用されて同梱されるようになりました。結構いい具合に躍進してきてるんで、UniRxも是非是非チェックを。ブログは全然書いてないんですが(!)機能拡充はずっと続けているんで、また近いうちに何か書きませう。

RxJava Night振り返り(Reactive Extensionsの歴史)

RxJava Nightというイベントで、Rxの歴史!というほど大仰なものでもないですが、誕生から今に至るまでをサラッと振り返った資料でLTしてきました。

The History of Reactive Extensions from Yoshifumi Kawai

普段は時間オーバー常習犯なんですが、今回はちゃんと時間を意識して収めましたよ!中身的にはまだまだアレもコレも詰めたい欲もあったりなかったりですが、まぁむしろこのぐらいが丁度良いのかもしれません。幸い、わりかし評判も良かったようで何よりです。

Rx across languages

改めて実感したのは、もうRxは.NETだけのものじゃない、ということです。むしろ他言語のほうがずっと盛り上がっているというのは実感するところで、ReactiveCocoa、そしてRxJava。こちらのほうが熱い。それはもう事実として。勿論いいことです。とはいえそうなると、もはや.NETのReactive Extensionsの存在すら知らない人も沢山いるというところなので、そこを少し知ってもらえたら何よりですね。会場のマック率が99%だったりC#erが2人ぐらいだったりと、ひぢょーにゲンジツを感じました!

もしこれ、RxJavaがRx***という名前付けないで別の名前だったら、それで普及して、完全にReactive Extensionsに言及されることはなくなっていたんだろうなあ、ということを思うと、名前が残って良かった良かった(笑)

.NETでRxがそんなに目立って使われないのはいくつか理由がなきにしもあらずなんですが、一番大きな理由は、今回の勉強会でも一番大きく言及されていた非同期関連においてはそんなに重宝しない、というとこかなあ、と思います。重宝しないというか、C# 5.0でasync/awaitが搭載されたので、そちらでやったほうがかなりスッキリ書けるという。

並列処理に関してはgihyoでのグラニがC#にこだわる理由 第1回 神獄のヴァルハラゲートの裏側をCTOが語り尽くす!の図の1の部分を見てもらいたいのですが、さっくり書けてますよね、と。コードは

var frontHPs = await field.OwnGuild.Members
    .Where(x => x.Position == Position.Front)
    .Select(async x => new
    {
        Name = await x.Name,
        CurrentHP = (await x.UserStatus).CurrentHP
    })
    .WhenAll();

とかね。これも含めてサーバーサイド全般での活用に関してはAWS Summit Tokyo 2014で発表した以下の資料をどうぞ。

AWS + Windows(C#)で構築する.NET最先端技術によるハイパフォーマンスウェブアプリケーション開発実践 from Yoshifumi Kawai

非同期系はそれとして、今回Androidのかたが多かったようにバインディングとかはどーなのか、というと、もともとC#にはView側がXAMLというバインディング前提のHTMLみたいなUIマークアップ言語があったので、Rxにフルに頼る必要がない、という事実はあります。なので、そこまで切羽詰まってない、みたいな。もちろん、組み合わせて使うというのは有り得るパターンで、GitHubでも使われている(というか作ってる人が中の人な)ReactiveUIや、私の作った(現在の機能向上は完全にokazukiさんに渡してます)ReactivePropertyといったライブラリもあります。それらのAndroid(+ Xamarin)への活用はamay077さんがXamarin.Forms と ReactiveProperty で快適MVVM生活といった記事も書かれていますし、色々ありますねというか、別に冷め切ってるわけでもなくて、やっぱRx熱いよ!ってのは全然あります!はい!

LINQ

C#erがRxを理解するにはLINQから入って考えるんですが、他言語の人はRxから入るんですよね。その辺が一番大きなギャップかもしれません。私的にはLINQから入って、IEnumerableとIObservableの関係性とか意識しながらのほうがスムースだったんですが(例えばIObservableでScanを考えるのは大変、このRxJava WikiのScanの図の意味不明さ!でも、IEnumerableでScanを考えるのは、まだ容易!)どーなんでしょふ。別にLINQじゃなくてStream APIとか、自分の言語のコレクション処理と少し付きあわせてみるといいかなあ、というのは本当に思っています。なぜ双対のソの字も出してないか。IEnumerableが前提じゃないから。そこの説明をする時間はない!(Erik Meijerの起こした会社の名前、Applied Dualityは勿論dualityから来てる)

Rxが流行るには?

懇親会で話したことなんですが、どーなんですかねえ、RxJava流行りますか!?個人的には勿論流行って欲しいんですが!言語関係なく共通のお話ができますし、悩みも活用も応用例も、言語が増えれば増えるほど盛り上がる、嬉しい、んですが、実際どうでしょう。まず、ラムダは必須。無名クラスで書くのは無理ですねえ。こういうの、IDEの自動生成でなんとかなるものとなんともならないものがあって、Rxぐらいラムダを使いまくるものは無名クラスで自動生成しまくると、生成後のブツの可読性が悪すぎて辛すぎます。AndroidではまだJava8対応していないようですし、いつするかもわからないということで、辛いですねえ。Groovyのほうがまだ可能性はありそうだけど、AndroidでGroovy、どうなんでしょう、それはそれでそれもまた流行るための壁が二段階増えてる感は否めない気もする。

学習コストは間違いなく大きい。うーん、Streamの時点でもそれなりに高いとは思うんですが、でもやってやれないこともないし、慣れてしまえば凄まじく便利でOKだと思います。ただ、Rxもそれと同じといえるかというとそうでもない。次元が1個増えたような感じなんですよね、シーケンス的な考え方に「時間」と「スレッド」の概念が混ざってくるので、慣れてても複雑に絡み合ったRxのチェーンを読み解くのは大変。二次元なら見えるけど四次元は辛いよね?的な。ただ、それはじゃあ普通に書いても複雑なステートになっているはずなので、Rxが悪いわけじゃあないといえばないんですがねえ。

Rxで非同期やるなら、下の層から上の層までIObservableで通す必要があると思っていて(そうでなければ途中の層でブロックしているということだ!)、ある種の非同期汚染みたいなのが発生します。これはC# 5.0のasync/awaitにもいえて、下から上までTask(Future/Promiseみたいなの)が貫く必要がある。そういった根本的な変化が生じるので、やるんならむしろ徹底的にやってしまったほうが大きな結果が得られるかな、と。

最後に全く関係なくそういえば懇親感で少し話したUnityでのLINQのAOT問題。Unity + iOSのAOTでの例外の発生パターンと対処法ではmonoからEnumerable.csを持ってきたらどーよ?と書いたんですが、OrderByは落ちます。で、うちの会社では(OrderBy以外にも)それなりに手を加えて調整しまくって、今現在はほとんど落ちない状態になっているんですが、とりあえずOrderByの改造点だけ。以下の様な変更をいれれば大丈夫です。

-abstract class OrderedEnumerable<TElement> : System.Linq.IOrderedEnumerable<TElement>
+public abstract class OrderedEnumerable<TElement> : IEnumerable<TElement>, IEnumerable
 
-abstract class SortContext<TElement> : IComparer<int>
+public abstract class SortContext<TElement> : IComparer<int>
 
-enum SortDirection
+public enum SortDirection

IOrderedEnumerableだけを露出させると危ないので、abstract classをpublicにしちゃいます。そうした調整のせいでSortContextとか本来はprivateにしてたいものも露出してっちゃって望ましくないんですが、まぁそこは背に腹は代えられないということで妥協しましょう。妥協は大事です。理想よりも現実が一番偉いんです。

BigQueryで数列生成とC#クラスからのTable生成とデータインサート

連番を作りましょう!突然!SQL的なものを見ると、まず連番を作りたくなるのはSQLで数列を扱うからなのですが、というわけでBigQueryでも作りますし作れます。実際、Enumerable.Rangeはダイジですからね?また、地味にLINQ to BigQueryもver 0.3.3になってました。ひっそり。そんなわけで、LINQで書くと何が嬉しいのかPart2です。

LINQ to BigQuery(やBigQuery)については、最初の記事LINQ to BigQuery - C#による型付きDSLとLINQPadによるDumpと可視化をどーぞ。

0-9を作る

TempTableにInsertというわけにもいかないので、まずは愚直にUNION ALLで並べましょう。BigQueryのUNION ALLはFromをカンマで並べること(ふつーのSQLとそこが違います)で、また、Subqueryも突っ込めます。ド単純に書くとこうなる。

// 以下contextとでてきたらコレのこと
var context = new BigQueryContext(/* BigqueryService */, /* projectId */);
 
var seq = Enumerable.Range(0, 10).Select(x => context.Select(() => new { num  = x }));
context.From(seq)
    .Select(x => new { x.num })
    .Run()
    .Dump(); // DumpはLINQPadのDumpね。
 
// ↓で、こんなクエリが出てくる
/*
SELECT
  [num]
FROM
(
  SELECT
    0 AS [num]
),
(
  SELECT
    1 AS [num]
),
// 以下9まで続くので(略) */

普通に動きはしますが、馬鹿っぽいですね!少しだけカッコヨク書いてみましょうか。どうやって列を増やすか、が割と課題なのですが、BigQueryではSplitを使って増やせます。

// LINQPadでRun().Dump()って書くの面倒いのでまとめちゃう:)
public static class MyExtensions
{
    public static QueryResponse<T> DumpRun<T>(this IExecutableBigQueryable<T> source)
    {
        return source.Run().Dump();
    }
}
 
// SELECT query which references non constant fields or uses aggregation functions
// or has one or more of WHERE, OMIT IF, GROUP BY, ORDER BY clauses must have FROM clause.
context.Select(() => new { digit = BqFunc.Integer(BqFunc.Split("0123456789", ""))}).DumpRun();

怒られました!FROM句を含めないとSplitが使えないそーですなので、wordはサブクエリに分離しましょう。この辺は覚えられないので怒られたらそーいうものなんだ、って感じに対応していきましょふ。案外エラーメッセージは(親切な時は)親切です。親切じゃない時は何言ってるのか分からないエラーメッセージを吐いてきますが、まぁ7割ぐらいは分かりやすいエラーメッセージを吐いてくれます、偉い。

context
    .Select(() => new { word = "0123456789" })
    .AsSubquery()
    .Select(x => new { digit = BqFunc.Integer(BqFunc.Split(x.word, ""))})
    .DumpRun();

さすがにFROM句に並べまくるよりは、綺麗に書けてる感が出てる気がします!

0-99を作る

0-9が出来たら、あとは簡単に増やせます。ここはCROSS JOINです。0-9と0-9の直積を取ればおk。LINQでBigQueryを書くことの利点に変数にクエリを渡せて、合成可能という点が挙げられます(また、合成可能というのはLINQらしい感じさせるための重要な要素でもある)。0-9を変数に置いてやれば、コピペで同じSQLを書かないでも済みます。

var digit = context.Select(() => new { word = "0123456789" })
    .Into()
    .Select(x => new { digit = BqFunc.Integer(BqFunc.Split(x.word, ""))});
 
// これは動かないけどネ
// Cannot query the cross product of repeated fields 
digit.Into()
    .JoinCross(digit, (d1, d2) => new { d1, d2 })
    .Select(x => new { seq = x.d1.digit + x.d2.digit * 10 })
    .DumpRun();

ネ。まぁこれは動かないんですけどネ。例によってエラーメッセージが出てから対処すればいいんですが、これはSplitで生成したカラムがrepeated fieldになってるのでcross joinできないよ、とのこと。FLATTENを使えば解決します。あとOrderByを忘れてるのでOrderByも足してやりましょうか。

var digit = context.Select(() => new { word = "0123456789" })
    .Into()
    .Select(x => new { digit = BqFunc.Integer(BqFunc.Split(x.word, ""))})
    .Into()
    .Flatten(x => x.digit);
 
digit.JoinCross(digit, (d1, d2) => new { d1, d2 })
     .Select(x => new { seq = x.d1.digit + x.d2.digit * 10 })
     .OrderBy(x => x.seq)
     .DumpRun();
SELECT
  ([d1.digit] + ([d2.digit] * 10)) AS [seq]
FROM FLATTEN(
(
  SELECT
    INTEGER(SPLIT([word], '')) AS [digit]
  FROM
  (
    SELECT
      '0123456789' AS [word]
  )
), [digit]) AS [d1]
CROSS JOIN FLATTEN(
(
  SELECT
    INTEGER(SPLIT([word], '')) AS [digit]
  FROM
  (
    SELECT
      '0123456789' AS [word]
  )
), [digit]) AS [d2]
ORDER BY
  [seq]

この辺まで来ると、圧倒的に手書きよりも捗るのではないでしょうか。というか、LINQならサクサク書けますが(エラー来たら、ああはいはいIntoね、みたいに対処するだけだし)、手書きSQLはシンドイ。むしろ無理。その上で、別に意図と全然違うクエリが吐かれるわけではない、というラインはキープされてると思います。

それとネストが深くなるクエリはどう整形したらいいか悩ましいものなのですが(Stackoverflowには可読性ゼロのめちゃくちゃなインデントのBigQueryのクエリの質問が沢山転がっている!実際きちんと書くのむつかしい)、LINQ to BigQueryは、まぁまぁ読みやすい感じにきっちりフォーマットして出してくれます。若干冗長に思えるところもあるかもですが、まぁそこはルールなのだと思ってもらえれば。見やすいフォーマットといえるものにするため、微調整を繰り返したコダワリがあります。

パラメータを使う

もう一個LINQ to BigQueryのいいとこは、パラメータが使えるとこです。パラメータというか、クエリ文字列にたいして値を埋め込めるの。例えば

// こんなメソッドを作るじゃろ
Task<string[]> GetTitleBetweenRevision(int revisionIdFrom, int revisionIdTo, int limit)
{
    return context.From<wikipedia>()
        .Where(x => BqFunc.Between(x.revision_id, revisionIdFrom, revisionIdTo))
        .Select(x => x.title)
        .Limit(limit)
        .ToArrayAsync();
}
 
// こういうふうに使いますね、的な 
var rows = await GetTitleBetweenRevision(1, 200, 100);
-- 1と200が文字列置換なくSQLに埋め込まれる
SELECT
  [title]
FROM
  [publicdata:samples.wikipedia]
WHERE
  ([revision_id] BETWEEN 1 AND 200)
LIMIT 100

その場でのクエリ書きには使いませんが、プログラムに埋め込んで発行する場合なんかは当然ながらあるといいですよね、と。文字列置換や組み立てはかなり手間かかるので、ずっとぐっと遥かに楽になれるかと思います。LINQなら条件によってWhereを足したり足さなかったり、みたいな書き方も簡単です。

(この機能は0.3.1から入れました!アタリマエのように見えて、ExpressionTreeを操作する上で、地味に微妙に面倒くさいのですよー。とはいえ実用性考えるとこういうのないとアリエナイというか私が使ってて不便したんでようやっと入れました)

クエリ書きに使うのに便利といえば日付の操作は圧倒的に楽になります。例えば昨日の20時というのをBigQueryだけでやると……

context // 走査範囲を狭くするために適当に5日前ぐらいからのRangeにしてる
    .From<github_timeline>("[githubarchive:github.timeline]").WithRange(TimeSpan.FromDays(5))
    .Where(x => x.type=="CreateEvent" 
        && BqFunc.ParseUtcUsec(x.repository_created_at) >= BqFunc.ParseUtcUsec(BqFunc.StrftimeUtcUsec(BqFunc.TimestampToUsec(BqFunc.DateAdd(BqFunc.UsecToTimestamp(BqFunc.Now()), -1, IntervalUnit.Day)), "%Y-%m-%d 20:00:00"))
        && x.repository_fork == "false"
        && x.payload_ref_type == "repository")
    .Select(x => x.repository_name)
    .DumpRun();
 
// SQL
SELECT
  [repository_name]
FROM
  [githubarchive:github.timeline@-432000000-]
WHERE
  (((([type] = 'CreateEvent') AND (PARSE_UTC_USEC([repository_created_at]) >= PARSE_UTC_USEC(STRFTIME_UTC_USEC(TIMESTAMP_TO_USEC(DATE_ADD(USEC_TO_TIMESTAMP(NOW()), -1, 'DAY')), '%Y-%m-%d 20:00:00')))) AND ([repository_fork] = 'false')) AND ([payload_ref_type] = 'repository'))

結構しんどいです。厄介な日付部分を取り出すと

PARSE_UTC_USEC(STRFTIME_UTC_USEC(TIMESTAMP_TO_USEC(DATE_ADD(USEC_TO_TIMESTAMP(NOW, -1, 'DAY')), '%Y-%m-%d 20:00:00'))))

ですからね!結構かなり絶望的……。これをC#のDateTimeで操作すれば

// 今日から一日引いてその日付のみのほうを取って20時間足す
var yesterday = DateTime.UtcNow.AddDays(-1).Date.AddHours(20);
 
context
    .From<github_timeline>("[githubarchive:github.timeline]").WithRange(TimeSpan.FromDays(5))
    .Where(x => x.type=="CreateEvent" 
        && BqFunc.Timestamp(x.repository_created_at) >= yesterday // ほら超スッキリに!
        && x.repository_fork == "false"
        && x.payload_ref_type == "repository")
    .Select(x => x.repository_name)
    .DumpRun();
 
// 日付比較部分のSQLはこう出力される
TIMESTAMP([repository_created_at]) >= '2014-10-03 20:00:00.000000')

その場で書いてクエリ実行する分には、別に日付が埋め込まれようとNOW()からSQLで全部操作しようと変わらない話ですからね。楽な方でやればいいし、日付操作は圧倒的にC#で操作して持ってたほうが楽でしょう、明らかに。

Tableを作る、データを投げる

サンプルデータを扱ってるのもいいんですが、やっぱ自分でデータ入れたいですね、テーブル作りたいですね。基本的には(Google API SDKの)BigqueryServiceを使え!っていう感じなのですが、それはそれでやっぱりそれもプリミティブな感じなので、テーブル作成に関してはちょっとしたユーティリティ用意してみました。以下の様な感じで作れます。

// DataTypeUtility.ToTableFieldSchemaでTableFieldSchema[]を定義から作れる
// 匿名型を渡す以外に既存クラスだったら<T>やtypeof(T)を渡すのもOK
// もちろん手でTableFieldSchema[]を作って渡すのも構わない
new MetaTable("project_id", "mydata", "people")
    .CreateTable(service, DataTypeUtility.ToTableFieldSchema(new
    {
        firstName = default(string), // STRING REQUIRED
        lastName = default(string), // STRING REQUIRED
        age = default(int?), // INTEGER NULLABLE
        birth = default(DateTimeOffset) // TIMESTAMP REQUIRED
    }));

Web Interfaceから作ると、「空のテーブルが作れない」「スキーマはなんかカンマ定義で指定してかなきゃいけなくてダルい」という点があって存外ダルいです。bqも同様。やはり時代はLINQPad、で作る。ちなみにSTRING NULLABLEはクラス定義から抽出するのが不可能だったので(こういうところが不便なのよね……)、まあTableFieldSchema[]を作ってから schemas[1].Mode = “NULLABLE” とでも書いてください。

データの投下も同じようにMetaTableを作ってInsertAllAsyncで。

// ExponentialBackOffを渡した場合はそれにのっとってリトライをかける
await new MetaTable("project_id", "mydata", "people")
    .InsertAllAsync(service, new[]
    {
        new { firstName = "hoge", lastName = "huga", age = 20, birth = new DateTime(2010,1,1,12,13,14, DateTimeKind.Utc)},
        new { firstName = "tako", lastName = "bcbc", age = 30, birth = new DateTime(1983,3,1,10,33,24, DateTimeKind.Utc)},
        new { firstName = "oooo", lastName = "zzzz", age = 45, birth = new DateTime(2043,1,3,11,4,43, DateTimeKind.Utc)},
    }, new Google.Apis.Util.ExponentialBackOff(TimeSpan.FromMilliseconds(250), 5));

これでBigQueryのStreming Insertになります。ひどーきなので別テーブルに並走して書きたい場合は複数書いてWhenAllすれば高速で良いでしょふ。Streaming Insertはそんな頻繁、ではないですけれどそれなりに失敗することもあるので、引数にExponentialBackOff(これ自体はGoogle API SDKに含まれている)を渡せばExponential backoffでリトライを試みます。

まとめ

基本的な機能は完全に実装完了したかなあ、という感じ。0.1 ~ 0.3.3の間に自分で使っててイラッとした細かい部分をチクチク修正してきましたが、そろそろ完全に満足!といったところです。不満ない!完璧!パーフェクち!というわけで、残るはRECORD型サポートに向けて改装すれば敵なし、LINQったらサイキョーね!

な、わけですが、まぁ.NET + BigQueryというニッチに二乗かけたようなアレなので、興味関心、はあっても使ってみた!という人は少ないでしょう、というかいないでしょう、残念無念。でもBigQueryは本当に凄く良いので使ってみて欲しいんだなー。ビッグデータなんてアタクシには無縁、と思ってる人も、実は使い出、使いドコロって、絶対あります。まずはログを片っ端から突っ込んでみましょう、から始めてみませんか?

LINQ to BigQuery - C#による型付きDSLとLINQPadによるDumpと可視化

と、いうものを作りました。BigQueryはGoogleのビッグデータサービスで、最近非常に脚光を浴びていて、何度もほってんとりやTwitterに上がってきたりしてますね。詳細はGoogle BigQuery の話とかGoogleの虎の子「BigQuery」をFluentdユーザーが使わない理由がなくなった理由あたりがいいかな、超でかいデータをGoogleパワーで数千台のサーバー並べてフルスキャンするから、超速くて最強ね、という話。で、実際凄い。超凄い。しかも嬉しいのが手間いらずなところで、最初Amazon RedShiftを検討して試していたのですが、列圧縮エンコードとか考えるのすっごく大変だし、容量やパワーもインスタンスタイプと睨めっこする必要がある。それがBigQueryだと容量は格安だから大量に格納できる、チューニング設定もなし、この手軽さ!おまけにウェブインターフェイスが中々優れていてクエリが見やすい。Query Referenceもしっかり書かれてて非常に分かりやすい。もう非の打ち所なし!

触ってすぐに気に入った、んですが、C#ドライバがプリミティブすぎてデシリアライズすらしてくれないので、何か作る必要がある。せっかく作るならSQLっぽいクエリ言語なのでLINQだろう、と。それとIQueryableは幻想だと思っていたので、じゃあ代替を作るならどうするのか、を現実的に示したくて、ちょうど格好の題材が出現!ということで、LINQで書けるようなライブラリを作りました。

ダウンロードは例によってNuGetからできます。今年はそこそこ大きめのライブラリを作ってきていますが、LINQ to BigQueryは特に初回にしては大きめで割と充実、非常に気合入ってます!是非使ってみてねー。GitHubのReadMe.mdはこのブログ記事で力尽きたので適当です、あとでちゃんと書く……。

簡単なDEMO

BigQueryの良い所にサンプルデータが豊富というところがあります、というわけでGitHubのデータを扱って色々集計してみましょう。データは[publicdata:samples.github_timeline]を使ってもいいのですが、それは2011年時点のスナップショットでちょっとツマラナイ。GitHub Archiveから公開データを引っ張ってくれば、現時点での最新の、今ついさっきのリアルタイムの情報が扱えて非常に素敵(あとBigQueryはこういうpublicなDataSetが幾つかあるのが本当に最高に熱い)。ひっぱてくるやり方は書いてありますが(超簡単)、テーブル名は[githubarchive:github.timeline]です。

まずは単純なクエリということで、プログラミング言語だけでグループ化して個数を表示してみます。github.timelineは、例えばPushしたとかBranch作ったとか、雑多な情報が大量に入っているので、別にリポジトリ数のランキングではなくて、どちらかといえばアクティビティのランキング、ぐらいに捉えてもらえれば良さそうです。とりあえずトップ5で。

この例では記述と表示はLINQPadで行っています。LINQPadは非常に優れていて、C#コードが入力補完付きでサクッと書けるほか、実行結果をDumpして色々表示させることも可能です。DumpChartはLINQ to BigQueryのために独自に作ったDumpなのですが、それにより結果のグラフ化がXとYを指定するだけのたった一行

.DumpChart(x => x.repository_language, x => x.count)

だけで出来てしまう優れものです。描画は.NET標準のチャートライブラリを使っているため、棒グラフの他にも円グラフでも折れ線グラフでも、SeriesChartTypeにある35個の表示形式が選べます。見たとおり、Tooltip表示もあるので個数が大量にあっても全然確認できるといった、チャートに求められる基本的な機能は満たしているので、ちょっとしたサクッと書いて確認する用途ならば上等でしょう。

(DumpChartやQuery.GetContextのコードはこの記事の末尾にコード貼り付けてあるので、それで使ってください)

Resultsタブのほうを開けば、クエリ結果の詳細が見れます。

クエリ文字列はBigQueryの性質上、色々なところで使うはずです。そうした他所で使える可搬性のために、生成結果を人間の読める綺麗なものにする事にこだわりました(TypeScript的な)。純粋なクエリビルダとして使う(ちなみにToString()すればRunしなくてもクエリを取り出せます)ことも十分可能でしょう。Rowsに関しては切り離してグリッド表示も可能で、そうすれば簡単なソートやCSVへの書き出しといった、データベース用IDEに求められる基本的な機能も満たしています。

TotalBytesProcessedが読みづらかったのでひゅーまんりーだぶるな形に直してあるのも用意してあるところが優しさ(普通に自分が使ってて困ったので足しただけですが)。

BigQueryはウェブインターフェイスが非常に優れている、これは正直感動ポイントでした。いやぁ、RedShift、データベース管理用のIDEがろくすっぽなくて(PostgreSQL互換といいつつ違う部分で引っかかって動かないものが非常に多い)どうしたもんか、と苦労してたんですが、BigQueryはそもそも標準ウェブインターフェイスが超使いやすい。スキーマも見やすいしクエリも書きやすい。まさに神。

てわけでウェブインターフェイスには割と満足してるんですが、表示件数をドバッと表示したかったり、グラフ化もサクッとしたいし(何気にGoogle SpreadSheet連携は面倒くさい!)、日頃からデータベースもSQL Server Management StudioやHeidi SQLといったデスクトップツールを使って操作するWindows野郎としては、デスクトップで使えるIDE欲しいですね、と。それに分析やる以上、結構複雑なクエリも書くわけで、そういう時に型が欲しいなーとは思ってしまったり。LINQ to BigQueryはAlt BigQuery Query、Better BigQuery Queryとして、ただたんにC#で書けます以上のものを追求しました。そして、LINQPadとの組み合わせは、現存するBigQuery用のIDEとして最も良いはずです(そもそもBigQuery用のIDEは標準ウェブインターフェイス以外にあるのかどうか説もあるけれど)。日常使い、カジュアルな分析にも欠かせない代物となることでしょう。

Why LINQ?

LINQ to BigQueryで書く場合の良い点。一つは型が効いているので、間違っていたらコンパイルエラーで(Visual Studioで書けばリアルタイムにエラー通知で)弾かれること。別にカラム名の名前間違いなどといったことだけじゃなくて、文字列であったりタイムスタンプであったりといった型も厳密に見えているので、型の合わない関数を書いてしまうといったミスもなくせます。例えばDate and time functionsの引数が文字列なのかタイムスタンプなのかUNIX秒なのか、そして戻り値もまた文字列なのかタイムスタンプなのかUNIX秒なのか、ってのは全く覚えてられないんですが、そんな苦痛とはオサラバです。

github_timelineのカラム数はなんと200個。さすがに覚えてられませんし、それの型だってあやふやってものです(例えばboolであって欲しいフォークされたリポジトリなのかを判定するrepository_forkというカラムには”false”といったような文字列でやってくるんですぜ!?)。

全ての関数はBqFuncの下にぶら下がっていて、引数と戻り値、それにドキュメント付きです。これなら覚えてなくても大丈夫!ちなみに、ということはクエリ中の全ての関数呼び出しにBqFunc.がついてきて見た目がウザいという問題があるのですが、それはC# 6.0のusing staticを使えば解決します。

// C# 6.0 Using Static
using BigQuery.Linq.BqFunc;

楽しみに待ちましょう(C# 6.0は多分2015年には登場するんじゃないかな?)。

LINQ to BigQueryはO/Rマッパーじゃありません。いや、もちろんクエリの構築やC#オブジェクトへのマッピングは行いますが、リレーションの管理はしません。かわりに、書いたクエリがほとんどそのままの見た目のクエリ文字列になります。なので意図しない酷いクエリが発行されてるぞー、というありがちななことは起きません。そして、LINQ to BigQueryで99%のクエリが記述できます、LINQで書けないから文字列でやらなきゃー、というシチュエーションはほぼほぼ起きません。LINQとクエリ文字列を1:1に、あえてほぼ直訳調にしているのはそのためです。

また、順序を強く規制してあります、無効なクエリ順序での記述(例えばGroupBy使わずにHaving書くとかLimitの後にWhere書いてしまうとか)やSelectなしの実行はコンパイルエラーで、そもそも書けないようにしています。

左はWhereの後のメソッド、これが全部でSelectとOrderByとWhere(ANDで連結される)しか使えない。右はSelect後で、GroupBy(奇妙に思えるかもしれませんが、GroupByの中でSelectの型が使えることを考えるとこの順序が適正)やLimit、そしてRunなどの実行系のメソッドが使えるようになっています。

これらにより、LINQ to BigQueryで書いたクエリは一発で実行可能なことが期待できるものが作れます(文字列で書くと、カラムの参照周りとかで案外つまづいてエラりやすい)。さすがにExpressionの中身は検査できないんですが、概ね大丈夫で、”守られてる感”はあるかと思います。ちなみにこんな順序で書けます。

From(+TableDecorate) -> Join -> Where -| -> OrderBy(ThenBy) -> Select ->                     | -> Limit -> IgnoreCase
                                       | -> Select | -> GroupBy -> Having -> OrderBy(ThenBy) | -> IgnoreCase
                                                   | -> OrderBy(ThenBy) ->                   |

そういうの実現するためにLINQ to BigQueryはIQueryableじゃないんですが、そのことはこの長いブログ記事の後ろのほうでたっぷりポエム書いてるので読んでね!あと、こんな割とザルな構成でもしっかり機能しているように見えるのは、BigQueryのSQLがかなりシンプルなSQLだから。標準SQLにできることは、あんま出来ないんですね。で、私はそこが気に入ってます。好きです、BigQueryのSQL。別に標準SQLにがっつし寄せる必要はあんまないんじゃないかなー、SQL自体は複雑怪奇に近いですから、あんまり良くはない。とはいえ、ある程度の語彙は共用されていたほうが親しめるので、そういったバランス的にもBigQueryのSQLはいい塩梅。

最後に、Table DecoratorsTable wildcard functionsが圧倒的に記述しやすいのも利点です。

// Table Decorators - WithRange(relative or absolute), WithSnapshot 
 
// FROM [githubarchive:github.timeline@-900000-]
.From<github_timeline>().WithRange(TimeSpan.FromMinutes(15))
 
// FROM [githubarchive:github.timeline@1411398000000000]
.From<github_timeline>().WithSnapshot(DateTimeOffset.Parse("2014-09-23"))
 
// Table wildcard functions - FromDateRange, FromDateRangeStrict, FromTableQuery
 
// FROM (TABLE_DATE_RANGE([mydata], TIMESTAMP('2013-11-10'), TIMESTAMP('2013-12-01')))
.FromDateRange<mydata>("mydata", DateTimeOffset.Parse("2013-11-10"), DateTimeOffset.Parse("2013-12-1"))
 
// FROM (TABLE_QUERY([mydata], "([table_id] CONTAINS 'oo' AND (LENGTH([table_id]) >= 4))"))
.FromTableQuery<mydata>("mydata", x => x.table_id.Contains("oo") && BqFunc.Length(x.table_id) >= 4)
 
// FROM (TABLE_QUERY([mydata], "REGEXP_MATCH([table_id], r'^boo[\d]{3,5}')"))
.FromTableQuery<mydata>("mydata", x => BqFunc.RegexpMatch(x.table_id, "^boo[\\d]{3,5}"))

Table decoratorは、例えばログ系を突っ込んでる場合は障害対応や監視で、直近1時間から引き出したいとか普通にあるはずで、そういう場合に走査範囲を簡単に制御できる非常に有益な機能です。が、しかし、普通に書くとUNIXタイムスタンプで記述しろということで、ちょっとムリゲーです。それがC#のTimeSpanやDateTime、DateTimeOffsetが使えるので比較にならないほど書きやすい。

FromTableQueryも文字列指定だったりtable_idってどこから来てるんだよ!?という感じであんま書きやすくないのですが、LINQ to BigQueryでは型付けされたメタテーブル情報が渡ってくるので超書きやすい。(ところでCONTAINSだけ、BqFuncじゃなくてstring.Containsが使えます、これはCONTAINSの見た目がこれだけ関数じゃないので、ちょっと特別扱いしてあげました、他の関数は全部BqFuncのみです)

Table DecoratorsとTable wildcard functionsは非常に有益なので、テーブル名の設計にも強く影響を及ぼします。これらが有効に使える設計である必要があります。TABLE_DATE_RANGEのために(垂直分割するなら)末尾はYYYYMMDDである必要があるし、Range decoratorsを有効に使うためには極力、水平シャーディングは避けたほうが良いでしょう。そこのところを無視して、ただ単にシャーディング、シャーディングって言ってたりするのは、ちょっと、ないなー。

複雑なDEMO

ひと通り紹介は終わったので、より複雑なクエリを一つ。同じく最新のGitHubのデータを扱って、一ヶ月毎に、新しく作られたリポジトリを言語毎で集計して表示してみます。まずはグラフ化の結果から。

LINQPadではちゃんと多重グラフもメソッド一発で書けるようにしてます。コードは後で載せるとしてグラフの説明ですが、縦がパーセント、横が日付、それぞれの折れ線グラフが言語。一番上はJavaScriptで今月は43000件の新規リポジトリが立ち上がっていて全体の19%を占めてるようです。2位はJava、3位はCSS、そしてRuby、Python、PHPと続いて、この辺りまでが上位組ですね。C#はその後のC++、Cと来た次の9位で9251件・全体の4%でした。

コードは、ちょっと長いよ!

Query.GetContext()
    .From<github_timeline>()
    .Where(x => x.repository_language != null && x.repository_fork == "false")
    .Select(x => new
    {
        x.repository_url,
        x.repository_created_at,
        language = BqFunc.LastValue(x, y => y.repository_language)
            .PartitionBy(y => y.repository_url)
            .OrderBy(y => y.created_at)
            .Value
    })
    .Into()
    .Select(x => new
    {
        x.language,
        yyyymm = BqFunc.StrftimeUtcUsec(BqFunc.ParseUtcUsec(x.repository_created_at), "%Y-%m"),
        count = BqFunc.CountDistinct(x.repository_url)
    })
    .GroupBy(x => new { x.language, x.yyyymm })
    .Having(x => BqFunc.GreaterThanEqual(x.yyyymm, "2010-01"))
    .Into()
    .Select(x => new
    {
        x.language,
        x.yyyymm,
        x.count,
        ratio = BqFunc.RatioToReport(x, y => y.count)
            .PartitionBy(y => y.yyyymm)
            .OrderBy(y => y.count)
            .Value
    })
    .Into()
    .Select(x => new
    {
        x.language,
        x.count,
        x.yyyymm,
        percentage = BqFunc.Round(x.ratio * 100, 2)
    })
    .OrderBy(x => x.yyyymm)
    .ThenByDescending(x => x.percentage)
    .Run()  // ↑BigQuery
    .Dump() // ↓LINQ to Objects(and LINQPad)
    .Rows
    .GroupBy(x => x.language)
    .DumpGroupChart(x => x.yyyymm, x => x.percentage);

規模感は全体で153GBで行数が2億5千万行ぐらいだけど、この程度は10秒ちょいで返してきますね、速い速い(多分)。

メソッドチェーンがやたら続いているのですが、実際のところこれはサブクエリで入れ子になってます。随所に挟まれてるIntoメソッドで入れ子を平らにしてます。入れ子の形で書くこともできるんですが、フラットのほうが直感的で圧倒的に書きやすいく、(慣れれば)読みやすくもあります。こういう書き方が出来るのもLINQ to BigQueryの大きなメリットだとは、書いてればすぐに実感できます。

(BqFunc.GreaterThanEqualが奇妙に思えるかもしれないのですが、これは文字列だけの特例です。数値やタイムスタンプの場合は記号で書けるようにしてあるのですが、文字列はそもそもC#自体に演算子オーバーロードが定義されていないのでコンパイラに弾かれる、けどBigQuery的には書きたい時がある、というのの苦肉の策でLessThan(Equal)/GreaterThan(Equal)を用意してあります)

チャート化はGroupBy.DumpGroupChartを叩くだけなんですが、ちょっと面白いのは、ここのGroupByはLINQ to Objects(C#で結果を受け取った後にインメモリで処理)のGroupByなんですよね。

.Run()  // ↑BigQuery
.Dump() // ↓LINQ to Objects(and LINQPad)
.Rows
.GroupBy(x => x.language)
.DumpGroupChart(x => x.yyyymm, x => x.percentage);

二次元のクエリ結果を、シームレスに三次元に起こし直せるってのもLINQの面白いところだし、強いところです。モノによっては無理にSQLでこねくり回さなくてもインメモリに持ってきてから弄ればいいじゃない?という手が簡単に打てるのが嬉しい(もちろん全件持ってこれるわけがないのでBigQuery側で処理できるものは基本処理しておくのは前提として、ね)。

例えば、実のところこれの結果は、言語-日付という軸だと歯抜けがあって、全ての月に1つは言語がないと、チャートが揃いません。グラフの見た目の都合上、今回は2010-01以降にHAVINGしてありますが、その後に新しく登場した言語(例えばSwift)なんかはうまく表示できません。まぁ主要言語は大丈夫なので今回スルーしてますが、厳密にやるため、その辺の処理を、しかしSQLのままやるのは存外面倒くさい。でも、こういう処理、C#でインメモリでやる分には簡単なんですよね。なんで、一旦ローカルコンピューター側に持ってきてから、少しだけC#で処理書くか、みたいなのがカジュアルにできちゃうのもLINQ to BigQuery + LINQPadのちょっと良いところ。

さて、実際に吐かれるSQLは以下。

SELECT
  [LANGUAGE],
  [count],
  [yyyymm],
  ROUND(([ratio] * 100), 2) AS [percentage]
FROM
(
  SELECT
    [LANGUAGE],
    [yyyymm],
    [count],
    RATIO_TO_REPORT([count]) OVER (PARTITION BY [yyyymm] ORDER BY [count]) AS [ratio]
  FROM
  (
    SELECT
      [LANGUAGE],
      STRFTIME_UTC_USEC(PARSE_UTC_USEC([repository_created_at]), '%Y-%m') AS [yyyymm],
      COUNT(DISTINCT [repository_url]) AS [count]
    FROM
    (
      SELECT
        [repository_url],
        [repository_created_at],
        LAST_VALUE([repository_language]) OVER (PARTITION BY [repository_url] ORDER BY [created_at]) AS [LANGUAGE]
      FROM
        [githubarchive:github.timeline]
      WHERE
        (([repository_language] IS NOT NULL) AND ([repository_fork] = 'false'))
    )
    GROUP BY
      [LANGUAGE],
      [yyyymm]
    HAVING
      [yyyymm] >= '2010-01'
  )
)
ORDER BY
  [yyyymm], [percentage] DESC

まず、ちゃんと読めるクエリを吐いてくれるでしょ?というのと、これぐらいになってくると手書きだと結構しんどいです、少なくとも私は。ウィンドウ関数もあんま手で書きたくないし、日付の処理の連鎖は型が欲しい。それと、サブクエリ使うとプロパティを外側に伝搬していく必要がありますが、それがLINQだと入力補完が効くのでとっても楽。Into()ですぐにサブクエリ化できるので、すごくカジュアルに、とりあえず困ったらサブクエリ、とぶん投げることが可能でめちゃくちゃ捗る。大抵のことはとりあえずサブクエリにして書くと解決しますからね!処理効率とかはどうせBigQueryなので何とかしてくれるだろうから、ふつーのMySQLとかで書く時のように気遣わなくていいので、めっちゃカジュアルに使っちゃう。

ところでどうでもいい余談ですが、LAST_VALUEウィンドウ関数はリファレンスに載ってません。他にも載ってない関数は幾つかあったりして(追加された時にブログでチラッと告知はされてるようなんですけどね、リファレンスにもちゃんと書いてくださいよ……)。LINQ to BigQueryならそういうアンドキュメントな関数もちゃんと網羅したんでひじょーにお薦めです!

Generate Schema

型付けされてるのがイイのは分かったけれど、それの定義が面倒なのよねー。と、そこで耳寄りな情報。まず、全部のテーブルのちょっとした情報(table_idとかサイズとか)はGetAllTableInfoという便利メソッドで取ってこれるようにしてます(実際便利!)。で、そこから更にテーブルスキーマが取り出せるようになってます。更にそこからオマケでC#コードをstringで吐き出せるようになってます。

var context = new BigQueryContext(/* BigqueryService, projectId */);
// Get All tableinfo(table_id, creation_time, row_count, size_bytes, etc...)
var tableInfos = context.GetAllTableInfo("mydataset");
// ToString - Human readable info
tableInfos.Select(x => x.ToString()).Dump();
 
// Get TableSchema
var schema = tableInfos[0].GetTableSchema(context.BigQueryService);
 
// Build C# class definition
schema.BuildCSharpClass().Dump();

まあ、そんなに洗練されたソリューションじゃないんでアレですが、一時凌ぎには良いでしょふ。publicdataとか自分のプロジェクト下にないものは直接MetaTableクラスを作ってからスキーマ取れるようになってます。

new MetaTable("publicdata", "samples", "github_timeline")
	.GetTableSchema(Query.GetContext().BigQueryService)
	.BuildCSharpClass();
 
// =>
 
[TableName("[publicdata:samples.github_timeline]")]
public class github_timeline
{
    public string repository_url { get; set; }
    public bool? repository_has_downloads { get; set; }
    public string repository_created_at { get; set; }
    public bool? repository_has_issues { get; set; }
    // snip...(200 lines)
	public string url { get; set; }
	public string type { get; set; }
}

TableName属性がついたクラスはFrom句でテーブル名を指定しなくてもそこから読み取る、っていう風になってます(今までのコードでテーブル名を指定してなかったのはそのお陰)

リアルタイムストリーミングクエリ

Streaming Insertによりリアルタイムにログを送りつけてリアルタイムに表示することが可能に!というのがBigQuery超イカス。今までうちの会社は監視系のログはSumo Logicを使っていたのですが、もう全部BigQueryでいいね、といった状態になりました、さようなら、Sumo……。

で、リアルタイムなんですが、リアルタイム度によりけりですが、1分ぐらいの遅延やそれ以上のウィンドウを取るクエリならBigQueryで十分賄えますね。Range decoratorsが最高に使えるので、定期的にそれで叩いてやればいい。そして最近流行りのReactive ProgrammingがC#でも使えるというかむしろC#はReactive Programmingの第一人者みたいなもんなので、Reactiveに書きましょふ。Rxの説明は……しないよ?

// まぁgithub.timelineがリアルタイムじゃないからコレに関しては意味ないヨ、ただの例
 
// [githubarchive:github.timeline@1411511274158000-1411511574167000]
// [githubarchive:github.timeline@1411511574167000-1411511874174000]
// [githubarchive:github.timeline@1411511874174000-1411512174175000]
// ...
Observable.Timer(TimeSpan.Zero, TimeSpan.FromMinutes(5))
    .Timestamp()
    .Buffer(2, 1) // Buffer Window
    .SelectMany(xs =>
    {
        var context = Query.GetContext();
        context.UseQueryCache = false;
        return context.From<github_timeline>().WithRange(xs[0].Timestamp, xs[1].Timestamp)
            .Select(x => new { x.repository_name, x.created_at })
            .ToArrayAsync();
    })
    .Dump();

アプリケーション側のStreaming Insertの間隔(バッファとかもするだろうし本当のリアルタイムじゃあないでしょう?)と、そしてBigQueryのクエリ時間(数秒)の絡みがあるので、まぁ1分ぐらいからでしょうかねー、でもまぁ、多くのシチュエーションでは十分許容できるんじゃないかと思います、障害調査で今すぐログが欲しい!とかってシチュエーションであっても間に合う時間だし。

よほどの超リアルタイム(バッファもほとんど取らず数秒がマスト)でなければ、もはやAmazon Kinesisのような土管すらもイラナイ感じですね。ストレージとしてもBigQueryは激安なので、Streaming Insertが安定するならば、もうBigQuery自体を土管として使って、各アプリはBigQueryから取り出して配信、みたいな形でも良いというかむしろそれでいい。Range decoratorsが効いてるなら走査範囲も小さいんで速度も従量課金も全く問題ないしねぇ。BigQuery最強すぎる……。

データ転送

本筋じゃないのでちょっとだけ話ますが、C#ってことは基本Windows Server(AWS上に立ってる)で、データをどうやってBigQueryに送るのー?と。もちろんFluentdは動かないし、(Windowsブランチあるって?あー、うーん、そもそも動かしたい気がない)、どうしますかね、と。ストレージに突っ込んでコピーは簡単明快でいいんですが、まぁ↑に書いたようにStreamingやりたいね、というわけで、うちの会社((株)グラニ。gihyoに書いた神獄のヴァルハラゲートの裏側をCTOが語り尽くす!とか読んでくださいな)では基本的にStreaming Insertのみです。ETW/EventSource(簡単な説明はWindows high speed logging: ETW in C#/.NET using System.Diagnostics.Tracing.EventSourceを)経由でログを送って、Semantic Logging Application Block(SLAB)のOut-of-process Serviceで拾って、自家製のSink(ここは今のところ手作りする必要あり、そのうちうちの会社から公開するでしょふ)でStreaming Insert(AWS->BigQueryでHTTP経由)。という構成。

今のとこリトライは入ってますが完全インメモリなんでまるごと死んだらログはロスト。といった、Fluentdが解決している幾つかの要素は解決されてないんですが、それなりに十二分に実用には使えるところかな、と。速さとかの性能面は全く問題ありません、ETWがとにかく強いし、そっから先もasync/awaitを活かした並列インサートが使えるので他のでやるよりはずっと良いはずきっと。

TODO:

実はまだRecord型に対応してません!なのでそれに関係するFLATTENやWITHIN句も使えません!99%のクエリが再現できる、とか言っておきながら未対応……。おうふ、ま、まぁ世の中のほとんどは入れ子な型なんて使ってませんよね……?そんなことはないか、そうですね、さすがに対応は必須だと思ってるので、早めに入れたいとは思ってます。

あと、LINQPadにはDataExplorerがあって、ちゃんとスキーマ情報の表示やコネクション保持とか出来るんですねー。というわけで、真面目にそのLINQPadドライバは作りたいです、というか作ろうとしていましたし、割と作れる感触は掴んだんです、が、大きな障壁が。LINQPadドライバは署名付きであることを要求するのですが、Google APIs Client Library for .NETが、署名されてない……。署名付きDLLは全部の参照DLLが署名付きであること必要があって、肝心要のGoogleライブラリが使えないという事態に。俺々署名してもInternalVisibleToがどうのこうのとかエラーの嵐で一歩も進めないよー。Googleが署名さえしてくれてれば全部解決なのに!だいたい著名なライブラリで署名されてないのなんかGoogleぐらいだよ!もはやむしろありえないレベル!なんとかして!

IQueryable is Dead. Long live Expression!

ちょっとだけC#の話もしよふ。以下、LINQ好きだからポエム書くよ!

LINQ to BigQueryはIQueryableじゃあ、ありません。この手のクエリ系のLINQはIQueryableでQuery Providerである必要が……、あるの?IQueryableは確かにその手のインフラを提供してくれるし、確実にLINQになる。けれど、絶対条件、なの?

私がLINQ to BigQueryで絶対譲れない最優先の事項として考えたのは、LINQで書けないクエリをなくすこと。全てのクエリがLINQで書ける、絶対に文字列クエリを必要としないようにする。そのためにはIQueryableの範囲を逸脱する必要があった。そして同時に強く制約したかった、順序も規定したいし、不要なクエリは(NotSupported!)そもそも書けないようにしたかった。これらはIQueryableに従っていては絶対に実現できないことだった。

LINQがLINQであるためにはクエリ構文はいらない。Query Providerもいらない。LINQ to XMLがLINQなのは何故?Parallel LINQがLINQであるのは何故?Reactive ExtensionsがLINQであるのは何故?linq.jsがLINQであるのは何故?そこにあるのは……、空気と文化。

LINQと名乗ること自体はマーケティングのようなもので、形はない。使う人が納得さえすれば、LINQでしょう。そこにルールを求めたがる人がいても、ないものはないのだから規定しようがないよ?LINQらしく感じさせる要素をある程度満たしてればいい。FuncもしくはExpressionを使ってWhereでフィルタしSelectで射影する(そうすればクエリ構文もある程度は使えるしね)。OrderBy系の構文はOrderBy/OrderByDescending/ThenBy/ThenByDescendingで適用される。基本的な戻り値がシーケンスっぽい何かである。うん、だんだん満たせてくる。別に100%満たさなくても、70%ぐらい満たせばLINQらしいんだよ。SelectManyがなくたって、いい。どうせNotSupportedExceptionが投げられるのなら、最初からないのと何が違うというの?

LINQ to BigQueryからはLINQらしさを感じられると思っています。最優先事項の全てのBigQueryのクエリを書けるようにすることやNotSupportedを投げないことなどを持ちつつも、可能な限りLINQらしさを感じさせるよう細心の注意を払ってデザインしました。極論言えば私がLINQだって言ってるんだからLINQなのですが(何か文句ある?)、多くの人には十分納得してもらえると考えています。LimitをTakeで”書けない”とかね、BigQueryらしくすることも使いやすさだし、LINQらしくすることも使いやすさ。この辺は私の匙加減。

と、いうわけでIQueryableは、データベース系クエリの抽象化というのが幻想で、無用の長物と化してしまったのだけど、しかし役に立たなかったかといえば、そうじゃあない。LINQだと感じさせるための文化を作る一翼をIQueryableは担っていたから。データベース系へのクエリはこのように定義されていると”らしい”感じになる。その意識の統一にはIQueryableは必要だった、間違いなく。しかし時は流れて、もう登場から6年も経ってる。もう、同時にかかった呪いからは解放されていいんじゃないかな?みんなでIQueryableを埋葬しよう。

と、いうのがIQueryableを使ってない理由。死にました。殺しました。IQueryableは死んだのですが、しかしExpressionは生きています!LINQ to BigQueryも当然Expressionで構成されています。空前のExpression Tree再評価の機運が!で、まぁしかしだからってふつーのアプリのクエリをExpression Treeでやりたいかは別の話ね。やっぱ構築コストとか、そもそもBigQueryは比較的シンプルなSQLだから表現しきれたけどふつーのSQLは複雑怪奇で表現できないだろー、とか、色々ありますからね。まぁ、あんま好ましく思ってないのは変わりません。

コストの話は、BigQueryの場合は完全に無視できるのよね。クエリのレスポンスが普通のDBだったら数msだけど、BigQueryは数千~数万msと桁が4つも5つも違う。リクエスト数もふつーのクエリは大量だけどBigQueryはほとんどない(一般ユーザーが叩くものじゃないからね)。なので、ほんとうの意味でExpression Treeの構築や解釈のコストは無視できちゃう。そういう、相当富豪的にやっても何の問題もないというコンテキストに立っています。だからLINQ to BigQueryはあらゆる点で完全無欠に有益。

LINQPad用お土産一式

Query.GetContextとかDumpChartとかは、LINQPadの左下のMy Extensionsのとこに以下のコードをコピペってください。それで有効になります。本当はLINQPad Driver作ってそれ入れれば有効になるようにしたかったんですが、とりあえず今のところはこんなんで勘弁してくだしあ。こんなんでも、十分使えますので。

// Import this namespaces
BigQuery.Linq
System.Windows.Forms.DataVisualization.Charting
Google.Apis.Auth.OAuth2
Google.Apis.Bigquery.v2
Google.Apis.Util.Store
Google.Apis.Services
 
public static class Query
{
    public static BigQueryContext GetContext()
    {
        BigQueryContext context;
        // Replace this JSON. OAuth2 JSON Generate from GCP Management Page. 
        var json = @"{""installed"":{""auth_uri"":""https://accounts.google.com/o/oauth2/auth"",""client_secret"":"""",""token_uri"":""https://accounts.google.com/o/oauth2/token"",""client_email"":"""",""redirect_uris"":[""urn:ietf:wg:oauth:2.0:oob"",""oob""],""client_x509_cert_url"":"""",""client_id"":"""",""auth_provider_x509_cert_url"":""https://www.googleapis.com/oauth2/v1/certs""}}";
 
        using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(json)))
        {
            // Open Browser, Accept Auth
            var userCredential = GoogleWebAuthorizationBroker.AuthorizeAsync(ms,
                new[] { BigqueryService.Scope.Bigquery },
                "user",
                CancellationToken.None, new FileDataStore(@"LINQ-to-BigQuery")) // localcache
                .Result;
 
            var bigquery = new BigqueryService(new BaseClientService.Initializer
            {
                ApplicationName = "LINQ to BigQuery",
                HttpClientInitializer = userCredential
            });
 
            context = new BigQueryContext(bigquery, "write your project id");
        }
        // Timeout or other options
        context.TimeoutMs = (long)TimeSpan.FromMinutes(1).TotalMilliseconds;
        return context;
    }
}
 
public static class MyExtensions
{
    public static IEnumerable<T> DumpChart<T>(this IEnumerable<T> source, Func<T, object> xSelector, Func<T, object> ySelector, SeriesChartType chartType = SeriesChartType.Column, bool isShowXLabel = false)
    {
        var chart = new Chart();
        chart.ChartAreas.Add(new ChartArea());
        var series = new Series { ChartType = chartType };
        foreach (var item in source)
        {
            var x = xSelector(item);
            var y = ySelector(item);
            var index = series.Points.AddXY(x, y);
            series.Points[index].ToolTip = item.ToString();
            if (isShowXLabel) series.Points[index].Label = x.ToString();
        }
        chart.Series.Add(series);
        chart.Dump("Chart");
        return source;
    }
 
    public static IEnumerable<IGrouping<TKey, T>> DumpGroupChart<TKey, T>(this IEnumerable<IGrouping<TKey, T>> source, Func<T, object> xSelector, Func<T, object> ySelector, SeriesChartType chartType = SeriesChartType.Line)
    {
        var chart = new Chart();
        chart.ChartAreas.Add(new ChartArea());
        foreach (var g in source)
        {
            var series = new Series { ChartType = chartType };
            foreach (var item in g)
            {
                var x = xSelector(item);
                var y = ySelector(item);
                var index = series.Points.AddXY(x, y);
                series.Points[index].ToolTip = item.ToString();
            }
            chart.Series.Add(series);
        }
        chart.Dump("Chart");
        return source;
    }
}

GCPの管理ページからOAuth2認証用のJSONをベタ貼りするのとプロジェクトIDだけ書いてもらえれば使えるかと。最初にブラウザ立ち上がって認証されます、2回目以降はローカルフォルダにキャッシュされてるので不要。まぁ色々ザルなんですが、軽く使う分にはいいかな、と。

まとめ

いやもう本当に、この手のソリューションではBigQueryが群を抜いて凄い。Azure使ってる人もAWS使ってる人(実際、うちのプロダクトはAWS上で動かしてますがデータはBigQueryに投げてます)もオンプレミスの人もBigQuery使うべきだし、他のものを使う意味が分からないレベル。とにかく試せ、であり、そして試すのは皆Googleアカウントは絶対持ってるはずだからワンポチするだけで立ち上がってるし、最初から膨大なサンプルデータがあるので簡単に遊べるし、一発で気にいるはず、間違いない。

そしてWindows(C#)の人には、LINQ to BigQuery + LINQPadがベストなツールとなってくれるはず。むしろあらゆるBigQueryを扱う環境の中でC#こそが最高といえるものになってくれるよう、色々やっていきたいですね。

ジェネリッククラス内の静的フィールドの挙動について

今メインで作ってるほげもげの進捗があんま良くないので、たまには少し小ネタでも。ジェネリッククラス内(静的クラスでも普通のクラスでもどっちでもいいです)の静的フィールドは、それぞれ独立して、各型に唯一のフィールドとして存在できます。違う型では共有されず、同じ型ないでは共有される、という挙動です。あまり良い例でもないですが、例えばこんな感じ。

public static class InstanceGenerator<T>
{
    static readonly Func<T> generator;
 
    static InstanceGenerator()
    {
        var newExpr = Expression.Lambda<Func<T>>(
            Expression.New(typeof(T).GetConstructor(Type.EmptyTypes)));
        generator = newExpr.Compile();
    }
 
    public static T CreateNew()
    {
        return generator.Invoke();
    }
}
 
class Program
{
    static void Main(string[] args)
    {
        var p1 = InstanceGenerator<Program>.CreateNew();
        var p2 = InstanceGenerator<Program>.CreateNew();
        var s = InstanceGenerator<StringBuilder>.CreateNew();
    }
}

さすがにこれだとnew Program()って書けよって話なので、クソの役にもたたなすぎる例なんです が、いちおう、Compileという重たい処理をキャッシュできますね、みたいな感じ。(このクラスはクソの役にも立たないけど)(こういうジェネリッククラスの挙動は)便利便利。で、それはいいんですけど、もしフィールドがジェネリックじゃない場合はどーなるでしょう?こんな風に、非許可の型チェックを入れてみたりします。

// 静的クラスでもふつーのクラスでもどっちでもいーですよ
public class InstanceGenerator<T>
{
    static readonly Func<T> generator;
    static readonly HashSet<Type> disallowType = new HashSet<Type>
    {
        typeof(StringBuilder),
        typeof(ArrayList)
    };
 
    static InstanceGenerator()
    {
        var newExpr = Expression.Lambda<Func<T>>(Expression.New(typeof(T).GetConstructor(Type.EmptyTypes)));
        generator = newExpr.Compile();
    }
 
    public InstanceGenerator()
    {
        if (disallowType.Contains(typeof(T))) throw new Exception("その型は許可されてません!");
    }
 
    public T CreateNew()
    {
        return generator.Invoke();
    }
}
 
class Program
{
    static void Main(string[] args)
    {
        // ok
        var pg = new InstanceGenerator<Program>().CreateNew();
 
        // exception
        var sb = new InstanceGenerator<StringBuilder>().CreateNew();
 
    }
}

HashSet<Type>は特に<T>とは関係のないフィールド。かつ「意図としては」readonlyで全ジェネリッククラスで共有して欲しい。で、実際どーなってるかというと……確認しませう。

public class InstanceGenerator<T>
{
    static readonly Func<T> generator;
 
    // 呼ばれたのをチェックするために遅延実行のシーケンスをかませる
    static readonly HashSet<Type> disallowType = new HashSet<Type>
        (Enumerable.Range(1, 3).Select(x => { Console.WriteLine(x); return typeof(int); }))
    {
        typeof(StringBuilder),
        typeof(ArrayList)
    };
 
    // 以下同じなので略
}
 
class Program
{
    static void Main(string[] args)
    {
        // 1,2,3,1,2,3と出力されて、(当然)二回HashSetが初期化されてるのが分かる
        var pg = new InstanceGenerator<Program>().CreateNew();
        var sb = new InstanceGenerator<StringBuilder>().CreateNew();
 
    }
}

といった感じに、非ジェネリックフィールドも当たり前のように共有されることなく、各ジェネリッククラスで独立して存在します。当たり前っちゃあ当たり前です(readonlyじゃないstatic fieldだって存在できるし、readonlyだってimmutableとは限らないので、そんな利用者都合の区別をコンパイラがつけられはしない!)

けれど多くの静的フィールドを使うシチュエーションにとっては、あんま都合よくないかな、と。大したことナイといえばないですが、正規表現のCompileしたのとか別個で持ってたくないし、その他色々色々。気になるっちゃあ気になります。さて、どうすればいいか、っていうと

internal class InstanceGenerator
{
    protected static readonly HashSet<Type> disallowType = new HashSet<Type>
    {
        typeof(StringBuilder),
        typeof(ArrayList)
    };
}
 
// 静的クラスじゃなければ継承するとか
public class InstanceGenerator<T> : InstanceGenerator
{
    // 以下略
}
 
// 静的クラスの時は適当に誤魔化すしかない
internal static class _InstanceGenerator
{
    public static readonly HashSet<Type> disallowType = new HashSet<Type>
    {
        typeof(StringBuilder),
        typeof(ArrayList)
    };
}
 
public static class InstanceGenerator<T>
{
    // 中略
 
    // なんかひどぅぃ
    public static T CreateNew()
    {
        if (_InstanceGenerator.disallowType.Contains(typeof(T))) throw new Exception("その型は許可されてません!");
        return generator.Invoke();
    }
}

独立した外の型として定義せざるをえないので、適当に誤魔化すしかないですね!あとはふつーにゆーてぃりてぃクラスとして独立させるとか設計で回避、的なアレ。

ちなみに

例がクソややこしく感じた人には一番シンプルなものを。

public static class MyClass<T>
{
    public static object X = new object();
}
 
class Program
{
    static void Main(string[] args)
    {
        var b = Object.ReferenceEquals(MyClass<int>.X, MyClass<string>.X);
        Console.WriteLine(b); // false
 
        MyClass<int>.X = 1000; // 違うもクソも外からセットできるし
        Console.WriteLine(MyClass<int>.X); // 1000
        Console.WriteLine(MyClass<string>.X); // System.Object
    }
}

ようはこれだけじゃないですかーやだー無駄にこねくりまわした例は余計わかりづらいー。

仕様

言語仕様的には「4.4.2 オープン型とクローズ型」の最後の部分の話です。

すべての型は、”オープン型” か “クローズ型” のいずれかに分類されます。オープン型は、型パラメーターと一緒に使用する型です。より具体的には、次のとおりです。

  • 型パラメーターはオープン型を定義します。
  • 配列型は、要素の型がオープン型の場合のみ、オープン型です。
  • 構築された型は、1 つ以上の型引数がオープン型の場合のみ、オープン型です。構築された入れ子になった型は、1 つ以上の型引数または外側の型の型引数がオープン型の場合のみ、オープン型です。

クローズ型とは、オープン型でない型です。

実行時、ジェネリック型宣言内のすべてのコードは、ジェネリック宣言に型引数を適用することによって作成されたクローズ構築型のコンテキストで実行されます。ジェネリック型内の各型パラメーターは、特定の実行時の型にバインドされます。すべてのステートメントおよび式の実行時の処理ではクローズ型が発生し、オープン型は、コンパイル時の処理でのみ発生します。

クローズ構築型には独自の静的変数セットがあり、このセットは他のクローズ構築型と共有されません。オープン型は実行時には存在しないため、オープン型に関連付けられた静的変数はありません。2 つのクローズ構築型は、同じ非バインド ジェネリック型から構築された場合は同じ型になり、対応する型引数も同じ型になります。

あとは「10.5.1 静的フィールドとインスタンスフィールド」でも触れられています。

静的フィールドは特定のインスタンスの一部ではなく、クローズ型 (4.4.2 を参照) のすべてのインスタンス間で共有されます。クローズ クラス型のインスタンスがいくつ作成される場合でも、関連付けられたアプリケーション ドメインに対する静的フィールドのコピーは 1 つだけです。

この辺りのは言い方がややこしいんで言語仕様とにらめっこしてるだけだとあんま頭に入ってこない系ですにぇ。

Prev |

Search/Archive

Category

Profile


Yoshifumi Kawai
Microsoft MVP for .NET(C#)

April 2011
|
March 2016

Twitter:@neuecc