できる!C#で非同期処理(Taskとasync-await)

ヤヴァいタイトル付けてしまった…. ええと、これはAdvent Calendarではありませんが、勢いで書いています(C# ADは既に埋まっていた…)

それには、こんな事情があったのです:

  • 「.NET非同期処理(async-await)を制御する、様々な方法」の記事がコンスタントにPVを稼いでいる
    (その割に、他の非同期関連の記事は読まれない)
  • asyncやawaitキーワードの使い方を度々聞かれる。
  • Task.Run()とか、Task.Factory.StartNew()とか、Task.Start()とか使ってるコードを頻繁に見る(不要なのに)。
  • Task.Wait()とか、Task.Resultとか使ってるコードを頻繁に見る(不要なのに)。
  • いまだにThreadクラス直接使ってるしかもその中でTask.Wait()とか(?!?!)

私のセッションや記事は、「なぜなのか」を深掘りして問題の根本を理解してもらう事を念頭に置いているのですが、さすがにこれだけ頻出してるのは、業界的に非常に問題があるんじゃないだろうかと思いました。特にXamarinや.NET Coreが注目されると、今までC#でコード書いた事が無い、と言う人が、見よう見まねで書き始める時期じゃないかと思います(それで何となく書けてしまうのが、C#の間口の広いところではあるのですが)。

もう一つ、現在、「非同期処理の必要性」のような記事を書いているのですが、まだ完成していないので、もうちょっとライトなやつを書いてさっさと公開した方が良い気がしてきました。

「できる(なんちゃら)」じゃないけど、そういう資料も切り口を丁寧に詰めればありかも知れない…

そういうわけで:

  • C#の非同期処理(Taskとasync-await)を使ってコードを書くなら、以下の事を知っていれば9割がたの問題は回避できる!!
  • ここで示した方法から外れたことをしようとしている場合は、殆ど間違いなく誤った記述をしようとしているので、もう一度何をしようとしているのか整理しよう!!
    (知っててやってる人はいいんです。こんな記事見る必要はありません)

章立てを2章だけにして、極限まで絞り込みました。ではどうぞ。

# 補足: F#erの人には説明は不要と理解しております :)
# コードはやや冗長に書いています。


1. 同期処理と非同期処理を対比させてみる

以下のコードは、ウェブサイトから同期的にデータをダウンロードします。WebClientクラスを使い、OpenReadメソッドでStreamを取得して、StreamReaderで文字列として読み取ります(クラス定義などは省略):

using System.IO;
using System.Net;

public static string ReadFromUrl(Uri url)
{
	using (WebClient webClient = new WebClient())
	{
		using (Stream stream = webClient.OpenRead(url))
		{
			TextReader tr = new StreamReader(stream, Encoding.UTF8, true);
			string body = tr.ReadToEnd();
			return body;
		}
	}
}

public static void Download()
{
	Uri url = new Uri("https://github.com/Microsoft/dotnet/blob/master/README.md");
	string body = ReadFromUrl(url);
	Console.WriteLine(body);
}

これを非同期処理として変形します。変形した箇所にはコメントを入れておきました。番号の順にやればスムーズに行くと思います:

using System.IO;
using System.Net;
using System.Threading.Tasks;

// Step5: メソッド内でawaitキーワードを使ったら、メソッドの先頭でasyncを加えなければならない。
// Step6: メソッド内でawaitキーワードを使ったら、戻り値の型はTask<T>型で返さなければならない。
//        T型は本来返すべき戻り値の型。
// Step7: 非同期処理対応メソッドは、「慣例」で「~Async」と命名する(慣例なので必須ではない)。
public static async Task<string> ReadFromUrlAsync(Uri url)
{
	using (WebClient webClient = new WebClient())
	{
		// Step1: OpenReadから非同期対応のOpenReadTaskAsyncに変更する。
		// Step2: OpenReadTaskAsyncがTask<Stream>を返すので、awaitする。
		//        awaitすると、Streamが得られる。
		using (Stream stream = await webClient.OpenReadTaskAsync(url))
		{
			TextReader tr = new StreamReader(stream, Encoding.UTF8, true);
			// Step3: ReadToEndから非同期対応のReadToEndAsyncに変更する。
			// Step4: ReadToEndAsyncがTask<string>を返すので、awaitする。
			//        awaitすると、stringが得られる。
			string body = await tr.ReadToEndAsync();
			return body;
		}
	}
}

// Step10: メソッド内でawaitキーワードを使ったら、メソッドの先頭でasyncを加えなければならない。
// Step11: メソッド内でawaitキーワードを使ったら、戻り値の型はTask型で返さなければならない。
//         ここでは元々返す値が無かった(void)なので、非ジェネリックなTask型を返す。
// Step12: 非同期処理対応メソッドは、「慣例」で「~Async」と命名する(慣例なので必須ではない)。
public static async Task DownloadAsync()
{
	Uri url = new Uri("https://github.com/Microsoft/dotnet/blob/master/README.md");
	// Step8: ReadFromUrlから非同期対応のReadFromUrlAsyncに変更する。
	// Step9: ReadFromUrlAsyncがTask<string>を返すので、awaitする。
	//        awaitすると、stringが得られる。
	string body = await ReadFromUrlAsync(url);
	Console.WriteLine(body);
}

# 補足: HttpClientではなくWebClientを使ったのは、対比させやすいためです。非同期前提で書くなら、始めからHttpClientを使うと良いでしょう。

asyncpractice1出来るだけわかりやすくなるようにコメントを入れたので、「ひたすらめんどくさそう」に見えますが、よく見れば大した作業ではない筈です。ReadFromUrlとDownloadのそれぞれのメソッドが、どのように変形されるのかを見てください。そうすると何となく法則が見えてくると思います。

また、この作業で、2つのメソッド「ReadFromUrl」と「Download」が、両方とも非同期メソッド化された事にも注意してください。

asyncpractice2つまり、特別な理由がない限り、非同期処理は呼び出し元から末端まで、「全て非同期メソッドで実装する必要がある」、と言う事です。ここで示した変形を行うと、連鎖的に、すべてのメソッドが非同期化されます。

さあ、核心です。以下にまとめます:

  • 非同期対応メソッドが存在する場合は、そのメソッドを呼び出すように切り替えます。上記では、「OpenRead」「OpenReadTaskAsync」に変えました。目的の非同期メソッドは、「TaskやTask<T>を返すメソッドがあるかどうか」や「~Async」と命名されているメソッドがあるかどうか、を目印に探すと良いと思います。また、この理由のために、メソッド名を「~Async」と命名する慣例が生きてきます。
  • TaskやTask<T>は、そのままでは戻り値T型になりません。これらの値は「await」キーワードを使う事で、戻り値を得る事が出来ます。awaitを実行している時、つまりそれが「非同期的に結果を待機している」と言う事です。
  • 自分で定義したメソッド内で「await」キーワードを使った場合は、メソッド先頭に「async」キーワードを追加する必要があります。「async」キーワードの効能はそれだけです!!! 以下のTask型云々は関係ありません。
  • 非同期メソッドの戻り値は、Task型、Task<T>型、または「void」とする必要がありますが、voidは殆ど使用しません(次章)。
  • 同期メソッドの戻り値の型がstringなら、Task<string>のように、ジェネリックなTask<T>型を使います。同期メソッドの戻り値がない(void)場合は、Task型(非ジェネリック)を返します。

これだけです。これだけですよ奥さん!!! 英語の動詞変形みたいな話です。この法則だけ覚えていれば、一旦同期的にコードを書いてみて、それから非同期処理に置き換えるという「練習」が出来ます。しばらくやっていれば、最初から非同期で全部書けるようになります。

ここまで、Task.Run()も、Task.Factory.StartNew()も、Task.Start()も、Task.Wait()も、Task.Resultも出ませんでしたね? これらを使う事はまずありません(断言)。

それでも、という方、では第2章へどうぞ。


2. 非同期操作のルーツ

非同期操作が考案された由来、とかそういう話ではなく、「どこからが非同期処理として扱われるか」あるいは「どこからを非同期処理として扱うのか」、という話です:

// これはWPFのウインドウ実装です(但し、WPFであることはあまり重要ではありません):
public partial class MainWindow : Window
{
    // コンストラクタ
    public MainWindow()
    {
        InitializeComponent();

        // ウインドウ表示開始時のイベントをフックする
        this.Loaded += MainWindow_Loaded; 
    }

    // ウインドウが表示されると呼び出される
    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        // 第1章で実装したReadFromUrlを呼び出して、テキストボックスに表示する
        Uri url = new Uri("https://github.com/Microsoft/dotnet/blob/master/README.md");
        string body = ReadFromUrl(url);
        textBox.Text = body;
    }
}

