NuGetでビルドプロセスに介入するパッケージを作る

NuGetNuGetを使うと、サードパーティ(MSではない)アセンブリの導入が簡単に行えます。他にも、MSBuildのビルドプロセスをカスタマイズ出来るパッケージを作る事も出来ます。Visual Studioはビルド時にMSBuildを使っているので、Visual Studio上でも、MSBuildによるコマンドラインのビルドでも、シームレスにビルドプロセスをカスタマイズ出来ます。

例えば、C#やF#のソースコードをコンパイルする直前に、カスタムコードを自動生成させたりする事ができます。このようなスクリプトをNuGetパッケージ化すると、配布と導入が非常に簡単になり、アンインストールも簡単で、多くの人に気軽に使って貰えるようになります。

# MSBuildを介さない操作のカスタマイズは出来ません。例えばVisual Studio自体のカスタマイズは、VSIX拡張(VSSDK)を使いますがここでは触れません。

この記事では、どうやって最小限の労力でNuGetパッケージを作るのかのポイントを示します。NuGetのパッケージを作ったことがある方が、より理解しやすいでしょう。


MSBuildとtargetsスクリプト

MSBuildは、*.csprojのようなプロジェクトファイルを入力として、一種の宣言ベーススクリプト処理を行います。中身を見たことがある人が多いと思いますが、XMLで記述します。大まかには、以下のような構造になっています(一部のみ抜粋)。

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <ProjectGuid>{3FCFCD0E-B40D-4A48-BF4C-85D552E46471}</ProjectGuid>
    <OutputType>Library</OutputType>
    <RootNamespace>CenterCLR.SampleCode</RootNamespace>
    <AssemblyName>CenterCLR.SampleCode</AssemblyName>
    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
    <!-- ... -->
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <OutputPath>bin\Debug\</OutputPath>
    <!-- ... -->
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <!-- ... -->
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="Microsoft.CSharp" />
    <!-- ... -->
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Program.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
    <!-- ... -->
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
       Other similar extension points exist, see Microsoft.Common.targets.
  <Target Name="BeforeBuild">
  </Target>
  <Target Name="AfterBuild">
  </Target>
  -->
</Project>

場合によってはもっと複雑なスクリプトになっている事もあります。MSBuildはこのXMLを読み取って、ビルド対象の情報を取得しています。例えば、PropertyGroupタグ内の定義は、ビルドに必要な各種定義(.NET Frameworkのターゲットバージョンや、アセンブリ名など)が含まれています。この定義は一種の変数のように振る舞い、スクリプト内からこれらの名前で参照する事ができます。

ざっと眺めると、Referenceタグでアセンブリ参照を定義し、Compileタグでコンパイル対象のファイルを定義しているように見えますね。

この記事で一番のポイントは、「Import」タグです。このタグのProject属性には、「*.targets」ファイルへのパスが示されています。上記の例ではImportタグは二か所ありますが、例えば最後のタグでは「$(MSBuildToolsPath)\Microsoft.CSharp.targets」を指定していますね。「$(MSBuildToolsPath)」がPropertyGroupなどで定義された値を参照する構文で、このフォルダ配下の「Microsoft.CSharp.targets」というファイルが指定されています。

このImportタグは、この場所にここで示されたファイルの定義をインポートするという定義です。つまり、「Microsoft.CSharp.targets」ファイルを読み取り、ここに挿入すると考えてください。丁度C言語のincludeのように機能します(XMLなので実際は違いますが)。

Microsoft.Common.propsやMicrosoft.CSharp.targetsファイルには、非常に多くの定義が含まれています。全てMSBuildのスクリプトです。Microsoft.Common.propsには、先ほどのMSBuildToolsPathのような共通のPropertyGroup定義が含まれているため、ほぼすべてのプロジェクトファイルはこのファイルをImportします。

Microsoft.CSharp.targetsはC#のビルドに必要な定義が含まれています。これには、*.csのコンパイルだけではなく、*.resxの変換やXAMLのコンパイル定義なども含まれます。F#のような他の言語では、「Microsoft.FSharp.Targets」のような感じで、その言語に合わせて変えています。

カスタムなスクリプトを直接プロジェクトファイルに書くことも出来るのですが、別のファイルにカスタムスクリプトを書いて、それをImportした方が、プロジェクトファイルを損傷させる可能性が減る(XMLなので容易に構造を壊してしまう可能性がある)ため、ベターです。基本的な方針は、この「カスタムtargetsスクリプト」を用意することと、これをどうやって既存のプロジェクトファイルに「自動的に挿入(Importタグを追加)」させるのかと言う事になります。

Visual Studio 2010未満の環境では、VSIX拡張を使って自動的に挿入させることができました(この手法は今でも使えます)。しかし、VSIX拡張は作ったり保守したりするのが非常に困難なため、お勧めしません。また、VSIX拡張を使ってしまうと、ビルドにdevenv.exe(つまりVisual Studio本体)が必要になってしまい、MSBuildによる継続インテグレーションが出来なかったり自由度が失われてしまいます。

# わかりやすい例として、AppVeyorのようなクラウドベースのビルドエージェントで継続インテグレーションが困難です(不可能ではありませんが、事前インストールと自動構成が必要)。

NuGetでビルドプロセスに介入する流れ

Visual Studio 2010からNuGetが使えるようになり、このtargetsスクリプトをNuGetで配布する事が出来るようになりました。どういう事かと言うと:

  1. NuGetパッケージに、targetsスクリプトとその他必要なファイル群を加える
  2. NuGetパッケージを配布する
  3. NuGetパッケージをプロジェクトに導入すると、プロジェクトファイルに自動的にImportタグが追加され、targetsの定義が利用可能になる

という流れです。但し、単にtargetsスクリプトを含めれば良い訳ではありません。以下の条件が存在します。

  • パッケージ内に「build」フォルダを追加し、その直下にtargetsスクリプトを配置する。
    パッケージを作ったことがある方なら分かると思いますが、通常ライブラリアセンブリを配布する場合は「lib」フォルダ配下に「net40」「net20」のようなプラットフォームターゲット名のフォルダを作り、その中にアセンブリファイルを配置します。しかし、targetsスクリプトファイルは必ずbuildフォルダ配下に配置しなければなりません。
  • targetsスクリプトファイル名は、必ず「[NuGetパッケージID].targets」でなければならない。
    例えばパッケージIDが「CenterCLR.RelaxVersioner」の場合は、「build/CenterCLR.RelaxVersioner.targets」に配置します。名前が違っていると、パッケージの生成には成功して配布も出来るのに、導入時に「ターゲットプラットフォームが存在しない」という不可解なエラーで失敗します。
  • targetsスクリプトで使用する追加のファイル群がある場合は、それらもbuildフォルダ内に全て配置する。
    targetsスクリプトからファイルを相対参照する場合、起点はbuildフォルダになります。どうしても別フォルダにしたい場合でも一旦buildフォルダに配置して、参照できることを確認してから移動した方が良いでしょう。

スクリプト実行レベルでの挿入や乗っ取りの方法

さて、これで物理的なパッケージングの枠組みは出来たのですが、実際カスタムスクリプトはどうやって書けば良いのでしょうか?

MSBuildのカスタムスクリプトは、これだけで膨大なテーマであるので、全部網羅的に調べるのは大変です。そこで以下のポイントを知っておけば、取り掛かりが楽になると思います。

  • PropertyGroupタグ内には、参照可能な定義を含める(これは前述で説明しました)。
    定義した値は、「$(Name)」という記法で参照出来ます。
  • ItemGroupタグ内には、操作対象のファイルなどを指定させる。
    「Compile」タグはコンパイル対象を指定させますが、これは「@(Compile)」という記法で参照できます。当然これらは複数定義されることがあるため、@(Compile)の評価結果はコレクション(複数)となります。
  • Condition属性に式を書くことで、そのタグがMSBuildの評価の対象になるかどうかが決定されます。
    csprojの例を見るとわかりますが、デバッグビルドやプラットフォームの識別を式で判定して、それぞれに定義されるPropertyGroupを変えたり、と言う事が出来ます。これはPropertyGroupだけではなく、ほぼすべてのタグで使用できます。
  • PropertyGroupやItemGroup内の定義は、串刺し的に評価される。
    例えば、PropertyGroupが何個定義されていても構わず、それらすべての定義が一つのPropertyGroupに定義されているのと同じように評価されます。この機能を利用して、カスタムスクリプト内で自由に定義を追加したり、Condition属性に応じて定義するかしないかを切り替えたりと言ったハックが実現出来ます。

余談ですが、「Debug|AnyCPU」という値の判定には、意味ありげに演算子っぽい「|」を使っていますが、これ、文字列としてただ連結しているだけです(ConfigurationとPlatformの値が結合して別の値と誤認しないようにするためと思われる)。

  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <OutputPath>bin\Debug\</OutputPath>
    <!-- ... -->
  </PropertyGroup>

and演算子が使えるにもかかわらず、このような「ダーティハック的な記述」が至る所にあるため、それらに惑わされれないように注意しましょう :)

このような前提で、まずは「Microsoft.Common.props」や「Microsoft.CSharp.targets」ファイルを眺めてみて下さい。これらのファイルは、「C:\Program Files (x86)\MSBuild\12.0」や、「C:\Windows\Microsoft.NET\Framework\v4.0.30319」配下にあります。

標準のMSBuildスクリプト群は非常に巧妙で複雑なのですが、ビルドを実現する戦略として以下のような手法を使っているようです。

Targetタグを論理的にネストさせて、ビルドシーケンスに階層構造を構築する

以下はMicrosoft.Common.targetsの一部です。

  <PropertyGroup>
    <BuildDependsOn>
      BeforeBuild;
      CoreBuild;
      AfterBuild
    </BuildDependsOn>
  </PropertyGroup>
  <Target
      Name="Build"
      Condition=" '$(_InvalidConfigurationWarning)' != 'true' "
      DependsOnTargets="$(BuildDependsOn)"
      Returns="$(TargetPath)" />

この定義は、「Targetタグ」によって「Build」という名前の「ターゲット」を定義しています。この名前は、MSBuildのコマンドラインオプションで指定することができます。ビルドする場合は、ここからスクリプトの評価が開始されると思って下さい。ただし、このTargetタグは空タグなので、このターゲット自体は何も実行しません。その代わり、「DependsOnTargets属性」で参照されるサブターゲットを評価します。

DependsOnTargets属性は、「BuildDependsOn」の値が参照されています。前半のPropertyGroup内に定義されている、「BeforeBuild;CoreBuild;AfterBuild」が対象です。改行と空白は無視し、セミコロンで区切られます。

このTargetタグの後、更に別のTargetタグによって「BeforeBuild」「CoreBuild」「AfterBuild」それぞれのターゲットが定義されています。このリストは、まずBeforeBuildの定義が評価され、次にCoreBuildの定義が評価され、最後にAfterBuildの定義が評価される、というように、リストに書かれた順に従って、サブターゲットが評価されることになります。

CoreBuildの定義は後で示しますが、BeforeBuildとAfterBuildはそれぞれ以下のように定義されています。

<Target Name="BeforeBuild"/>
<Target Name="AfterBuild"/>

要するに空定義なので何も実行しませんが、このターゲット名、何か見覚えがありませんか? 実は、csprojのデフォルトのテンプレートには、BeforeBuildとAfterBuildのひな型が含まれています。再度抜粋します:

  <!-- ... -->

  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
       Other similar extension points exist, see Microsoft.Common.targets.
  <Target Name="BeforeBuild">
     // 必要ならここにカスタムの事前実行スクリプトを書く
  </Target>
  <Target Name="AfterBuild">
     // 必要ならここにカスタムの事後実行スクリプトを書く
  </Target>
  -->
</Project>

つまり、csprojでこのタグのコメントアウトを外してここに何か書くと、BeforeBuildとAfterBuildの内容をオーバーライドできると言う事です。こういうカラクリで、プロジェクトファイルにカスタムスクリプトを記述出来るようになっています。