XAMLは省略します。TextBoxコントロールに”textBox”という名前を付けて定義してある前提です(また、本当はデータバインディングでやるべきです)。

さて、これを非同期処理に対応させます:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.Loaded += MainWindow_Loaded; 
    }

    // Step3: メソッド内でawaitキーワードを使ったら、メソッドの先頭でasyncを加えなければならない。
    // Step4: メソッド内でawaitキーワードを使ったら、戻り値の型はTask型で返さなければならない。
    //        しかし、Loadedイベントのシグネチャは変更できない。従って「void」のままとする。
    // Step5: 非同期処理対応メソッドは、「慣例」で「~Async」と命名する。
    //        しかし、値を返さないためawait忘れの可能性はない事と、あくまで慣例であるため
    //        ここでは「~Async」と命名しない。
    private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        Uri url = new Uri("https://github.com/Microsoft/dotnet/blob/master/README.md");
        // Step1: ReadFromUrlから非同期対応のReadFromUrlAsyncに変更する。
        // Step2: ReadFromUrlAsyncがTask<string>を返すので、awaitする。
        //        awaitすると、stringが得られる。
        string body = await ReadFromUrlAsync(url);
        textBox.Text = body;
    }
}

asyncpractice31MainWindow_Loadedメソッドと、第1章のDownloadAsyncメソッドとの違いを確かめてください。Loadedイベントのシグネチャは「RoutedEventHandler」デリゲートであり、戻り値を返さない「void」メソッドであることが決められています。非同期メソッドはTask型かTask<T>型を返すべきですが、ここでは型が合いません。値を返さない場合に限り、このような非同期メソッドを「void」で定義します。

ポイント:

  • 戻り値を返さず「void」と定義するメソッドは、イベントをフックしたハンドラメソッドのような場合しかありません。戻り値をvoidとした場合、呼び出し元は非同期処理の結果を追跡できず、処理結果を感知しません。
  • 逆に考えると、Loadedイベントの場合、そもそもイベントの呼び出し元は非同期処理の結果など気にしていません(voidなので)。だから、voidとしても良い、と解釈することもできます。
  • 「Task」を返そうが「void」を返そうが、「await」キーワードを使ったので「async」キーワードを追加する必要があります。

(何らかの)処理の開始位置が、このハンドラメソッドにある事にも注目してください。このように、呼び出し元(WPFのLoadedイベント処理)が、呼び出し先(MainWindow_Loadedメソッド)の事について感知していないような場合にのみ、呼び出し先の非同期メソッドの実装方法を考える必要があります。

コンソールアプリケーションの非同期処理化ではどうでしょうか:

// コンソールアプリケーションのメインエントリポイント
// awaitを使っているのでasyncを追加する(?)
public static async void Main(string[] args)
{
    // Task型が返されるので、この処理を待機するためawaitする(?)
    await DownloadAsync();
}

asyncpractice4第1章のDownloadAsyncメソッドを呼び出してみました。もし、この呼び出しをawaitで待機する場合、Mainメソッドの先頭にasyncと付ける必要があります。このコードは問題なくコンパイルできますが、実行すると(殆どの場合)ダウンロードが完了することなくプログラムが終了します。

Mainメソッドは、DownloadAsyncメソッドの非同期処理をawaitで待機します。しかしこれは「非同期的に待機」しているので、「Mainメソッドの処理(メインスレッド)は継続的に実行されている」のです。すると、メインスレッドはそのままMainメソッドを抜けてしまい、一瞬でプログラムが終了してしまいます。

したがって:

public static void Main(string[] args)
{
    // Step1: Task.Wait()を使わざるを得ない。
    Task task = DownloadAsync();
    task.Wait();
}

asyncpractice5このような場合にのみ、Task.Wait()で待機する必要があります。GUIアプリケーション(WPF/UWP/Xamarinなど)を実装している皆さんには、まったく縁のない話なので、このことは忘れても構いません。GUIアプリケーションでTask.Wait()を使うとデッドロックを起こします。

大事な事なのでもう一度: 「デッドロックします。使ってはいけません」


まとめ

これだけです。とりあえずこれだけで、「マトモ」な非同期処理コードを書くことが出来るはずです。繰り返しますが:

  • C#の非同期処理(Taskとasync-await)を使ってコードを書くなら、これらの事を知っていれば9割がたの問題は回避できる!!
  • ここで示した方法から外れたことをしようとしている場合は、殆ど間違いなく誤った記述をしようとしているので、もう一度何をしようとしているのか整理しよう!!

これを読んで新たに疑問が湧いてくるかもしれません(例えば、どうしてデッドロックするのか、voidの非同期メソッドの結果はどうなるのか、など)。それは良い事です。そのような場合は、より深く解説した記事を読んでみてください。

それでは!

回転寿司をVisual Studioに降臨させる – Visual Studio Advent Calendar 2016

この記事は、Visual Studio Advent Calendar 2016の4日目の記事です。

sushi_rotation_on_mbp少し前に、新しいMacBook Proで回転寿司が話題になりましたね。こんな奴です:

centerclr-sushirotatorこれが転じて、最終的にVisual Studioの拡張機能として、こんなものを作りました:

Visual Studioの拡張機能… 私は度々dis… もとい、非常に難易度が高いという話をしているのですが、この拡張機能の実装はそれほど難しくありません。とは言え、罠が至る所に待ち構えています。今回は、拡張機能を作るうえで躓いてツマラナイ思いをしないように、導入部分のガイドを記録に残しておこうと思います。

なお、この拡張機能はGitHubで公開しています: CenterCLR.SushiRotator
すぐに試してみたい方は: Visual Studio Market place、または拡張機能から「sushi」で検索すれば出てきます。

# やじうまの杜でも取り上げられました: MacのTouch Barが回転寿司に! エディターでも続々カイテン


事前に考える必要のある事

Visual Studioの拡張機能は、俗に「VSIX」と呼ばれています。これは、拡張機能のパッケージに「CenterCLR.SushiRotator.vsix」のような拡張子が付いていることでもわかります。
ややこしいのは、拡張機能のすべてにvsix拡張子がついているわけではなく、一般的なセットアップインストーラーだったり、msi形式だったする事もあることです。

VSIX形式は、Visual Studio(のVSIXInstaller.exe)を使ってインストールします。また、VSIXでは、Visual Studio Market placeからの自動更新に対応していて、ユーザーレベルでのインストールにも対応しているため、ユーザーからすると扱いやすいものになっています。例えば、拡張機能がGACへのインストールを必要としているなどの複雑な処理を行う場合は、VSIX形式とすることはできないため、msiや独自のインストーラーを作る必要があります。

今回は、要するにエディタを拡張するだけであり、エディタの拡張ならVSIXだけで完結するため、標準的なVSIX形式で作ります。

また、対象のVisual Studioを何にするかも考える必要があります。Visual Studioの拡張機能は、以下のエディション(以上)でサポートされます:

  • Visual Studio 2005 Professional
  • Visual Studio 2008 Professional
  • Visual Studio 2010 Professional
  • Visual Studio 2012 Professional
  • Visual Studio 2013 Community
  • Visual Studio 2015 Community
  • Visual Studio 2017RC Community

より古いバージョンに対応させようとすると、それだけ拡張可能な機能が制限されます(機能拡張に対応するインターフェイスが定義されていないなど)。