さて、CoreBuildの定義を見ると:

  <PropertyGroup>
    <CoreBuildDependsOn>
      BuildOnlySettings;
      PrepareForBuild;
      PreBuildEvent;
      ResolveReferences;
      PrepareResources;
      ResolveKeySource;
      Compile;
      ExportWindowsMDFile;
      UnmanagedUnregistration;
      GenerateSerializationAssemblies;
      CreateSatelliteAssemblies;
      GenerateManifests;
      GetTargetPath;
      PrepareForRun;
      UnmanagedRegistration;
      IncrementalClean;
      PostBuildEvent
    </CoreBuildDependsOn>
  </PropertyGroup>
  <Target
      Name="CoreBuild"
      DependsOnTargets="$(CoreBuildDependsOn)">

    <OnError ExecuteTargets="_TimeStampAfterCompile;PostBuildEvent" Condition="'$(RunPostBuildEvent)'=='Always' or '$(RunPostBuildEvent)'=='OnOutputUpdated'"/>
    <OnError ExecuteTargets="_CleanRecordFileWrites"/>
  </Target>

Buildとおなじような構造の定義になっています(細かい相違は省略)。CoreBuildもまた空タグなので、このターゲット自体は何も実行しません。そしてDependsOnTargets値は「CoreBuildDependsOn」を参照し、今度は更に細かくサブターゲット群が定義されています。このリストそれぞれに対応するターゲット定義が、この後のスクリプトに全て含まれていて、この順にTargetタグが評価されます。このように、ネストしたサブターゲットを再帰的に探索してビルドを実現しているのです。