これ以前のバージョン(2003など)でも、基本的なインターフェイスは実装されています(それどころか、古のVisual InterDevやJ++でも)。しかし、これらのバージョンでの拡張は、マイクロソフトと特別な契約を結んだデベロッパだけが開発することが出来たため、ほとんどの皆さんは対応させることが出来ません(まあ実際には書いたら動くのかもしれませんが。それから、既にサポート対象外なので、実質無視して良いと思います)。

また、以下の制限もあります:

  • 各バージョンのExpress Editionは、拡張機能に対応していません。
  • VSIXパッケージをサポートするのは、Visual Studio 2010以上です。2010未満は、カスタムインストーラーが必要です。当然、Market placeでの自動更新はできません。
  • 今回対象とする「テキストエディタの拡張」は、Visual Studio 2010以上でのみサポートされます。これは、2010以降はVisual StudioがWPFで書き直され、WPFコントロールを使う前提となっていることが遠因となっていると思われます。
  • Visual Studio 2012以降は、後述のvsixmanifestファイルの形式が変更されたため、2010をサポートする場合は古い形式で記述する必要があります。
  • それぞれの拡張機能を実装するには、対応するVisual Studio SDKが必要です。例えば、2010の場合は、VSSDK2010が必要となります。これらは下位互換性があるため、普通は目指すバージョン群の最も高いバージョンに対応するSDKをインストールすれば、問題ありません。
  • 使用する.NET Frameworkのバージョンにも注意して下さい。対応させる最も古いVisual Studioで開発可能な、最後のバージョンを選択します。(大丈夫だと思いますが、.NET Coreは使えません、念のため)。

vs2017rc_install_vsix上記の事を考え、今回はVisual Studio 2012 Professional以上、つい先日リリースされた2017RCまでを対象とします。SDKは2017RCのものが必要ですが、これは2017RCのインストーラーで選択すればインストール出来ます。.NET Frameworkは4.5を使用します。

ややこしいのは、拡張機能の開発自体は、対応させる最も新しいVisual Studioを使う必要がある、と言うことです。したがって、2017RCを使用します。


拡張機能のプロジェクトテンプレート

vsix1VSIXの拡張機能を作るには、プロジェクトの新規作成から「Extensibility」にある「VSIX Project」でプロジェクトを作成します:

vsix2次に、プロジェクトにエディタ拡張を追加します。これも、Extensibility配下から選択します。いくつか候補がありますが、ここでは「Editor Margin」を選択します。

vsix3このEditor Marginが、テキストエディタの一部の領域を占有する事が出来る拡張です。これを使って寿司を流します。

これらのテンプレートですが、以前は本当にくぁwせdrftgyふじこlp… いや、色々と問題があった… のですが、2017RCでは非常にすっきりしたテンプレートとなっていて、扱いやすく修正されました。基本となるVSIX Projectでプロジェクトを作っておいて、そこに必要なitemとして後から追加する感じです。但し、拡張機能で実現可能な機能に対して、圧倒的にテンプレートが足りてません。大半は一から手でクラスを書いたりする必要があります。

# 何故かテキストエディタに対する拡張のテンプレートだけが色々あります… もちろん、本当はもっと多彩な事が出来ます。


Editor Margin

Editor Marginは、テキストエディタの一部領域が、拡張機能のために予約され、そこに自由に何かを表示させることが出来るVSIXの拡張機能です。この一部領域とはつまり、WPFのCanvasであり、まあ、あとはWPFで好きなようにやってよ、という感じです。Editor Marginクラスを加えると、以下のようなテンプレートコードが生成されます:
(Disposeの面倒を見るコードもあるんですが、長いので省略)

namespace VSIXProject1
{
    /// <summary>
    /// Margin's canvas and visual definition including both size and content
    /// </summary>
    internal class EditorMargin1 : Canvas, IWpfTextViewMargin
    {
        /// <summary>
        /// Margin name.
        /// </summary>
        public const string MarginName = "EditorMargin1";

        /// <summary>
        /// Initializes a new instance of the <see cref="EditorMargin1"/> class for a given <paramref name="textView"/>.
        /// </summary>
        /// <param name="textView">The <see cref="IWpfTextView"/> to attach the margin to.</param>
        public EditorMargin1(IWpfTextView textView)
        {
            this.Height = 20; // Margin height sufficient to have the label
            this.ClipToBounds = true;
            this.Background = new SolidColorBrush(Colors.LightGreen);

            // Add a green colored label that says "Hello EditorMargin1"
            var label = new Label
            {
                Background = new SolidColorBrush(Colors.LightGreen),
                Content = "Hello EditorMargin1",
            };

            this.Children.Add(label);
        }

        #region IWpfTextViewMargin
        /// <summary>
        /// Gets the <see cref="Sytem.Windows.FrameworkElement"/> that implements the visual representation of the margin.
        /// </summary>
        /// <exception cref="ObjectDisposedException">The margin is disposed.</exception>
        public FrameworkElement VisualElement
        {
            // Since this margin implements Canvas, this is the object which renders
            // the margin.
            get
            {
                this.ThrowIfDisposed();
                return this;
            }
        }
        #endregion

        #region ITextViewMargin
        /// <summary>
        /// Gets the size of the margin.
        /// </summary>
        /// <remarks>
        /// For a horizontal margin this is the height of the margin,
        /// since the width will be determined by the <see cref="ITextView"/>.
        /// For a vertical margin this is the width of the margin,
        /// since the height will be determined by the <see cref="ITextView"/>.
        /// </remarks>
        /// <exception cref="ObjectDisposedException">The margin is disposed.</exception>
        public double MarginSize
        {
            get
            {
                this.ThrowIfDisposed();

                // Since this is a horizontal margin, its width will be bound to the width of the text view.
                // Therefore, its size is its height.
                return this.ActualHeight;
            }
        }

        /// <summary>
        /// Gets a value indicating whether the margin is enabled.
        /// </summary>
        /// <exception cref="ObjectDisposedException">The margin is disposed.</exception>
        public bool Enabled
        {
            get
            {
                this.ThrowIfDisposed();

                // The margin should always be enabled
                return true;
            }
        }

        /// <summary>
        /// Gets the <see cref="ITextViewMargin"/> with the given <paramref name="marginName"/> or null if no match is found
        /// </summary>
        /// <param name="marginName">The name of the <see cref="ITextViewMargin"/></param>
        /// <returns>The <see cref="ITextViewMargin"/> named <paramref name="marginName"/>, or null if no match is found.</returns>
        /// <remarks>
        /// A margin returns itself if it is passed its own name. If the name does not match and it is a container margin, it
        /// forwards the call to its children. Margin name comparisons are case-insensitive.
        /// </remarks>
        /// <exception cref="ArgumentNullException"><paramref name="marginName"/> is null.</exception>
        public ITextViewMargin GetTextViewMargin(string marginName)
        {
            return string.Equals(marginName, EditorMargin1.MarginName, StringComparison.OrdinalIgnoreCase) ? this : null;
        }
        #endregion
    }
}

コンストラクタで渡される引数IWpfTextViewと、IWpfTextViewMarginというインターフェイスの実装が、拡張機能をサポートする要となっています。また、このクラスはCanvasクラスを継承していますね。サンプルの実装は、このCanvasにLabelを手動生成して追加しています。やる気になれば普通にXAMLで書いても動きそうな気配です(試してません)。

コンストラクタの引数は依存注入されるのでしょう(Visual StudioはMEFを使用しているので、MEF経由で渡されるようです)。(間違えました、以下で示すファクトリクラスから生成しています)注意すべき点としては、このクラスにはstaticフィールドを含めることが出来ない、と言う事です。含めた場合、拡張機能のロード時に失敗します(結構悩んだ。Visual Studioのバグかもしれない)。staticフィールドが欲しい場合は、別のクラスに定義して、そこを参照するようにします(回転寿司でもやっています)。

サンプルのコードはコンストラクタ内で、Heightを20pxに指定しています。Canvasなので、このサイズが表示領域を規定することになります。また、WPFは標準でクリッピングを行いません。手動描画した場合に描画がはみ出ると、エディタ領域が酷いことになってしまうため、ClipToBoundsをtrueとしているようです。はみ出す可能性がなく、パフォーマンスに問題がある拡張機能を実装するのであれば、これをfalseとしても良いでしょう(どんな拡張機能なのかは良くわからない…)。

まんまWPFなので、もちろんアニメーションも使えます。回転寿司では、寿司を流すのにWPFのアニメーションを使っています。なので、非常に滑らかに流れることがわかると思います(上のGIFアニメーションはガタガタですが、ぜひ実際にインストールして見て下さい)。


Editor Margin Factory

Editor Marginクラスは、ファクトリクラスとの関連付けで実行されます。ファクトリクラスはEditor Marginテンプレートを追加した際に、自動的に追加されているはずです。以下に例を示します:

namespace VSIXProject1
{
    /// <summary>
    /// Export a <see cref="IWpfTextViewMarginProvider"/>, which returns an instance of the margin for the editor to use.
    /// </summary>
    [Export(typeof(IWpfTextViewMarginProvider))]
    [Name(EditorMargin1.MarginName)]
    [Order(After = PredefinedMarginNames.HorizontalScrollBar)]  // Ensure that the margin occurs below the horizontal scrollbar
    [MarginContainer(PredefinedMarginNames.Bottom)]             // Set the container to the bottom of the editor window
    [ContentType("text")]                                       // Show this margin for all text-based types
    [TextViewRole(PredefinedTextViewRoles.Interactive)]
    internal sealed class EditorMargin1Factory : IWpfTextViewMarginProvider
    {
        #region IWpfTextViewMarginProvider
        /// <summary>
        /// Creates an <see cref="IWpfTextViewMargin"/> for the given <see cref="IWpfTextViewHost"/>.
        /// </summary>
        /// <param name="wpfTextViewHost">The <see cref="IWpfTextViewHost"/> for which to create the <see cref="IWpfTextViewMargin"/>.</param>
        /// <param name="marginContainer">The margin that will contain the newly-created margin.</param>
        /// <returns>The <see cref="IWpfTextViewMargin"/>.
        /// The value may be null if this <see cref="IWpfTextViewMarginProvider"/> does not participate for this context.
        /// </returns>
        public IWpfTextViewMargin CreateMargin(IWpfTextViewHost wpfTextViewHost, IWpfTextViewMargin marginContainer)
        {
            return new EditorMargin1(wpfTextViewHost.TextView);
        }
        #endregion
    }
}

初見殺し的なひどいコードですが、このクラスの属性から、Visual StudioがEditor Marginのコードを認識して、実行可能となります。いじるのは、色々わかってからの方が良いでしょう。


vsixmanifestファイル

VSIX Projectプロジェクトを生成すると、*.vsixmanifestというファイルも追加されます。このファイルが、VSIXをパッケージングするのに使われる定義体で、中身はXMLです。NuGetで言う所のnupkgに対応するnuspecファイルです。