名前からして分かりやすそうな「Compile」というターゲットは、各処理系(C#やF#)のスクリプトに定義されているTarget定義に処理が移譲され、更に細かくターゲットがネストし、最終的にはC#やF#のコンパイラを起動してコンパイルを行っています。

さて、これでどうやってビルドプロセスに介入したら良いか分かってきましたね? 必要となるタイミングに対応するTargetタグを調べて(実はここが大変)、そのターゲットと同じ名前でオーバーライドしてしまうか、あるいは「*DependsOn」をオーバーライドして独自のターゲット名のリストに入れ替え、そこに自分のターゲット名を挿入しておいたり、標準のターゲットの代わりに自分のターゲットを実行させるように入れ替えたりすれば良い訳です。


ビルドプロセスに介入する例

CenterCLR.RelaxVersioner.128私が取り組んでいるプロジェクトに、RelaxVersionerというパッケージがあります。これは、Gitを前提として、バージョン番号の埋め込みなどを自動的に行うNuGetベースのパッケージです。

# バージョン番号を簡単に自動的に埋め込むには、何のツールが良いかとか、悩んでいる人多いですよね? ね??
# フィードバックやPR貰えるとやる気が出ます、よろしく :)

デザインの指針は、

  • とにかく簡単(ほぼパッケージをインストールするだけ)
  • ビルドの副作用による影響なし
  • 独自のUIを作らない。コントロールはすべてGitの標準機能(ブランチ・タグ・メッセージ)を使って実現する
  • 約束事を最小にする
  • 新たな学習コストを最小にする
  • Gitのリポジトリを汚さない(バージョン番号の変更を明示的にステージングから除外したりしなくても良い)
  • 継続インテグレーションにもそのまま使用可能
  • 必要であればカスタマイズ可能

というところを目指しています。現状で大体実現出来ています(mono対応が出来てない。コードは書いたけどうまく動かず… 誰か助けてー)。使い方はGitHubの方を見てもらうとして、とにかく導入が簡単で影響を最小化したかったので、この記事で説明したような、NuGetによる導入で即使用可能というところが重要でした。

やっていることは、各ソースファイルのコンパイル直前に、Gitのローカルリポジトリから吸い上げた情報を元に、AssemblyVersion属性を含んだソースファイルをテンポラリフォルダに自動生成して、コンパイルの対象に含める、という内容です。こんなコードが自動的に生成されてコンパイルされます:

[assembly: AssemblyVersion("0.5.30.0")]
[assembly: AssemblyFileVersion("2016.1.15.41306")]
[assembly: AssemblyInformationalVersion("a05ab9fc87b22234596f4ddd43136e9e526ebb90")]
[assembly: AssemblyMetadata("Build","Fri, 15 Jan 2016 13:56:53 GMT")]
[assembly: AssemblyMetadata("Branch","master")]
[assembly: AssemblyMetadata("Tags","0.5.30")]
[assembly: AssemblyMetadata("Author","Kouji Matsui <k@kekyo.net>")]
[assembly: AssemblyMetadata("Committer","Kouji Matsui <k@kekyo.net>")]
[assembly: AssemblyMetadata("Message","Fixed tab")]

細かいコードは除外して、ポイントだけに絞ったtargetsスクリプトはこのようなものです(オリジナルはココ):

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <!-- Common -->
    <PropertyGroup>
        <RelaxVersionerOutputPath Condition="'$(RelaxVersionerOutputPath)' == ''">$([System.IO.Path]::Combine('$(ProjectDir)','$(IntermediateOutputPath)','RelaxVersioner$(DefaultLanguageSourceExtension)'))</RelaxVersionerOutputPath>
        <CoreBuildDependsOn>
            RelaxVersionerBuild;
            $(CoreBuildDependsOn);
        </CoreBuildDependsOn>
    </PropertyGroup>

    <!-- Build definition -->
    <Target Name="RelaxVersionerBuild" BeforeTargets="BeforeCompile" Outputs="$(RelaxVersionerOutputPath)">
        <Exec
            Command="CenterCLR.RelaxVersioner.exe &quot;$(SolutionDir.TrimEnd('\\'))&quot; &quot;$(ProjectDir.TrimEnd('\\'))&quot; &quot;$(RelaxVersionerOutputPath)&quot; &quot;$(TargetFrameworkVersion)&quot; &quot;$(TargetFrameworkProfile)&quot; &quot;$(Language)&quot;"
            Outputs="$(RelaxVersionerOutputPath)"
            WorkingDirectory="$(MSBuildThisFileDirectory)" />
        <ItemGroup>
            <Compile Include="$(RelaxVersionerOutputPath)" />
        </ItemGroup>
    </Target>
</Project>

RelaxVersionerOutputPathは、中間ファイルフォルダの配下にソースファイルを配置するためのパスを示します。Conditionはパスのカスタマイズの余地を残すためです。このパスは、ほとんどの場合、プロジェクトフォルダ配下の「obj\Debug\RelaxVersioner.cs」のようなパスとなります。

そして、CoreBuildDependsOnをオーバーライドし、この直前にRelaxVersionerBuildターゲットを実行するように再定義します。リストを丸ごと置き換えると、標準targetsスクリプトが更新された時におかしくなってしまう可能性があるので、元の定義を生かしたまま挿入するハックです。

# CoreBuildではなく、もっと深いコンパイルに近い位置でのターゲットでも良いかもしれません。どこの位置が適切なのかが難しいところですが、RelaxVersionerはマルチ言語対応の予定があり、言語毎に細分化されたターゲットに挿入すると意味がないので、CoreBuildを選択しています。

実際にソースコードを生成するのは、「CenterCLR.RelaxVersioner.exe」という実行ファイルで、これはNuGetパッケージのbuildフォルダに一緒に含めておきます。もちろん、ほかに必要なアセンブリも全て含める必要があります。RelaxVersionerはlibgit2sharpを使っているので、これらも配置してあります。

# MSBuild Taskではなく実行ファイルであるのは… MSBuild Taskはデバッグとかバージョンとかつらいんです。後でTask化にトライする予定。

コマンドラインにソースファイルのパスや、対象の言語名、.NET Frameworkのバージョン情報などを渡して、生成するソースコードの内容を変えています。最後に、生成したソースコードがコンパイルのターゲットとなるように、Compileタグで追加しておきます。


補足

.NET Core環境は、現在のところMSBuildを使用しないことになっているために、この記事の手法が使えません。ご存知の通り.NET Coreでは「project.json」がプロジェクト定義となっているのですが、これはdotnetコマンドが解釈を行います。

また、.NET Core toolsetをインストールしたVisual Studioのテンプレートから.NET Coreのプロジェクトを作った場合、project.jsonの他に*.xprojが生成されます。このxprojはMSBuildのプロジェクトスクリプトなのですが、NuGetの管理を行わない(NuGetの管理はproject.jsonで行われるため、結局Importタグが埋め込まれない)ので、やはり機能しません。

# ややこしいことになっていますが、VSでビルドした場合に、xproj (MSBuild) –> DNX Task (MSBuild) –> dotnet (project.json) –> NuGetting & Build というように処理が移譲されるようです(あまり細かく追っていないので、間違っているかもしれません)

.NET Coreはプロジェクト定義をproject.jsonベースに移行する事を前提として作業していたのですが、RTMのリリース直前になってこれがキャンセルされ、MSBuildをマルチプラットフォームに移植するという方針に変わりました。

しかし、結局RTMには移植が間に合わず、project.jsonベースでNuGetを管理する状態のままリリースされてしまいました。従って、まだしばらくは.NET Coreでのスムーズなビルドのカスタマイズは出来ないと言う事になりますが、MSBuildが移植される方針なので、将来的に同じ手法でビルドプロセスをコントロール出来そうです。

mono環境ではMSBuildのmono実装である「XBuild」が使えます。サブセットであるので一部互換性がありませんが、概ね同じような手法が使えると思います。モウコレデイインヂャナイノ?

余談

度々見るのと、私自身も思っているのですが、C#/F#/VB.netなど、複数の言語を一つのプロジェクト内でまとめて扱えるようにして欲しいという要望があります。しかし、現在のMSBuildのtargetsスクリプト群を見てわかる通り、これを実現するには「標準のtargets全部書き直し」しか無いですね。.NET Coreのxprojを見ていると、ひょっとしたらこれを進めてproject.jsonだけどうにかすれば出来るんじゃないか?と思ったんですが、.NET Coreチームはxprojもcsprojに戻すことを選択したようで、がっかりです。

# まあ、なんか、何でそこだけjsonなんだよ、分離したければどっちかに統一すれば良いのに、と思った。あと「project.json」っていう命名も誤解の元。
# それに、targetsがまともに機能しなくなるのが分かってた筈なのに、何で再考しなかったんだというのも… 互換性無くなるなら無くなるで、一部だけどうにかするんじゃなくて全体から(つまりMSBuild自体)見直して欲しかった。

MSBuild、複雑すぎます。何故こうなった感が大きいです。初期導入が古いとは言え、最初から不満でした。まず、階層構造をタグの論理的なネストで実現したのも、正直言ってXMLで処理系作ってその枠組みで無理やり実現しました感があるし、既存のスクリプトを壊さないように処理を追加したり交換したりといった事が簡単に出来ないし、targetsスクリプトを追って行くのがとにかく大変で、この記事のようにシンプルにまとめ上げれるようになるまでにかなり時間が掛かりました。こんな構造だから、拡張のためのエコシステムが発展しないんだと思う…

仕事でVSIX拡張を触っているのですが、VSIXの難解なインターフェイスと、MSBuildの複雑構造と、NuGetのどうしようもなく整理されていない感が組み合わさって、出自が違うので仕方ないとは言え本当につらい。この記事は備忘録も兼ねて書いたのですが、少しでもお役に立てれたら嬉しいです。

Async deepdive before de:code と de:code振り返り

WP_20160523_21_05_51_Richちょっと遅くなりましたが、de:code 2016の前夜祭である、Japan ComCamp meets de:codeで登壇した時のスライドです。

1セッション15分と言う事で(ちょっとオーバーした)、スライドはSlideShareに載せてあり、ブログに載せるほどのものでもないのですが、折角なので載せておきます。

一般的に、async-awaitを使った非同期プログラミングを行う理由付けを、Windowsのメッセージポンプ(又はUIスレッド)との協調を行わなければならないという理由から展開しますが、このスライドでは純粋にパフォーマンスの観点から、非同期プログラミングの重要性にアプローチしました。つまり、「サーバーサイド」での非同期プログラミングの必要性を凝縮したものです。

主に「Async訪ねて3000里」や、過去私が解説してきた事の要約で構成されています。

某氏に「linuxではどうなのか?」と聞かれたんですが、linuxの非同期I/Oシステムコールがどうなっているのか次第だと思いますが、セッションで説明したテクニックを使って、殆どCPUリソース非依存な非同期ライブラリを作ることが出来るはずだと思います。

技術レベル:400~500


de:code振り返り on Center CLR

これもここに貼っておきます。先週、de:code振り返りな会を開催しました。裏番は「Rebirth Tohoku 2016」ですねつよい。

de:codeのセッションはそれはもう沢山あるので、あらかじめ関心のあったセッションをポストイットで貼り出してもらい、多いものから順に聞いてきた人が喋ってディスカッションする、というスタイルにしました。

私はChalkTalkと徘徊ばかりしていたため、あまり解説出来なかったのですが、赤間さんのセッションの解説(平内さん)が特にウケていました(その状況はどうなんだという疑問はある :)

教材は各セッションのスライドと動画(channel 9)です。特に今年からはchannel 9でストリーミングも行われるようになったので、去年も要望を出していたので非常にうれしい。これで何時でもセッションを再確認できるし、誰にでも資料のポインタとして使えるようになりました。

以下がディスカッションした対応セッションです。この他のセッションも概要レベルで解説などがありました。

  • CLT-001 今だからもう一度確認したい、クライアントテクノロジの概要と選択
  • DBP-019 りんなを徹底解剖。”Rinna Conversation Services” を支える自然言語処理アルゴリズム
  • DEV-010 エンプラ系業務 Web アプリ開発に効く! 実践的 SPA 型モダン Web アプリ開発の選択手法
  • CLT-016 拝啓 『変わらない開発現場』を嘆く皆様へ ~エンプラ系 SI 開発現場の「今」を変えていくために~
  • CLT-002 Windows 10 デバイスと UWP 完全解説
  • DEV-022 これから始める Xamarin ~環境構築から iOS/Android/UWP アプリのビルドまで~
  • SPL-005 オープンソースから見たマイクロソフト

de:codeへの感想ですが、一言で言うと「OSSで浮き足し立っている」かな。決して悪い意味ではなく、まだ定着するには時間が必要なんだと思います。殊更OSSを題材に挙げる期間が過ぎた時に、初めて評価されるんじゃないかなと。そうは言っても何もアピールしないのでは伝わらないので、MSは方向修正したよ!!って事を大々的に示している… ざっくりとですが、そんな気がしました。

それではまた。

パターンマッチングの面白さを見る

fsharpツイッターのTL見ていると、徐々にC# 7の話題が出てきています。C# 7でパターンマッチングが導入されるとか(まだ範囲は確定してません)。そんなわけで、「パターンマッチング」で面白いと感じた事を書いてみます。

C# 7で導入が検討されている機能については、岩永さんの記事が非常にわかりやすくて良いと思います。是非参照してみてください。

私の場合、時期が微妙にずれているのですが、C# 7ではなくF#でパターンマッチングに触れました。その前にはScalaでほんの少しだけ触れたのですが、結局実用的なコードをほとんど書かなかったこともあって、モノにもならなければその良さもわからず仕舞いでした。さらにその前では、某名古屋のシャチョーさんにHaxeで判別共用体を使う例をちょっとだけ紹介してもらった事がある程度です。

F#は業務で使っていることもあり(但し、まだ使いこなしているとは言い難い)、F#に限ってですが、ようやくその利点が頭の中で整理されてきた感があります。「パターンマッチング」の何が良いのか、面白いのか、という事を明らかに出来たらいいなーと思っています。

# サンプルコードはそのままではコンパイルできない式があったりしますが、説明を優先します。また、F#らしからぬコード(例: printfn使ってない)がありますが、対比を優先させています。加えて、多少冗長に書いている箇所があります。


switch-caseの代替としてのパターンマッチング

パターンマッチングの最初の導入は、単にif-then-elseやswitch-caseの置き換えと言うものです。その前にswitch-caseの導入を(ばかばかしいですが)しておきましょう。

var value = 102;

// ifで分岐
if (value == 100)
{
  // 100に対応する処理
}
else if (value == 101)
{
  // 101に対応する処理
}
else if (value == 102)
{
  // 102に対応する処理
}
else if (value == 103)
{
  // 103に対応する処理
}
else
{
  // それ以外の値の処理
}

これが:

var value = 102;

// switchで分岐
switch (value)
{
  case 100:
    // 100に対応する処理
    break;
  case 101:
    // 101に対応する処理
    break;
  case 102:
    // 102に対応する処理
    break;
  case 103:
    // 103に対応する処理
    break;
  default:
    // それ以外の値の処理
    break;
}

こうなりますね。利点としては、判定される対応する値が「case」句によって明示される(のでわかりやすい)という事でしょうか。また、case句には定数しか置けないので、複雑な判定条件が入る余地がないという事もあります。私はこれを「宣言的」だと感じています。デメリットとして、それがそのまま欠点となり、caseに判定式が書けないという事になります。

C# 1(1ですよ)が出た当時、Javaとの比較において、判定対象に文字列が使えることがアドバンテージだと言われていました。

var value = "CCC";

// switchで分岐
switch (value)
{
  case "AAA":
    // "AAA"に対応する処理
    break;
  case "BBB":
    // "BBB"に対応する処理
    break;
  case "CCC":
    // "CCC"に対応する処理
    break;
  case "DDD":
    // "DDD"に対応する処理
    break;
  default:
    // それ以外の値の処理
    break;
}

私は文字列をswitch-caseの判定に使えるからと言って、それが大きな利点だとは思えませんでした。むしろ、判定対象の値がプリミティブ型と文字列だけではなく、それ以外の型のインスタンスが指定されたら、EqualsやIEquatable<T>を使って判定してくれればもっと使い出があるのに、と思ってました(caseにどのようにインスタンスを指定するのかという課題はありますが…)。このあたりのモヤモヤにも、あとで取り組みます。

さて、switch-caseと同じことをF#のパターンマッチングでやってみます。

let value = 102

// matchでパターンマッチング
match value with
| 100 ->
  // 100に対応する処理
| 101 ->
  // 101に対応する処理
| 102 ->
  // 102に対応する処理
| 103 ->
  // 103に対応する処理
| _ ->
  // それ以外の値の処理

F#を書いたことがない人でもだいたい読めると思います。switch-caseと比較してもほぼ一対一に置き換わっている感じがしますね。

最初のポイントは、match-withを使う場合は「それ以外の処理」を省略できない事です。F#ではすべての式(上記では処理)が戻り値を持つことになっているので、特定のマッチが値を返さないと矛盾してしまいます。最後のアンダースコアを使う “_ -> …” というマッチで、その他の値が担保されます。これによって、valueに対する評価が「網羅される」(条件に抜けがない)ことがコンパイラによって保証されます(漏れていると警告が発生します。目的を考えるとバグと思われるので、警告をエラーとするコンパイルスイッチを有効化しても良いかもしれません)。


直値を避けることと、その判定

上記に挙げたような「直値」による判定は、出来るだけ避けた方が良いというのが一般的な指針でしょう。簡単なミスですが、例えば”100″とタイプするところを”10″とタイプしてしまってこれに気が付かないというようなバグを生んでしまう可能性があります。C/C++言語であれば、#defineによるプリプロセッサを使って置き換えることを考えると思います。或いは(C#でも)enum型をつかえば、直値をシンボリックな値に置き換える事が出来ます。そして、enum値はswitch-caseでも使えます。

# 抽象型を導入して解決すべきという意見もあるかもしれませんが、その話はまた後で。

// enum型を宣言する
public enum Modes
{
  LightMode = 100,
  StandardMode = 101,
  ManualMode = 102,
  CatMode = 103
}

var value = Modes.StandardMode;

// switchで分岐
switch (value)
{
  case Modes.LightMode:
    // LightModeに対応する処理
    break;
  case Modes.StandardMode:
    // StandardModeに対応する処理
    break;
  case Modes.ManualMode:
    // ManualModeに対応する処理
    break;
  case Modes.CatMode:
    // CatModeに対応する処理
    break;
}

良い感じです。普段はシンボル名を使うとして、実際の直値”100″をLightModeに対応付ける方法は考える必要があります。C#の場合は、上記のようにenumの各値にintの値を対応付けることもできるので、これを使うと実装が楽になると思います。

同じようにこれをF#で書きます:

// enum型を宣言する(数値は省略)
type Modes = LightMode | StandardMode | ManualMode | CatMode

let value = Modes.StandardMode

// matchでパターンマッチング
match value with
| Modes.LightMode ->
  // LightModeに対応する処理
| Modes.StandardMode ->
  // StandardModeに対応する処理
| Modes.ManualMode ->
  // ManualModeに対応する処理
| Modes.CatMode ->
  // CatModeに対応する処理

match-with式が値の網羅性を担保するため、enum型の値4つのパターンが網羅されていればコンパイルに成功します。逆に足りない場合は警告が発生するので、うっかり記述漏れを起こすということがありません。逆に「該当しない」場合を泥縄的に処理する場合は、defaultと同じように書く事が出来ます:

match value with
| Modes.LightMode ->
  // LightModeに対応する処理
| Modes.CatMode ->
  // CatModeに対応する処理
| _ ->
  // それ以外の値をすべて処理

F#が、式が完全である事を要求するこだわりは、以下の例でよくわかります。

// enum型を宣言する(対応する数値を明示する)
type Modes = LightMode = 100 | StandardMode = 101 | ManualMode = 102 | CatMode = 103

let value = Modes.StandardMode

// "警告 FS0025: この式のパターン マッチが不完全です
//  たとえば、値 'enum<Modes> (0)' はパターンに含まれないケースを示す可能性があります。"
match value with
| Modes.LightMode ->
  // LightModeに対応する処理
| Modes.StandardMode ->
  // StandardModeに対応する処理
| Modes.ManualMode->
  // ManualModeに対応する処理
| Modes.CatMode->
  // CatModeに対応する処理

まず、enum型の値に数値を明示する場合は、すべての値に明示する必要があります(C#では省略可能で、直前の値がインクリメントされる)。そして、上記のenum値には”0″に対応する値が定義されていないため、「valueが値”0″に相当した場合の処理を網羅していない」という警告が発生します。

詳しい説明は省きますが、enum型はValue-Type(値型)なので、デフォルトの値(C#で言う所のdefault(Modes))となる可能性があり、ここではそのことを警告しています(言語というより、.NET CLRの特性による)。enum型の値に対応する数値を明示的に指定しない場合、enum型の最初の値(上記ではLightMode)が値”0″に対応するので、結果的に警告は発生しません。


switch-caseとパターンマッチングの分かれ目

前節の例のように、F#は「転びそうなミスを拾ってくれ」ます。しかし、今まで見てきた例は、殆ど直接的にif-then-elseやswitch-caseに置き換える事が出来ます。これでは、パターンマッチングという新しい武器に「興味をそそられない」としても、仕方が無いと思います。

しかし、ここからがパターンマッチングの強力さを発揮する部分です。

複数の値の組み合わせに基づいて処理を決定するようなコードを書いたことがあるでしょう。ここにswitch-caseを適用すべきかどうかは、個人によって判断の基準が色々ありそうです。ここではベタに書いてみます。

// enum型の定義は省略
var value1 = Modes1.StandardMode;  // (4種類)
var value2 = Modes2.PowerfulMode;  // (3種類)

switch (value1)
{
  case Modes1.LightMode:
    switch (value2)
    {
      case Modes2.MinimumMode:
        // LightModeかつMinimumModeの処理
        break;
      case Modes2.RichMode:
        // LightModeかつRichModeの処理
        break;
      case Modes2.PowerfulMode:
        // LightModeかつPowerfulModeの処理
        break;
    }
    break;

  case Modes2.StandardMode:
    switch (value2)
    {
      case Modes2.MinimumMode:
        // StandardModeかつMinimumModeの処理
        break;
      case Modes2.RichMode:
        // StandardModeかつRichModeの処理
        break;
      case Modes2.PowerfulMode:
        // StandardModeかつPowerfulModeの処理
        break;
    }
    break;

  // (以下、ひたすら続く...)
}

「こんな惨いコード書かねぇ!これならifで書いたほうが良い!!」或いは「抽象化しろ!」って言われそうです。見た目の問題やコードがやたら長くなることも問題ですが、もっと深刻なのは、組み合わせが正しく網羅されているのかどうかを確認するのが大変そうだという事です。

F#で書いてみます:

// enum型の定義は省略
let value1 = Modes1.StandardMode;  // (4種類)
let value2 = Modes2.PowerfulMode;  // (3種類)

// 組み合わせをパターンマッチングで評価する
match value1, value2 with
| Modes1.LightMode, Modes2.MinimumMode ->
  // LightModeかつMinimumModeの処理
| Modes1.LightMode, Modes2.RichMode ->
  // LightModeかつRichModeの処理
| Modes1.LightMode, Modes2.PowerfulMode ->
  // LightModeかつPowerfulModeの処理

| Modes1.StandardMode, Modes2.MinimumMode ->
  // StandardModeかつMinimumModeの処理
| Modes1.StandardMode, Modes2.RichMode ->
  // StandardModeかつRichModeの処理
| Modes1.StandardMode, Modes2.PowerfulMode ->
  // StandardModeかつPowerfulModeの処理

| ... (以下、マッチ式が続くが、組み合わせが網羅されてないと警告が出る)

私は初見で、C#にもこれ欲しい!!と思いました。或いは、これならネストしていた方が良いと思う人もいるかもしれません(matchをネストさせることもできますが省略)。

しかし、パターンマッチングはもっともっと強力です。面白いのは、組み合わせ網羅を全部書かなくても良いことです:

// 組み合わせをパターンマッチングで評価する
match value1, value2 with
| Modes1.LightMode, Modes2.MinimumMode ->
  // LightModeかつMinimumModeの処理
| Modes1.LightMode, Modes2.RichMode ->
  // LightModeかつRichModeの処理
| Modes1.LightMode, Modes2.PowerfulMode ->
  // LightModeかつPowerfulModeの処理

| _, Modes2.MinimumMode ->
  // 残りのMinimumModeの処理
| _, Modes2.RichMode ->
  // 残りのRichModeの処理
| _, Modes2.PowerfulMode ->
  // 残りのPowerfulModeの処理

// (これですべて網羅された)

これはつまり、value1がLightModeの時だけ、value2に応じた個別の処理を行い、value1がそれ以外の値の場合はvalue2に応じた共通かつ個別の処理を行うという事です。これを見て、コードがより「宣言的」に近づいたと思いませんか? 実際、F#コンパイラはこれらの網羅に漏れがないかどうかをチェックしています。さらに複雑な例を見てみます:

// 組み合わせをパターンマッチングで評価する
match value1, value2 with
| Modes1.LightMode, Modes2.MinimumMode ->
  // LightModeかつMinimumModeの処理
| Modes1.LightMode, Modes2.RichMode ->
  // LightModeかつRichModeの処理
| Modes1.LightMode, Modes2.PowerfulMode ->
  // LightModeかつPowerfulModeの処理

| Modes1.CatMode, Modes2.RichMode ->
  // CatModeかつRichModeの処理

| _, Modes2.MinimumMode ->
  // 残りのMinimumModeの処理
| _, Modes2.PowerfulMode ->
  // 残りのPowerfulModeの処理

| _, _ ->
  // 残りのすべての処理

「残りのすべての処理」が無いと警告が発生します。何が該当するかは、実際に考えてみて下さい。

もちろん、match式の組み合わせ対象の値は、更に3個4個…n個とカンマで区切って指定できます。複雑にネストしてしまうようなif-thenやswitch-caseよりもスマートに書ける上に、メタプログラミング(T4のようなコード生成)でmatch式を動的に生成する場合にも重宝しそうです。


タプルとパターンマッチングの関係

前述の例で、Modes1の残りの値の組をデフォルト処理しましたが、具体的に値が何であったのかを知る方法があります。

// 組み合わせをパターンマッチングで評価する
match value1, value2 with
| Modes1.LightMode, Modes2.MinimumMode ->
  // LightModeかつMinimumModeの処理
| Modes1.LightMode, Modes2.RichMode ->
  // LightModeかつRichModeの処理
| Modes1.LightMode, Modes2.PowerfulMode ->
  // LightModeかつPowerfulModeの処理

| v1, Modes2.MinimumMode ->
  // (v1からModes1の実際の値が得られる)

| _, _ ->
  // 残りのすべての処理

アンダースコアの代わりにシンボル(ここではv1)を指定することで、対応する値が何であったのかを得る事が出来ます。value2はマッチ式からMinimumModeであることが分かっているので、value2については不要ですね。この例ではv1とはつまりvalue1の事なので、値が取得できたところで意味が無い(value1でアクセスすればよい)ように見えます。この構文が生かされるのはもう少し後です。

所で、マッチ式を以下のようにして、結果をまとめて取得する方法もあります。

// 組み合わせをパターンマッチングで評価する
match value1, value2 with
| Modes1.LightMode, Modes2.MinimumMode ->
  // ...
| Modes1.LightMode, Modes2.RichMode ->
  // ...
| Modes1.LightMode, Modes2.PowerfulMode ->
  // ...

| _, _ as pair ->
  // (pairで両方の値にアクセスできる)

「as」演算子を使うと、パターンマッチングの結果が参照できるようになります。では、上記の「pair」で何が参照出来るのでしょうか? 実はpairは「タプル」になります。F#のタプルは、内部的にはSystem.Tuple<…>で定義されているのですが、Item1、Item2のようなプロパティは参照できず、その必要もありません。

タプルの値を分解するには、以下のようにletを使います:

// 組み合わせをパターンマッチングで評価する
match value1, value2 with
  // ...

| _, _ as pair ->
  let v1, v2 = pair  // v1とv2にそれぞれItem1とItem2の値が入る
  System.Console.WriteLine("{0}, {1}", v1, v2)  // 普通に使える

v1とv2はもちろん型推論で自動的にModes1型とModes2型として扱われます。ところでこの式を見ていて、マッチ式がモヤモヤしてきませんか?

// 組み合わせをパターンマッチングで評価する
match value1, value2 with
  // ...

| v1, v2 ->  // letで分解しなくても、実はこれでいい?
  // let v1, v2 = pair  // v1とv2にそれぞれItem1とItem2の値が入る
  System.Console.WriteLine("{0}, {1}", v1, v2)

そして:

// タプルを作る
let pair = (value1, value2)

// それって実はこうじゃね?
match pair with
| Modes1.LightMode, Modes2.MinimumMode ->
  // ...
| Modes1.LightMode, Modes2.RichMode ->
  // ...
| Modes1.LightMode, Modes2.PowerfulMode ->
  // ...

| v1, v2 ->  // letで分解しなくても、実はこれでいい?
  // let v1, v2 = pair  // v1とv2にそれぞれItem1とItem2の値が入る
  System.Console.WriteLine("{0}, {1}", v1, v2)

実は “match value1, value2 with” という式は、組み合わせマッチングのための特別な構文ではなく、その場でvalue1とvalue2のタプルを作って渡しているだけと見る事が出来ます。更にマッチ式のカンマで区切った構文も特別な構文ではなく、タプルの各要素にマッチングしているだけなのです!!

そういうわけで、パターンマッチングを書いている場合は、実はあまりタプルの事は意識していませんが、内部では非常に多くの場面でタプルが自動的に使われています。

この例でもまだvalue1とvalue2にはアクセスできるので、v1、v2に分解する事に意味を見いだせないかもしれません。これならどうでしょうか:

// 関数化(引数はタプル)
let applyPair p =
  // タプルから直接パターンマッチングする
  match p with
  | Modes1.LightMode, Modes2.MinimumMode ->
    // ...
  | Modes1.LightMode, Modes2.RichMode ->
    // ...
  | Modes1.LightMode, Modes2.PowerfulMode ->
    // ...
  | v1, v2 ->  // マッチ式で直接タプルを分解
    System.Console.WriteLine("{0}, {1}", v1, v2)

// タプルを作る
let pair = (Modes1.StandardMode, Modes2.PowerfulMode)

// 関数呼び出し
applyPair pair

もはやapplyPairは独立した関数なので、内部のマッチ式で直接value1、value2にはアクセスできません。それどころかタプルを直接生成しているので、value1もvalue2も定義されていません。

関数の引数pは型が推測され、Modes1とModes2のタプルとなります。そしてマッチ式によって判定とタプルの分解が一度に行われます。これに慣れてくると、Tuple<…>ほにゃほにゃとか、Item1、Item2のようにプロパティにいちいちアクセスするのが、とても面倒になってきます。また、C#でTupleを使うと意味が不明瞭になるため乱用しない方が良いというセオリーがありますが、F#のタプルではその点で困ることはあまりありません。


シーケンスのマッチング

シーケンスとは、IEnumerable<T>の事です。つまりC#で言う所のLINQ可能なインスタンスです。F#の世界では、手軽に扱えるシーケンスが2種類あります。「リスト」と「配列」です。F#配列は、System.Arrayの派生クラスなのでC#と同じですが、F#のリストは.NETの「System.Collections.Generic.List<T>」クラスではなく、読み取り専用の順方向リンクリストとして実装されたクラスです。これを使うと、シーケンスの各要素をパターンマッチングでマッチさせながら分解までやらせることが出来ます。

// F#リストを手動で定義
let values = [102; 107; 111; 133; 50]

// F#リストをパターンマッチングする
match values with
| [] ->
  // 空のリストにマッチ
  System.Console.WriteLine("Empty")

| [x; y; z] ->
  // 3つの要素が含まれるリストにマッチし、かつそれぞれの値を参照可能にする
  System.Console.WriteLine(
    System.String.Format(
      "x={0},y={1},z={2}",
      x, y, z))

| [a; b; c; d; e] ->
  // 5つの要素が含まれるリストにマッチし、かつそれぞれの値を参照可能にする
  System.Console.WriteLine(
    System.String.Format(
      "a={0},b={1},c={2},d={3},e={4}",
      a, b, c, d, e))

| xs ->
  // その他のリストにマッチし、xsで参照可能にする
  System.Console.WriteLine(System.String.Join(",", xs))

この例では、a,b,c,d,eの5つの要素を分解するマッチ式にマッチします。これもベタで書くと面倒なコードになりますが、パターンマッチングで書けばシンプルかつ安全に書くことが出来ます。

配列の構文は “[| … |]” ですが、上記のF#リストの例と全く同じように記述できます。残念ながら「シーケンスそのもの」をパターンマッチングでマッチさせる事はできません。シーケンスを扱う場合は、リストか配列に固定化すれば扱えます。

// F#リストを手動で定義し、シーケンスとして扱う
// 型: seq int (IEnumerable<int>)
let values = seq [102; 107; 111; 133; 50]

// シーケンスをF#リストに変換してマッチさせる
match values1 |> Seq.toList with
| [] ->
  // ...
| [x; y; z] ->
  // ...
| [a; b; c; d; e] ->
  // ...
| xs ->
  // ...

ところで、パターンマッチングで使える道具はほとんど自由に組み合わせて使えます。enum型も使えるし、タプルでもOKです。それらをマッチ式内で分解させると、普通に書くには面倒すぎる操作も自由自在です。組み合わせた例を示します:

// enum型を宣言する
type Modes = LightMode | StandardMode | ManualMode | CatMode

// 複雑なF#リストを定義する
let values = [
  (102, ("ABC", Modes.StandardMode));
  (107, ("DEF", Modes.CatMode));
  (111, ("GHI", Modes.LightMode));
  (133, ("JKL", Modes.StandardMode));
  (50, ("MNO", Modes.ManualMode))
]

// F#リストを要素毎に定義されたマッチ式でマッチさせる
match values with
| [] ->
  System.Console.WriteLine("Empty")

| [(number0, (name0, mode0)); (number1, (name1, mode1))] ->
  // 2つの要素が含まれるリストにマッチし、内容をすべて分解して参照可能にする
  System.Console.WriteLine(
    System.String.Format(
      "Number={0},Name={1},Mode={2}",
      number0, name0, mode0))
  System.Console.WriteLine(
    System.String.Format(
      "Number={0},Name={1},Mode={2}",
      number1, name1, mode1))

| [(number0, (name0, mode0)); _; _; _; (number1, (name1, mode1))] ->
  // 5つの要素が含まれるリストにマッチし、最初と最後の要素だけを分解して参照可能にする
  System.Console.WriteLine(
    System.String.Format(
      "Number={0},Name={1},Mode={2}",
      number0, name0, mode0))
  System.Console.WriteLine(
    System.String.Format(
      "Number={0},Name={1},Mode={2}",
      number1, name1, mode1))

| _ ->
  // ...

マッチする事自体は判定したいけど、実際にマッチした値が不要なら、アンダースコアを使って取得しない、という選択も出来ますよ。そしてもちろん、タプルの内側の特定の値だけ不要な場合とか、全く同じように書けます(例えばname1が不要なら、”(number1, (_, model1))”)。

「うわ、何これ…」みたいに感じてきましたか? まだまだこれからです。


実行時の型でマッチングする

例えば、与えられる値の種類に応じて、それぞれの処理を切り替えたいという要求はよくあると思います。XML DOMのノードが良い例です。XMLのノードは、エレメント・テキスト・XML属性などで構成されていますが、トラバースする場合は同じ「ノード」として扱い、それぞれの場合に応じて処理を変更する必要があります。

using System.Xml.Linq;

// 指定されたノードを解析して結果を取得する再帰メソッド
public static string TraverseNode(XNode node)
{
  // ノードがエレメントなら
  var element = node as XElement;
  if (element != null)
  {
    // 子ノードも探索する
    return string.Format(
      "Element: {0} {{ {1} }}",
      element.Name,
      string.Join(", ", element.Elements().Select(child => TraverseNode(child))));
  }

  // ノードがテキストなら
  var text = node as XText;
  if (text != null)
  {
    return string.Format(
      "Text: \"{0}\"",
      text.Value);
  }

  // (他は省略)
  return "(Unknown)";
}

ここで、switch-caseが使えれば良いのですが、caseには直値を指定しなければならないため、switch-caseは使えません。同じような処理をF#で書いてみます:

open System.Xml.Linq

// 指定されたノードを解析して結果を取得する再帰関数(引数に型注釈が必要)
let rec traverseNode (node: XNode) =
  match node with
  // ノードがエレメントなら
  | :? XElement as element ->
    System.String.Format(
      "Element: {0} {{ {1} }}",
      element.Name,
      System.String.Join(", ", (element.Elements() |> Seq.map (fun child -> traverseNode child))))

  // ノードがテキストなら
  | :? XText as text ->
    System.String.Format(
      "Text: \"{0}\"",
      text.Value)

  // (他は省略)
  | _ -> "Unknown"

# 型注釈と言うのは、型を推測できない場合にヒントとして補うことです。この例で型注釈(XNode)が必要な理由については次節で説明します。

マッチ式が “:? XElement as element” となっていますが、これは「指定された値の型がXElementでキャスト可能な場合に、キャストされた値を”element”で参照できるようにする」という意味です。XTextも同様です。このように、switch-caseのような直値だけではなく、柔軟な判定を行う事が出来ます。

マッチ式に “:?” パターンを使う場合は、必ずすべてのマッチ式が “:?” を使わなければならない訳ではありません。有効なマッチ式であればどのようにでも指定する事が出来ます。


言語機能の直交性

前節の例では、.NET標準のXML LINQの型を使ったので、型の動的な判定を行わせています。XElementやXTextはXNodeを継承しているという「特性を利用」し、引数ではXNodeを渡させています。引数にXNodeの型注釈が必要なのは、クラスの継承やインターフェイスの実装を考えると、F#だけで型を推測することが出来ないためです(未知の関連型を網羅検索出来る必要がある)。

そのため、F#だけで書いてよいのなら、OOP的なアプローチを取らず、「判別共用体」という種類の型を使います。

// ノード型(判別共用体)
type Node =
  // エレメントの場合は名前と子要素のシーケンスをタプルで保持
  | Element of (string * (Node seq))

  // テキストの場合は文字列を保持
  | Text of string

# タプルの型記法は慣れてないと気持ち悪いかもしれません “(type1 * type2 …)” のようにアスタリスクで要素の型を区切ります。

「共用体」というと、C言語のunionを思い浮かべる人も居るかもしれませんが、意図としては似ています。このコードは”Node”型を定義しますが、これはいわゆる「基底クラス」や「基底インターフェイス」ではありません。内部に定義されている「Element」と「Text」の二つの種類の値(用語でTagと言います)を持ちうる型です。また、この値以外の値が存在しない(未知の値がない)事がはっきりしています。

enum型と比較すると、シンボル名が整数に対応づけされるものではなく、それぞれの値が独自の異なる値の組を持つ事が出来るところが違います。

  • Nodeの値が”Element”である場合は “(string * (Node seq))” つまり文字列とNodeのシーケンス(のタプル)を持つ。
  • Nodeの値が”Text”である場合は “string” つまり文字列だけを持つ。

# 判別共用体がIL上でどのように実現されているのかについては、ここでは触れません。少なくともILのネイティブ表現に対応するものはありません。

C言語のunionは危険な使い方が出来ますが、判別共用体で危険な使い方は出来ません。例えば、Nodeの値が”Text”であるのに、無理矢理Elementとみなしてアクセスすることは不可能です。逆もまた然り。その安全性の担保は、マッチ式で行われます。

type Node =
  | Element of (string * (Node seq))
  | Text of string

// 指定されたノードを解析して結果を取得する再帰関数
let rec traverseNode node =
  match node with
  // ノードがエレメントなら
  | Element (name, children) ->
    System.String.Format(
      "Element: {0} {{ {1} }}",
      name,
      System.String.Join(", ", (children |> Seq.map (fun child -> traverseNode child))))

  // ノードがテキストなら
  | Text text ->
    System.String.Format(
      "Text: \"{0}\"",

  // (他の値はとりようがないのですべて網羅された)

このコードには色々と「おいしい」部分が含まれています。まず、関数の引数に型注釈は不要です。どうやってNode型であることを推測しているかというと、マッチ式に”Element”と”Text”が現れ、しかもキャプチャする引数の並びがすべて一致(Elementは2個、Textは1個)し、それぞれの引数の型が、内側の式から推測された型に一致する定義がNode型しか無いと認識できるからです。

そしてマッチ式を見ると、ElementとTextに「関数引数のようなもの」が指定されています。これらが、それぞれの値(Tag)にマッチした時の値として参照可能になります。つまり:

type Node =
  | Element of (string * (Node seq))  // (name, children)
  | Text of string                    // text

let rec traverseNode node =
  match node with
  | Element (name, children) ->
    // 判別共用体Element Tagのタプルにマッチし、
    // タプルの値がそれぞれ name と children で参照可能となるように分解される

  | Text text ->
    // 判別共用体Text Tagにマッチし、文字列がtextで参照可能になる

前節でタプルをパターンマッチングで分解する例を見せましたが、ここでも分解が応用されています。もちろん:

let rec traverseNode node =
  match node with
  | Element pair ->
    // 判別共用体Element Tagのタプルにマッチし、pairで参照可能になる
    let name, children = pair
    // ...

  | Text text ->
    // 判別共用体Text Tagにマッチし、文字列がtextで参照可能になる

タプルをマッチ式で分解しないで、直接参照可能にすることもできます。

このように、パターンマッチングで使われるこまごまとした機能が直交的に組み合わされて、高い表現力を持つようになっています。前節で示した、enum型・タプルのネスト・シーケンスのマッチングを判別共用体と組み合わせることも可能です。


ネストした判別共用体のマッチング

パターンマッチの機能が直交的だと思える例をもう一つ。ここまでで示した道具がすべて組み合わせ可能と言う事は、判別共用体自身もまた組み合わせ可能と言う事です。ネストした判別共用体の強力なパターンマッチング例をお見せします。

type Value =
  | Numeric of int
  | Text of string
  | Date of System.DateTime

// ネストした判別共用体
type Information =
  | Type1 of (string * Value)
  | Type2 of string

// Information型の値
let value = Type1 ("ABC", Numeric(123))

// Information型のパターンマッチング
match value with
| Type1 (name, Numeric numeric) ->
  System.Console.WriteLine(
    "Type1: Name={0}, Numeric={1}",
    name,
    numeric)

| Type1 (name, Text text) ->
  System.Console.WriteLine(
    "Type1: Name={0}, Text={1}, Length={2}",
    name,
    text,
    text.Length)

| Type1 (name, Date date) ->
  System.Console.WriteLine(
    "Type1: Name={0}, Year={1}, Month={2}, Day={3}",
    name,
    date.Year,
    date.Month,
    date.Day)

| Type2 text ->
  System.Console.WriteLine(
    "Type2: Text={0}",
    text)

もちろん、いくらでもネスト可能で、ネストした細部の値を直接分解して参照することが出来ます。パターンマッチング無しで同じ処理を実現しようと思うと、不可能ではありませんが(大した処理ではないにも関わらず)非常に面倒でミスも起きやすいコードになると思います。


型のメンバーにパターンマッチングしたい

ここまでマッチングが柔軟であると、クラスのメンバ、例えばプロパティに対してもパターンマッチングしたいと考えるかもしれません。

// クラス型の定義(引数はコンストラクタの引数)
type DemoClass (firstName: string, lastName: string, age: int) =
  // コンストラクタ引数の値を読み取り専用プロパティとして公開
  member __.FirstName = firstName
  member __.LastName = lastName
  member __.Age = age

// DemoClassのインスタンスを生成する
let value = DemoClass("Taro", "Hoge", 25)

// クラスのインスタンスをマッチングする
// "{ ... }"によるパターンの記法については後述
match value with
// エラー FS1129: 型 'DemoClass' にフィールド 'FirstName' が含まれません
| { FirstName=fn; LastName=ln; Age=a } ->
  System.Console.WriteLine(
    "Name={0} {1}, Age={2}",
    fn, ln, a)

# クラス型は上記のように定義しますが、詳細は省きます。コンストラクタが定義されること、プロパティが定義されてコンストラクタの引数が渡されることに注目してください。

しかし、マッチ式でプロパティが認識できません。これまで見てきたように、マッチ式は分解を行うこともできますが、単に値が一致することを確認する場合もあります。その両方の構文を満足させるためには、上記のような構文をサポートするだけでは不十分で、DemoClassのインスタンスを任意の値で初期化できて、かつ副作用が存在しない事が明らかであるような、宣言的な手段が必要です。

F#には「レコード型」という種類の型があり、この目的に使うことができます。レコード型の中身(IL)は単なるクラス型ですが、F#上では定義の構文が異なります。

// レコード型を定義する
type DemoRecord = {
    FirstName : string
    LastName : string
    Age : int
}

// レコード型の初期化(型は推測される)
let value = {
    FirstName = "Taro"
    LastName = "Hoge"
    Age = 25
}

// レコード型をマッチングする
match value with
| { FirstName=fn; LastName=ln; Age=a } ->
  // レコード型の各フィールドを分解できる
  System.Console.WriteLine(
    "Name={0} {1}, Age={2}",
    fn, ln, a)

レコード型を使うと、判別共用体と同じく型推論が働くようになります。上記の例ではvalueのインスタンスを生成するのに、型名”DemoRecord”を書く必要がありません。レコード型はF#によってフィールド(FirstName・LastName・Age)の初期化方法が管理可能な形で定義されるので、式だけで初期化が可能になり、パターンマッチングでもフィールド名を自動的に特定して値の分解もできるようになるのです。

レコード型のパターンには、”{ … }”という記法を使う事が出来ます。コード例に示した通り、「フィールド名=(マッチ式)」として記述できます。右辺を「マッチ式」と書いた通り、ここに更にネストしたマッチ式を書くことが出来ます。

さらなる例は省きますが、このように、レコード型もこれまでのマッチ式と柔軟に組み合わせて使用することができます。


真打ち・アクティブパターン

ここまでのパターンマッチングでも、もうかなりの応用力がある武器ですが、今まではささやかな序章だった・真のラスボスは云々、と言ったら信じてもらえますか? (*´Д`)

私が真面目にF#をモノにしようと決めたきっかけが、このアクティブパターンです。アクティブパターンは大きく2種類あるのですが、順番に解説します。どちらのアクティブパターンも「パターンマッチングを動的に実行できる」事が特徴です。

動的な分解

前節で、クラス型のメンバーを直接キャプチャ出来ないため、レコード型を使用するという例を示しました。アクティブパターンを使用すれば、クラスのメンバーを直接分解・キャプチャさせることが出来ます。

// DateTimeを分解してタプルで返すレコグナイザー関数
let (|Date|) (dt: System.DateTime) =
  dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, dt.DayOfWeek

let now = System.DateTime.Now

// DateTimeをマッチングする
match now with
// レコグナイザーを使ってタプルに分解する
| Date (yyyy, MM, dd, HH, mm, _, _) ->
  System.Console.WriteLine(
    System.String.Format(
      "yyyy:{0} MM:{1} dd:{2} HH:{3} mm:{4}",
      yyyy, MM, dd, HH, mm))

# 本来DateTimeはクラスではなくて構造体ですが、違いはありません。

マッチングを動的に判定するには、「アクティブレコグナイザー関数」を定義します。この関数の名前は、何かしら魔術めいた括弧 “(| … |)” で括られていますが、これは「バナナクリップパターン」(バナナのように見えますね?)と呼びます。

中身は単なる関数です。したがってどのような値でも返すことが出来、返された値がマッチ式でキャプチャ出来るようになります。ここでは、分解した結果をタプルで返しているので、マッチ式でもタプルで受け取っています。そして、キャプチャしたくない要素はアンダースコアで捨てることもできます。

値を解析して分類分けできる

与えられた値に応じた結果を動的に変えられると便利です。例えば、判別共用体はTagによってマッチの有無が(静的に)判定されますが、この判定を動的に実行できるようなものです。

// ゼロ・奇数・偶数を識別するレコグナイザー関数
let (|Zero|Even|Odd|) value =
  match value, (value % 2) with
  | 0, _ -> Zero
  | _, 0 -> Even (value / 2) // 除算結果を返す
  | _, 1 -> Odd (value / 2)  // 除算結果を返す

let value = 131

match value with
// レコグナイザーを使って判定とキャプチャを行う
// ゼロにマッチ
| Zero ->
  System.Console.WriteLine("Zero")

// 偶数にマッチ
| Even half ->
  System.Console.WriteLine(
    System.String.Format(
      "Even: HalfValue={0}", half))

// 奇数にマッチ
| Odd half ->
  System.Console.WriteLine(
    System.String.Format(
      "Odd: HalfValue={0}", half))

// (Zero・Even・Odd以外の値にはなり得ない)

レコグナイザー関数の名前は、更に奇妙な事になっています。この例では、レコグナイザーの結果として取りうる値を “|” で区切ります。値がゼロ・偶数・奇数に応じて、”Zero”, “Even” 又は “Odd” の値を返すことを意味します。また、EvenやOddの場合は、除算の結果(value / 2)も返しています。

このレコグナイザーをマッチ式で使うと、マッチングの実行時に関数が呼び出され、値を動的に判定してZero・Even・Oddを返すので、それぞれのマッチ式にマッチします。Zeroの場合は値が得られませんが、EvenとOddは除算結果が返されるので、値をキャプチャすることが出来ます(half)。

また、このレコグナイザーを使うと言う事は、値がZero・Even・Oddの何れかにしか分類されないことがはっきりしているため、それ以外の値をとる事がありません(その他にマッチする式が不要)。

このマッチ式を見ていると、判別共用体を動的に判定しているかのように見えますね。最初のswitch-caseの説明で、数値や文字列だけではなく、任意の値(インスタンス)に対してEqualsやIEquatableを使ってカスタムコードで判定を行うことができれば良いのに、という話をしました。レコグナイザー関数を使えば、そういった判定を安全に行うことが出来ます。

値を解析してマッチ式の判定に影響させる

上記のレコグナイザーは、取りうる結果の種類があらかじめ完全に把握されている(Zero・Even・Oddの何れかしかない)場合に使えます。しかし、判断が該当しない場合は、単に「該当なし」として判定したい事もあります。

// 正の偶数を識別するレコグナイザー関数
let (|IsPositiveEven|_|) value =
  if value < 0 then
    None  // 該当しない
  else if (value % 2) = 1 then
    None  // 該当しない
  else
    Some (value / 2)  // 該当したので除算した結果を返す

let value3 = 131

match value3 with
// レコグナイザーを使って判定とキャプチャを行う
| IsPositiveEven half ->
  System.Console.WriteLine(
    System.String.Format(
      "Plus & even: HalfValue={0}", half))

// 正の偶数ではなかった
| v1 ->
  System.Console.WriteLine(
    System.String.Format(
      "Other: {0}", v1))

このレコグナイザー関数は、末尾にアンダースコアを付けることで「パーシャルレコグナイザー」として機能するようになります。パーシャルレコグナイザーは、戻り値が “Option型” の値で返される事を想定します。

Option型は判別共用体で、”Some”と”None”のTagを持ちます。C#で言うところの”Nullable”に近いのですが、存在するならその値、または値が存在しないと言う事を表し、判別共用体なので安全に使用できます。上記の例では、与えられたvalueが負数か0なら”None”、それ以外の場合はその値の1/2を返すようになっています。

このレコグナイザーを使用すると、マッチ式の評価は “Some” の場合にだけ変換された値がキャプチャされ、”None” の場合はそのマッチ式がマッチしなかったことになり、他のマッチ式を試行します。従って、この場合は変換に失敗したマッチを用意する必要があります(マッチが漏れていると警告が発生します)。

アクティブレコグナイザーを使うことで、これまで静的な宣言による判定しかできなかったコードで、動的に複雑な判定を行わせることが出来るようになります。しかもその実装は単なる関数であり、判定の詳細を関数内に完全にカプセル化することが出来ます。こうして設計されたレコグナイザーは、他のパターンマッチングのすべてのパターンと直交的に組み合わせて使用できるため、非常に高い表現力・安全性・応用性を持ちます。


まとめ

F#でのパターンマッチングを、これまでの一般的な比較・分岐処理と対比させて解説しました。私の感想としては、やはりアクティブパターン強し!と言う所です。私はC#のLINQも好きですが、LINQが凄かったのは、IEnumerable<T>による統一された集合処理と、拡張メソッドによる直交性のある独自の拡張が可能であったことだと思います。F#で直接対比するとすればシーケンス処理がそれに該当するのですが、正直すでにLINQが当たり前なので、F#で同じことが出来ることはある意味当然というイメージでした(むしろ、F#にはシーケンス処理を汎化した「コンピュテーション式」があり、そっちはこれまた大海原です)。

パターンマッチングの良いところは、今までであれば「デザインパターン」に落とし込んで回避しなければならなかったような複雑な構造を、シンプルに、簡単に、わかりやすく、安全に記述出来て、検証可能で、しかもすぐに応用することが出来るようになった事だと思います。例えば、前半で示したパターンの組み合わせについて、抽象クラスを導入して実装を派生させて…という考え方ももちろん可能です。しかし、F#の世界ではそんな事をしなくても、タプルや判別共用体とパターンマッチングを組み合わせるだけで解決できてしまいます。

プログラミング言語の進化としては、ジェネリックプログラミング以来のインパクトを感じました。F#ではこれらがもう当たり前の世界になっていて、C#もこれからパターンマッチングを取り込んでいくと思います。応用性の高い、直交性のある拡張を期待したいです。

補足: パターンマッチングのマッチ式内で使用できるパターンは、すべて網羅していません。より複雑なパターンもサポートされています。この記事で解説した内容を把握していれば、それらについても容易に理解できると思います。詳しくはMSDNを参照してください。


修正履歴

  • タプルの型Tuple<…>が隠され、Item1, Item2等のプロパティに直接アクセスできないので修正しました。もちろん、リフレクションを使用した場合はこの限りではありません。
  • F#のリストは順方向リンクリストであることを明示しました。内部的にはFSharpList<‘T>というクラスです。
  • “:?”や”[|…|]”や”(|…|)”は演算子ではないので、記述を改めました。MSDNには「パターン」と書かれています。パターンという語は一般的過ぎて説明には適さないと思っているのですが、合わせるようにしています(まだ文書全体では統一されていません)。
  • “:?”パターンでマッチさせたい目的が、型の動的判断ぐらいしかないという記述は無くしました。実際にはすべてのパターンは直交的に組み合わせ可能なので、実現したい判断次第ですね。
  • 関数名をよりふさわしく修正し、int.TryParseという使われていない解説を修正しました。
  • OptionとNullableの関係について修正しました。Optionは判別共用体である事にフォーカスさせました。

※まだ修正作業中です。気になる方はfsugjpでの指摘を参照してください。

Docker for Windows betaを試す (Hyper-V enabled)

※WordPressが不調でレイアウトがものすごく酷い事になってます。そのうち直します。

docker今更説明不要だと思いますが、Docker。そのDockerの標準提供されている動作環境の一つに「Docker for Windows」があります。

Docker for Windowsは、Dockerの実行環境をWindows上で実現する、手軽なパッケージです。但し、現状のDocker for Windowsは「Virtual Box」を使って実現しているため、Hyper-Vを使用する環境では使えません(Hyper-Vを無効にする必要がある)。

※正確には、Docker for Windowsに含まれる「Docker Engine」がVirtualBoxを必要としています。Docker clientは単独で動作します。

現在新たに開発中のバージョンでは、仮想マシンをVirtualBoxではなくHyper-V上で実現させることが出来るようになっていて、非常に操作性に優れています。このベータ版は、Dockerのベータプログラムに申し込んだユーザーに順次配布されているようで、先日私のところにもやってきたので早速試してみました。

Dockerをサクッと試してみたいという方も、これで始められると思います。


ベータプログラム

ベータプログラムは以下から申し込めます。

Explore a new kind of Docker – Docker early access

いつ配布されるのかは良くわかりません。私もWindows Subsystem for Linuxにかまけてすっかり忘れていました。


インストール

dockerbeta1確かめていませんが、古いDocker for Windowsはアンインストールしておいた方が良いでしょう。

ベータプログラムの順がやってくると、メールで案内が来ます。メール文中にDocker for Windowsのベータ版へのリンクと、アクセスキーが含まれているので、それぞれ保存しておきます。

dockerbeta2ダウンロードしたmsiを実行します。インストールは非常に簡単ですが、Hyper-Vを構成している場合は、デフォルトの構成ファイルと仮想イメージの配置フォルダにDocker Engineが配置されるので、必要であればあらかじめ配置されるフォルダを変更しておくことをお勧めします。

dockerbeta3インストールされると、デスクトップにこのようなアイコンが配置されます。

dockerbeta4起動すると、初回はこのようなダイアログが表示されます。ベータプログラムのキーを入力して開始します。

dockerbeta5すると、タスクトレイにこのようなクジラのアイコンが表示されます。

いくつかの環境で試したのですが、一台だけHyper-Vの仮想スイッチの構成がうまくいかず、「DockerNAT」仮想スイッチが外部ネットワークに接続されていないケースがありました。もし起動時に通信できない事があった場合は、Hyper-Vマネージャの仮想スイッチの構成を確認してみて下さい。

私の環境では、他に仮想スイッチがあった場合に、一旦それらを削除しておいて実行したところ、正常に動作しました。例えば、Visual StudioでWindows Phoneエミュレーターを起動した場合に、自動的に仮想スイッチが構成されるので、これを削除しておきます(DockerNATが作られた後で、手動で戻す)。

Dockerチームはbeta8でこの事を認識しているので、いずれ修正されるでしょう。

“Error response from daemon: dial tcp 10.0.75.2:2375: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond”


Docker EngineがHyper-Vで起動

dockerbeta6これを待ち望んだ人もいるかと思います。ちゃんとHyper-Vの仮想マシンの一員として動作しています。Docker Engineの仮想マシンには「MobyLinuxVM」という名前が付けられており、シャットダウンも普通に可能です。なので一旦シャットダウンし、CPUやメモリ構成を変更することが可能です(どの程度保証されているのかはわかりません)。私は再起動時に前回の状態に復元(起動していれば起動する)するようにしました。

なお、仮想マシンのコンソールには「何も」表示されません。文字通り何も表示されず、Hyper-Vのロゴが表示されたままになっています。これはこの仮想マシンがDockerで必要な最小限のリソースしか使わないように最適化されているからと思われます。なので、基本的に仮想マシンのコンソールを操作することはありません。

dockerbeta7その代わり、タスクトレイのアイコンを右クリックするとコンテキストメニューが表示され、ここで幾つかの操作を行うことが出来ます(但し、興味深い何かはありませんでした)。

  • Docker Engineが起動したかどうかの基本的な確認は「Logs…」で確認できます。
  • 「Dashboard…」は、もっかのところ「Kitematicダウンロードしてね」と表示されるだけです。Kitematicはこの表示で初めて知りました。細かいところはググってみて下さい。使いやすいGUIインターフェイスです。

Hello Docker on Windows/Hyper-V

dockerbeta9ということで、これでもう動いています。また、Docker Client CLIもReadyなので、すぐに使い始めることが出来ます。とりあえず、Docker初めての方向けの手引きです。普通にコマンドラインを開きます。CLIはパスが通っているので、cmd.exe直実行でもOKです。

これですよこれ、この手軽さが欲しかった!

では、DockerでUbuntuコンテナを実行し、bashを操作してみます。

C:\> docker run -i -t ubuntu /bin/bash

これは、DockerHubからubuntuの最新Dockerイメージをダウンロードしてきて、Docker Engine内(つまりHyper-V)で実行して、/bin/bashを実行します。「-i -t」はbashの標準入出力をWindowsのコマンドプロンプトに結合して、直接操作可能にするという意味です。

初回はイメージのダウンロードに時間がかかるかもしれません。成功するとこのようにbashのプロンプトが(コマンドプロンプトに)表示され、普通に操作できます。Welcome Docker!!

dockerbeta10ここでexitでシェルを抜けるとコマンドプロンプトに戻りますが、これでDocker Engine内のubuntuインスタンスは綺麗さっぱり消えています(ダウンロードしたイメージだけキャッシュされている)。もう一度起動すると、今度はほぼ一瞬で起動するはずです。

これがDockerの利点で、後片付けが不要なので、ちょっと何か試すとかの用途には非常に便利です。もちろん、本来のDockerの強みである、DockerFileを定義したコンテナの作成と利用も当然可能です。「ディスポーザブルインフラストラクチャー」として、気軽に使ってみてください。

continuatioN Linking – NL名古屋

「continuatioN Linking」という題目で、NL名古屋で登壇してきました。

NとLの字さえ使っていれば何でも良いというカオスなショートセッション会なのですが、内容は非常に濃くて面白いものばかりでした。とても満足度が高かったです。Togetterのまとめはこちら

今回写真撮り忘れてしまったのでありませんが、50名超えと大入りな感じでした。


継続ネタ

継続渡しスタイル(Continuation Passing Style)の話に絡めて、.NET TaskとF# Asyncのシームレスな相互運用を行うネタを発表してきました。

何しろここは名古屋 (;´Д`) なので、このネタで行くにはまだ「ひよっこ」で恐怖しかない感じでしたが、掴みもスベる事なく発表できたのでうれしかったです。一部の方には触れるものがあったようで、「おおーー!!」という声はやって良かった感ありました。

発表はショートセッションですが参加者が多いこともあり、最終的に時間制約が10分となって苦しいところでしたが、予定通り進行を無視して「継続」させていただきましたありがとうございます。


補足

さて、内容は駆け足だったので少し補足しておきます。

FSharp.Control.FusionTasks.128GitHub: kekyo/FSharp.Control.FusionTasks
NuGet (F# 2.0): FSharp.Control.FusionTasks.FS20
NuGet (F# 3.0): FSharp.Control.FusionTasks.FS30
NuGet (F# 3.1): FSharp.Control.FusionTasks.FS31
NuGet (F# 4.0): FSharp.Control.FusionTasks.FS40

検討

アイデアはすぐに思いついたものの、実現可能かどうかが良くわかりませんでした。そもそも素人の浅はかで、.NET TaskクラスとF# Asyncクラスは似ているな、もしかしたら簡単に相互運用できるかも?と思ったのがきっかけです。

  • わざわざ似ているものが併存している事に何か特別な理由があるのかと思っていましたが、実はF# AsyncのほうがTaskよりも歴史が古く、CLR 4.0の形も無いころから既に非同期ワークフローがサポートされていました(.NET 2.0から実現している)。だからこの疑問は不適切で、.NET TaskはF#に依存しないように改めて設計しなおしたのだと思われます。
  • F#非同期ワークフローは、AsyncBuilderクラスを使用する、一種の構文糖(computation式)だと理解しています。F#の場合、任意の型に(インスタンスであろうとスタティックであろうと)拡張メンバー(C#で言う所の拡張メソッド)を生やす事ができるため、AsyncBuilderに.NET Taskを扱うメンバーを増やすことで、非同期ワークフローでシームレスに.NET Taskを扱えるのではないかと考えました。
  • 一番大きな動機は、HttpClientなどのネイティブ非同期対応ライブラリを、そのままF# Asyncで使うことが面倒であったということがあります。F#にも標準で.NET Taskのためのサポートはあります。「Async.AwaitTask関数」がそれですが、この関数は見ての通りT型が特定されなければならず、非ジェネリックTaskをAsync<unit>に変換できません。また、逆の変換(AsyncからTask)への変換方法もありません。逆の変換ができると、F#で書いた非同期ワークフローのインスタンスを、C#側でawait出来るようになるため、さらに応用性が広がります。

※ computation式については、「詳説コンピュテーション式」が詳しいです。

非同期コンテキストの結合

.NET非同期処理(async-await)と例外の制御 で持ち出した「タスクコンテキスト」という用語なのですが、もはや統合されたこの世界では「非同期コンテキスト」としか言いようがないですね。

「非同期コンテキスト」をシームレスに結合した場合、維持されなければならない重要なポイントがあります。それは、「同期コンテキスト(SynchronizationContext)」「キャンセルトークン(CancellationToken)」です。うーん、コンテキスト紛らわしい (*´Д`)

.NET Task側の事はある程度わかっているのですが、F#非同期ワークフローではこれがどのように維持されるのかがわからず、シームレスな相互運用が実現するにはココの担保が不可欠だと考えていました。

fsugjpの方々とやりとりしたり(あまりまとまりは無いです)、MSDNを調べたりして、以下の事がわかりました (Thx fsugjp!!)

  • F#にはAsync.FromContinuations<T>関数があり、これを使うとコールバック関数を経由してAsyncクラスの結果を制御できます。これは丁度TaskCompletionSource<T>クラスを使ってTaskを間接的に操作することに相当するので、Taskの結果を反映させることに使えます。
    余談ですが、初見ではあの引き数で何ができるのか、さっぱりわかりませんでした(つまりビギナー認定w)
  • 「Continuations」という単語の響きに導かれ、逆のパターンにはAsync.StartWithContinuations<T>関数を使えばよいことがわかりました。こっちはもっと単純で、正常・異常・キャンセルの継続処理をCPS形式で渡すだけです。

同期コンテキスト自体は、Asyncクラス内で非同期待期に使用しているので、F#非同期ワークフローで使う限り特に問題なさそうです。Task.ConfigureAwaitメソッドに相当する同期コンテキストのキャプチャ制御についてはむしろF#の方が柔軟性があり、Async.SwitchToContextAsync.SwitchToThreadPoolAsync.SwitchToNewThread関数を使用して、いかようにでも同期コンテキストを操ることができます。

そしてキャンセルトークンの方ですが、F#側はAsyncクラスのインスタンスを何らかの方法で実行するときにトークンを保持し、それがAsyncクラスの非同期コンテキストの情報として保持されて使われます。簡単に言うと:

// 非同期ワークフロー内で非同期スリープ(Task.Delayと概念は同じ)
let asyncBody = async {
  // (トークンはこの非同期ワークフローのコンテキストに暗黙に伝搬している)
  do! Async.Sleep(10000)
}

// キャンセルトークンを準備
let cts = new CancellationTokenSource()

// 非同期ワークフローをキャンセルトークンありで同期実行する
Async.RunSynchronously(asyncBody, Timeout.Infinite, cts.Token)

※ 注意: RunSynchronouslyは説明のために使用しています。本当は使わないように書くべきです。Task.Waitに相当します。

上記のような状態でSleepしているときにトークンがシグナル状態となると、正しくSleepが中断されます。Task.Delayの場合はキャンセルトークンを受け取るオーバーロードを使用していないと中断できませんが、F#非同期ワークフローの場合は、現在の非同期コンテキストに伝搬するトークンが管理され、Async.Sleepはそれを監視しているので正しく中断されるのです。

伝搬の手法は魔術的な何かでもなんでもなく、RunSynchronouslyで渡されたトークンがAsync.CancellationToken プロパティで管理されていて、Async.Sleepはこれを参照しているからです。実際、Task.Delayと異なり、Async.Sleepにはトークンを明示的に指定するオーバーロードはありません。

作ってみた感想

この部分、私的にはとても良くできていると思いました。というのも、F#非同期ワークフローの「非同期処理」に関する範囲と制約は、すべてワークフロー(computation式)内に閉じているからです。キャンセルトークンの管理もここで閉じているので、操作する側はAsyncクラスのインスタンス=非同期コンテキストと言うように、はっきりとイメージ出来ます。

対照的に.NET TaskとC# async-awaitではこれが曖昧で、ビギナーにasync-awaitの事を説明するためには、どこからが非同期コンテキストなのかをしつこいほどに解説する必要がありました。また、デフォルトではキャンセルに対する操作は完全に使用者任せとなり、Task.Delayのようにトークンを引き渡すシグネチャを明示的に設計に盛り込む必要があります。私はこのことに気が付くのが遅かったために、フレームワークインターフェイスの変更という大手術を行う羽目になったことがあります。そして、たとえ周到に準備しても、使用者がトークンを渡すのを忘れると台無しとなり、トークンをどこで管理するのかということも(使用者が)考える必要があります。

物事にはトレードオフがあるはずで、.NET Taskとasync-awaitが後発でありながらこういう選択をした理由も恐らくあるのでしょう。想像出来ることと言えば、async-await方式は、メソッド内のコードに「await」という単語が入るだけで、それ以上の大幅な変化がないと言うことです。しかし、実用的な非同期コードをasync-awaitで書いたことがある方ならわかると思いますが、現実には非同期であることを意識したコードを書かないと、pitfallに落とされます(一例を例外の記事でも書きました)。

F#非同期ワークフローでは、「async」に囲まれたcomputation式内でしか使えず、computation式では「let!」「do!」などの普通には使わない予約語を使う必要があります。ただ、上で述べたような問題とのトレードオフとしては悪くない選択だなと思います。説明も簡単なのが大きいですね。

そして、一番最初の「もしかしたら簡単に相互運用できるかも?」というのは、「それなりにイケた」という感じですが、やってみた後の感想としては、.NET TaskとF# Asyncクラスはそれぞれ役割もインターフェイスも似ているにも関わらず、現実には設計思想からして全く違うものだという印象が強くなりました。

後は、現在進行中のもう一つのプロジェクトと合わせて、何とかF#のコードが書けるようになったと言うことかな。人に説明するにはまだいろいろ足りてない感じですが、F#や関数型言語の面白さの一端は見えてきました。

それではまた。

PCいぢりメモ

訳あってPCの構成を変更する必要が生じたので、メモ。

CPUが一世代前なので、今更これを一から作るという人はいないと思うのですが、多分、この構成でやってる人はあまりいないと思うので、役に立つかも。気になるであろう部分を写真多めに盛っておきます。

構成

※Amazonでリンク張ってるので気になる方は直接ググって下さい

この構成の難しいところは、Mini-ITXなのでケース容積が非常に小さいところにハイエンドパーツを埋めているところです。ちっちゃいPC好きなので、どうしてもこういう無理ゲーな構成をやってしまう…

事の発端

朝電源を入れたらコレ。まぁ、SSDは壊れていなさそうと言うことと、前日Windows Updateで新たなIPが降ってきてた事もあり、それが直接の原因かも。ただ、折角(?)ブートしなくなってしまったので、とある問題の解決も含めて構成変更しようかと思いました。

というのも、SSD 2発をIntel RAID 0で構成してあったんですが、どうも感触が良くない。このSSD、体感かなり良いはずなのに、RAID 0構成してからあまり「速い」という印象もなく、なんだか勿体ないなと思ってました。それならRAID 0をやめて別のマシンにSSDを1台持って行ったほうが良いじゃないかと。

とはいえ、480GBだけでは心もとないので何か追加したい。それなら噂のNVMeに行くかと。しかしNVMeするならマザーボードも変えたほうが良く、この時点できちんとケースに収まるかかなり不安に…

最大の問題はクーラーで、現状のSAMUEL 17が新しいマザーに付くかどうか。念のため Cooler Master 風神スリム も手配。これは同じマザーのレビューで使われていたのが理由ですが、結局ダメでした(後述)。

マザーボード構築 (1)

WP_20160408_15_18_37_Proマザーボードだけさっくり置き換えるという訳にはいかず、一旦すべてのパーツをケースから外し、ケーブリングからやり直す事に。Mini-ITXの難しさの一つでもあるかな。で、取り出したマザーからCPUとメモリを外し、新しいマザーに移植したところ。

WP_20160408_15_19_01_Pro写っているヒートシンクはSAMUEL 17で、この時点で風神スリムは付かないことが判明。理由はメモリのヒートシンク高が高いこと。このマザーの特徴的なVRMの高さはぎりぎり収まるものの、メモリは完全にアウトでした。これは高さの様子です。

WP_20160408_15_19_21_Pro

WP_20160408_15_20_09_Proこういうことが買う前にわかる、良い施策無いかなーと思います。今はPCパーツショップがレポートとか書いてくれることが下支えになってると思うのですが、エッジの効いた構成だと事例がない…

WP_20160408_15_20_39_Proで、SAMUEL 17はちょっと変わったフットプリントなんですが、このヒートシンクは未来を予見しすぎているのか、買ってからずっと乗り換えで使えてるんですよね。今回もまさかこんなにフィットするとは思っていなかった。

WP_20160408_15_20_51_Proこれが….

WP_20160408_15_21_46_Proこう付きます。

WP_20160408_15_21_54_Pro

WP_20160408_15_22_30_Pro

WP_20160408_15_22_44_Pro

WP_20160408_15_23_01_Proもう、ホントギッチリで、SAMUEL天才か

WP_20160408_15_25_47_Pro大抵ここで心配になるのはEPS12Vコネクタの取り回しで、ほとんどのマザーはヒートシンク下にあるので先にコネクタを接続しておかなければならず、組み立てに苦労します。しかしこのマザーはそれを予見するかのように、EPS12Vを前方外周に配置していました。これ、かなりやりやすくて良いです。ASUSは昔トラブって以来避けてきたのですが、見直しました。

マザーボード構築 (2)

WP_20160408_15_29_18_Pro個人的にライザーカードの類は好きではない(どうしてもコネクターのような信頼性を損なう部品が必要になる)のですが、このマザーはライザーが3つもある。一つはVRM、そしてこのサウンドカード、最後にPCIe/Wifiカード。サウンドについては外付けのDACがあるので不要なのですが、これをつけておかないとバックパネルにみっともない穴が開いたままになるので、一応付けることに。

WP_20160408_15_30_55_Pro付けるとこんな感じ。

WP_20160408_15_34_47_ProWifiも不要なのですが、このライザーカードには、PCIeが刺さります。そして今回の構成変更の目的である、NVMeがここに刺さるので、やはりこれも使います。ただ、Wifiモジュールは外せそうだったので取り外しました。幸いWifiアンテナの穴(バックパネル)は出荷時で塞がっているので、サウンドカードのような問題はありませんでした。

WP_20160408_15_44_26_Pro_fこれが….

WP_20160408_15_45_03_Pro_fこう。

WP_20160408_15_54_50_Pro_fWifiはMini PCIeなので、外してオミットします。

WP_20160408_16_08_38_ProこのNVMe SSDは非常に高温になる事が知られているので、念のためヒートシンクを貼っておきました。サウンドカードとのクリアランスがちょっと不安になる感じです。間に何か挟んだほうが良いかもしれません。

WP_20160408_16_08_50_Pro

WP_20160408_16_09_06_Pro

組み込み

WP_20160408_16_14_08_Proさて、いざ組み込もうとしたら、なぜかきっちり入らない。よくよく見てみると、なんとVRMのライザーカードが共締め仕様となっており、ネジで止まってる…

WP_20160408_16_18_49_Proこのネジを外したところ、さっくりとハマりました。しかし、あらかじめ取り付けたファンが邪魔で一旦外す羽目に… 外してしまうとコネクタを再挿入するのが大変だ。

WP_20160408_16_30_37_Proケーブルを接続。良いですね、このきっちりかっちり感 :)

WP_20160408_16_33_16_Proファンのコネクタをどうにか接続し、取り付け完了(実はファンが逆であることに後で気が付く)。

WP_20160408_16_39_18_ProPCIeライザーカードとRadeonを取り付け、上部ファンも取り付ける。

上部ファンは12cmの穴が開いていますが、92mmのファンを無理やり付けています。理由はSFX電源の奥行きがやや長尺で、12cmだと干渉してしまうからです。この電源は650Wなんですが、これ以外に選択肢はなく、以前使っていた450WではRadeonを安定運用できなかったので、やむなくこういうことになっています。Mini-ITXは難しい…

WP_20160408_16_45_51_ProSSDとHDDを取り付けて…

WP_20160408_16_48_14_Pro

WP_20160408_16_49_26_Proどうにか、収まりました。

WP_20160408_16_50_44_ProSATAケーブルはスマートケーブルに変えないとダメですね。そのうちやります。

Overall

WP_20160409_11_22_48_Proファンの再換装は省略。

cdi折角なのでCristalDiskMarkで測ってみました。

cdm測定中、51℃まで上昇… 雫ちゃんに大音響でお願いされたのでびびった (*´Д`)

温度は使わなければすぐに冷えたので、ベンチマークをやり続けるとか極端な事をしなければ問題ないように感じました。

ここからVSやらOfficeやら再インストール。まだ先が長い…
ちなみにVectorは作業用として、HDDはデイリーバックアップ用として使います。

それでは。

OzCodeでかゆいところに手が届くデバッグを

wizardVisual Studioの拡張機能としてリリースされているOzCodeを紹介します。

記事の発端はtwitterで私がぽろっとRTした内容に反応があったことで:

まさか反応があるとは思ってなかったので、秘孔を突かれた感 (;´Д`) 国内ではR#の事はよく聞くんですが、OzCodeはまだマイナーかもしれないと思い、機能を紹介してみます。

なお、有償ですがTrialできます。また、Personalで全機能使えるので個人ならPersonalでも十分、サポートが必要ならCorporateかSite licenseを選択すればよいでしょう。Personalで$79なので、効果を考えるとお安いですよ!

# 誰かさんが呟いてましたが、Academicなら$25ですよ


デバッガの強化

OzCodeはVisual Studioのデバッガ機能を強化する拡張機能です。特徴的でわかりやすく機能を絞っているので、R#とはまた違う位置づけで良い製品だと思います。

Head-up displayとMagic Glance

Head-up displayは、メソッドの引数や式の評価内容を、いちいちWatchしなくてもインプレースで可視化します。

CodeLensと同じでエディタ上に場所をとるので、最初は慣れが必要かもしれません(しばらくしたら慣れました)。個人的にはプレビューウインドウがモードレスであることと、検索ウインドウから部分的なシンボル名で串刺し検索が出来るのがとても良い感じです。

もうね、このビデオの通りだと思うのですよ…

magicGlanceそして、Magic Glanceは、式のどの部分が評価されて無視されるのかを、色分けで示してくれます。

そもそも副作用のある式を書いたりしないように注意して設計していれば、この事が問題になることはないと思いますが、あまり良い出来ではないサードパーティ製ライブラリなどを使用する場合は、うっかりこのような罠に掛かっている事が見ただけで判ったりするので便利です。

プレビューの強化 (Reveal)

いつも確認するプロパティとか、ファボっておけます。ファボるとプレビューの上位に出てくるので、いちいちスクロールする必要がないのです。

オブジェクトの比較

比較が成立するには条件がありそうです(確認していませんが、パブリックフィールド・メンバじゃないとダメかな)が、あらかじめそういう設計にしておけば、無双感あります。

Exception Trail

例外ビュアーの強化・構造化された例外(InnerException)をマルチペーンビューで簡単に追跡できます。

InnerExceptionを横に並べてくれるので、映像にあるようにいちいちツリーを展開するメンドクササが軽減されます。また、まだあまり例外クラスについての知識がない場合は、その場でGoogleなどから検索できるのもイイんじゃないでしょうか?

まとめ

ほかにも小粒で便利な機能が: この辺を見てもらったほうが早いかも。

まだ日本語化されていませんが、日本語である必要性はほとんど無い(「魅せる」タイプのアプリですね)ので問題ないと感じました。敢えてネタを挙げるとすると:

  • LINQ (IEnumerable<T>)の演算子間を流れるTの観測をしたい。かなり無茶を言ってる事は分かってるんですが、いちいちToListしたりとか、もし回避出来るならかなりいい感じかなと思いました。
  • C#のみサポート。個人的にはぜひF#もサポートしてほしい… F#にはFSharp.Compiler.Serviceがありますよー? :)

今後の機能拡張が楽しみです。

なお、彼らは日本市場にも興味を持っているようですよー アンケート投下しておいたので話題に上れば何かあるかも #あるとは言ってない


ほかのVisual Studio拡張機能については、以下の記事もどうぞ:
Visual Studio 2012/2013 の拡張機能を紹介 (1)
Visual Studio 2012/2013 の拡張機能を紹介 (2)