<?xml version="1.0" encoding="utf-8"?>
<PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
  <Metadata>
    <Identity Id="VSIXProject1.Kouji Matsui.86820e26-2ec6-496e-ad20-38a2ba88bf46" Version="1.0" Language="en-US" Publisher="Kouji Matsui" />
    <DisplayName>VSIXProject1</DisplayName>
    <Description>Empty VSIX Project.</Description>
  </Metadata>
  <Installation>
    <InstallationTarget Id="Microsoft.VisualStudio.Community" Version="[15.0]" />
  </Installation>
  <Dependencies>
    <Dependency Id="Microsoft.Framework.NDP" DisplayName="Microsoft .NET Framework" d:Source="Manual" Version="[4.5,)" />
  </Dependencies>
  <Prerequisites>
    <Prerequisite Id="Microsoft.VisualStudio.Component.CoreEditor" Version="[15.0,16.0)" DisplayName="Visual Studio core editor" />
  </Prerequisites>
  <Assets>
    <Asset Type="Microsoft.VisualStudio.MefComponent" d:Source="Project" d:ProjectName="%CurrentProject%" Path="|%CurrentProject%|" />
  </Assets>
</PackageManifest>

XMLはこのような形式です(上で述べたように、2010に対応させるには異なる形式で記述する必要があります)。但し、SDKがインストールされていれば、ソリューションエクスプローラーからダブルクリックで開くと、以下のようにビジュアル編集出来ます:

vsix4パッケージをMarket placeで公開する場合は、Identity・DisplayName・Descriptionなどをきちんと埋めておきましょう。他にもIcon・PreviewImage・License・Tagsなどが指定出来ます。

vsix5また、重要な項目として、Dependenciesの.NET Frameworkのバージョンと、Prerequisitesの「Visual Studio core editor」のバージョンを合わせておく必要があります。

vsix6Version Rangeの指定は、数学で使うような、以上・未満の記号による指定を行います。バージョン11(2012)以上~16(2018?)未満の場合、”[11.0, 16.0)”のように、以上を角括弧、未満を丸括弧で示します。このとき、”[11.0, 15.0]”としてはいけません。これでは、2017のマイナーバージョンアップに対応出来なくなってしまいます(15(2017)「以下」なので、マイナーバージョンが上がると外れてしまう)。

InstallationTargetは複数定義できます。最初に検討した、対応させるVisual Studioのバージョンとエディションの組み合わせが複雑になるので、以下のように定義するとよいと思います:

  <!-- これは回転寿司で使っている定義です。
       InstalledByMsiはmsiでインストールするかどうか、AllUsersは昇格が必要かどうかです。 -->
  <Installation InstalledByMsi="false" AllUsers="false">
    <!-- VS2012 -->
    <InstallationTarget Id="Microsoft.VisualStudio.Pro" Version="[11.0,12.0)" />
    <!-- VS2013- -->
    <InstallationTarget Id="Microsoft.VisualStudio.Community" Version="[12.0,16.0)" />
    <InstallationTarget Id="Microsoft.VisualStudio.Pro" Version="[12.0,16.0)" />
  </Installation>

いつも通りと言うか、一旦vsixmanifestを生成したら、後は手動でXMLを弄った方が早い感じがひしひしと…


VSIXをデバッグする

このプロジェクトをビルドすると、bin\Debug(またはbin\Release)に、*.vsixが生成されます。デバッグ実行すると、検証用のVisual Studioが別途起動し、そこにVSIXが自動登録されてデバッグ可能になります。以下はデバッグ実行した例です。右側が開発中のVisual Studio、左側がデバッグ実行で新たに起動した「実験的なインスタンス」と命名されているVisual Studioで、その上で寿司が流れています。

vsix7

Visual Studio 2015以降で特徴的な、WPFライブビュー用のツールバーも表示されていることがわかります。

ところで、デバッグしていると途中で動作がおかしくなったりすることがあります。特に、変更したコードが反映されていないなどです。これは、VSIXのインストールフォルダが何らかの問題で破損した場合に発生します。インストールフォルダはユーザー毎に作られます。場所は、「C:\Users\[ユーザー名]\AppData\Local\Microsoft\VisualStudio」のような場所です。この配下に、Visual Studioのバージョン番号毎にフォルダが作られ、その中にインストールされます。

デバッグ実行した場合も、このフォルダ配下にコピーされます。2017RCの場合、サブフォルダは「15.0_31113400Exp」です。最後の「Exp」が「実験的なインスタンス」に対応します。何か動きがおかしいと思ったら、このフォルダを丸ごと削除して再度ビルドしてみて下さい(Visual Studioは一旦終了させる必要があるかもしれません)。削除しても、次回起動時には再生成されます。なお、経験的には「わりとよく壊れます」。アッハイとかつぶやきながら、削除すると良いでしょう。

それから、最新のVisual Studio対応バージョンを上げる場合(例えば、2013対応のものを2017RCに対応させる)、デバッグ実行時に起動するdevenv.exeへのパスも更新することをお忘れなく。「実験的なインスタンス」へのデプロイは最新のVisual Studioに行われる(これはSDKの機能で行われる)のに、起動するVisual Studioが古いバージョンのままだと、VSIXがロードされないように見えて混乱します。この問題も忘れたころにハマるパターンです。

C#のプロジェクトは、デバッグ時の起動パスを*.csproj.userに保存します。通常、Gitでソースコード管理していると、このファイルは管理対象外とする(.gitignoreで除外)のですが、ここにパスが書かれているため、git cloneしてデバッグしようとして失敗するのも良くハマります。*.csprojに手動で定義を移しておくとベターですが、そうすると今度は*.csproj.userでいつの間にかオーバーライドされていてハマるという、非常に苦しい展開が待っています…

# オーバーライドを無視するか警告する属性が欲しい…


VSIXのNuGetと低いバージョンに対応させる話

2017RCのVSIX Projectでプロジェクトを作ると、VSIXで必要なライブラリが、NuGet経由で参照されます。このパッケージは「Microsoft.VSSDK.BuildTools」「Microsoft.VisualStudio.CoreUtility」「Microsoft.VisualStudio.Text.UI」などで公開されています。これらのパッケージが公開されていることを知ったのは今回の回転寿司によるんですが、昔はNuGetパッケージ化されていなかったため、VSIXを含むプロジェクトのビルドは、非常に面倒な問題を含んでいました(当然、CIするにはVisual Studio本体が必要だったり)。

今はこのように、VSIXテンプレートとは別体となったおかげで、ずいぶんやりやすくなったと思います。NuGet経由での参照となったのは把握していませんが、ごく最近のバージョンからの筈です。

が、このパッケージの最低バージョンが2013以降のようで、2012やそれ以前のバージョンには対応していません。もし、古いバージョンも対応させる予定であるなら、別のパッケージ「VSSDK.CoreUtility」が公開されているので、これを使うと良いと思います(StyleCop Analyzersのauthorですね)。NuGetで「VSSDK」で検索すると、細分化されたパッケージのリストが表示されるので、必要なものを追加します。但し、VSIXのビルドシステムだけは最新のパッケージを使う必要があります(vsixmanifestの最新のスキーマを認識可能にするため)。つまり、「Microsoft.VSSDK.BuildTools」は、最初に挙げたもの(の最新版)を使うことをお勧めします。

昔のVSIXテンプレートから生成したプロジェクトを保守している皆様… ご愁傷さまです。手動でゴリゴリcsprojを書き換える未来が待っております… (泣


まとめ

全体的なコードは、回転寿司を参考にして見て下さい。VSIXのパッケージをローカルデバッグ出来ていよいよ公開できるようになったら、Visual Studio Market placeで公開できます。公開は無料で可能です。公開プロセスは変更中のようで、現在は Visual Studio Team Onlineで使う拡張機能と統合したいのか、変に中途半端になっている感じです。Market placeからのリンクで申請しようとすると VSTO 向けの公開として扱われて、認証プロセスがどうのとか面倒な事になります。

vsix8「Visual Studio Gallery」と呼ばれているこっちから行き、中ほどにある「アップロード」をクリックすると確実です。アップロードする前に、vsixmanifestの内容を見直してください。また、バージョンアップする場合は、vsixmanifestのバージョン番号を更新することをお忘れなく。

一応、アップロードした時点で、パッケージングが正しいかどうかの検証は行ってくれるようです。なお、更新されたパッケージをアップロードすると、使用中のユーザーにはおなじみのように「拡張機能の更新があります」と通知されます。

何か面白い拡張機能作って放流してください :)
それでは。

Beachhead implements new opcode on CLR JIT – .NET Fringe Japan 2016

10/1に、松竹スクエアのドワンゴセミナールームにて、「.NET Fringe Japan 2016」が開催され、そこで登壇してきました。

fringe2016

ニコニコ動画での配信も行われました。タイムシフト予約が必要だったので、今からは見れないかもしれません。
mov貰えないかと言う話をしているので、もしかしたらch9に転載するかも…

10時~20時までと、非常に長い一日で、かつ非常に濃い内容の一日。遠慮不要な題材を扱ったという点で、とても面白かったです。

私は主にtwitterで反応してました。こっちにまとめてあります。

上のニコニコのコメントですが、Roslynの赤いシャツはココで売っています。いいですよね、赤。私のパーソナルカラーです。また、「The Root of .NETの人」は、私の後の最後のセッションの荒井さんです。

この.NET Fringeは海外のコミュニティで、結構大規模に運営しているようです。ターゲットとしているのは.NETであってもOSS界隈のようで、HanselmanやSymeの顔がばーんと出てる辺りでも大きそうな感じです。

ただ、我々日本のコミュニティから見ると、.NETでありながらOSS寄りのコミュニティと言うのはあまりメジャーではない印象だったこともあり、.NET Fringeの日本開催の打診があった時には、どの程度人が集まるのか良くわからない、と言う所からスタートしました。

丁度2月初旬ごろだったと思うのですが、yukitosさんがtwitterで運営の声掛けをしていて、(私が名古屋にいると言う事もあり)一瞬迷ったのですがおもしろそうだと思って、運営に参加させていただきました。

まあ、それから上記の通り規模感が分からないのと、あまり大きくやろうとすると運営側の準備も大変になってしまうため、初回はOSS寄りと言う意向だけ本家からくみ取って、あとは自由にやる感じで進めて今日に至りました。

運営的な課題もあったのですが、開催してみたら人は集まるし、心配は杞憂でした。非常に有意義だったと思います。次回も是非やりたいですね。

このあたり、enoさんも詳しく感想書いて見えるので、参照お勧めします。


さて、私のセッションは、「Beachhead implements new opcode on CLR JIT」と言うタイトルでした。

セッションでもちょっと漏らしていましたが、このネタの準備が結構大変で (;´Д`) 無事にやりきることが出来たのは、自分自身も嬉しかったです。導入の事を詳しくしゃべれなかったので、この記事で書いておきます。

  • 去年のこみゅぷらすの時に、fujiwaraさんと初めて会って、その時にシリアライズ技術の話で盛り上がりました(ここはセッション中に軽く触れました)。で、シリアル化の高速化技術の手段として、ネイティブコードかそれに近い筋の最適化技術を適用できないか(特にデータのコピー)と考えていました。
  • 私は過去にx86のSSE命令などを駆使して、アセンブラで超高速なコピーコードを書いていた経験があったので、この時はネイティブコードでそれを実装して、シリアライザからDllImportとかでそれを呼び出すか、或いはC++/CLIかーみたいな事を考えていました。
  • しかし、それをやるとコードのマルチプラットフォーム対応をやりにくくなってしまう(特に、PCL化にも関心があった)ため、そのアイデアは途中で頓挫しました… まぁ、今回のネタでも解消したわけではありませんが。
  • 今回のネタのために、改めて資料やコードを読み返してみたりしていました。余談ですが、fujiwaraさんのMessagePack for CLIセッションを聞きながら、あーそう言えばアレがこうだったとかいろいろ思い出した…
  • もう一つ、Windows IoT Core側の話があります。「NETMFもう駄目じゃね?」みたいな話がいつぞやの勉強会で話題になったような気がするんですが(状況が芳しくない事には同意)、個人的にはIoT Coreの存在意義をあまり見いだせていなくて、やっぱりNETMFのように「.NET動いてくれて軽量な、良さげなランタイム欲しいな」と思ってました。RPiでLinux動くし、monoや.NET Coreが動くようになったので、ぶっちゃけそれでもいいわけですが、もっとOSカーネルコードが要らないような世界でも使えたら良いなと。ひょっとすると、coreclrがOSSになったことで、これをNETMFの後釜に出来る(ように改造できる)可能性があるかなと言う、淡い期待。で、果たして改造できるような見込みがあるのかどうか、感触を得てみたい(いつか)

このような(お話してない)バックグラウンドがあった上で、まぁRoslynもcorefxもアレだし、と言う所に繋がってきています。

で、難産だった部分の話も少し触れておくと:

  • 9月中は色々本業が忙しくて、思うように検証の時間が取れなくて焦っていた…
  • 最初に考えたネタがシリアライズの延長線として、JITでSSE2のPREFETCHNTA命令とかMOVNTDQとかを生成できないかなーと始めたのですが、これが最初にしては余りにネタの高望みし過ぎで、余計な改造をあっちこっちにした挙句、最後にプリフェッチヒント対象のアドレスをどう関連付けるのか、と言う所で(焦りと入り混じって)詰んでしまい、敢え無くボツ。
  • ベースキャンプに行って頭を冷やしながら、本業の仕事を少しこなしていた時に、たまたま名古屋方面で良く顔を合わす面々と昼飯食べながらボツネタの話をしていたら、新たなネタを思いついたので、そっちに方針転換。
    これは、coreclr上で全てのクラス内インスタンスメソッドを、強制的にvirtualとして扱うようにして、「本来オーバーライドできないメソッドがオーバーロード出来るようになりました、これでJava派の人たちに.NETでMockを扱いにくいとは言わせないぞ!!」という、これまた鬼畜で楽しそうなネタでした。
  • しかし、これは、coreclrの改造としてはすぐに実現したものの、一体どうやってこれを検証(Verifing tests)するのか?と言う所で詰み…
    メタデータ上はあくまでvirtualではないメソッドをvirtualとして扱うので、コンパイル時にもこれをだましてILを生成させないと、テストコードが実現出来ません。virtualなメソッドを呼び出すときには、callではなくcallvirtを使わなければならないからです。

ここまで来て詰んだ感かつ、他の発表者は全員題目発表済みだったので、かなり焦りがありました。

で、頭をひやして、とにかく「coreclrを改造可能であること」に重点をおいて、そういう「はしか」にかかった人たちへの道しるべとなる内容にして、全部自分でやろうとするのではなく、「コミュニティの力」を信頼することが大切じゃないのか? みたいな事を考えて、今回のネタに絞り込みました。

なので、今回のセッション内容の組み立ても、「まずは作ってみる(改造してみる)」「検証の方法も考える」という、自分でハマった点すらネタにして作り上げました (*´Д`) どうでしたか?実践的でしょ :)

実際になんとか動作検証出来たころには、狭めた範囲でも十分セッションの時間を使い切りそうだという事がわかり、やっぱり「やりすぎは良くない」「継続的に進めるのが重要」と言う事を改めて自覚できました。


さて、本家は分からないのですが、.NET Fringe Japanとしては、年一回のペースでやろうか、という話をしているところです。次回が何時で、どんな内容になるのかわかりませんが、ひょっとすると本家やF# foundationから誰かスピーカーをお願いしたり、と言う事があるかも知れませんね。ああ、すっかり忘れていましたが、OzCodeのFounderさんが、是非日本でOzCodeの解説をしたい、なんて言う話も貰ってました。

日本に閉じるのではなくて、もっと世界ともつながっていきたいと思います。

# スライドを全部(腐った)英語で書いたのは、そういう試みでもあります…

今回のセッションを生で聞いていただいた方、ニコニコ経由で聞いていただいた方、ありがとうございました。
それではまた。

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を定義したコンテナの作成と利用も当然可能です。「ディスポーザブルインフラストラクチャー」として、気軽に使ってみてください。