Archive | 10:50

メタプログラミング

24 12月

C++たんがまた今年も一人パーティ用のケーキとシャンパン買ってましたが、皆様いかがお過ごしでしょうか。

 

さて、本日はメタプログラミングがテーマです。

パターン

プログラミングには、「こういう場面はこう書くべきです」というようなパターンがつきものです。

この手のパターンは、昨日も少し話したような一種の「失敗しないための教科書」です。守らないと、アプリの欠損を招く確率を跳ね上げます。それはもう、たいそう跳ね上げます。

ある種のパターンは、ライブラリ化することで回避できたりします。しかし、ライブラリにできないパターンというのもあって、そういうものは、ちゃんとパターンを覚えてコードを書かないといけません。

ということで、言語構文を一通り覚えたら、次は、パターン本を読んで全部覚えろと言われるわけです。

しかし、それを社内すべてのエンジニアに期待できるのかというと、なかなか難しいのが実情です。そして、悪貨は良貨を駆逐する。しっかり勉強してる人が、わかってない人のコードに疲弊して、ばかばかしくなって辞めていくわけです(※末尾に補足あり)。

パターンを言語機能化

ライブラリ化しにくいパターンも、言語機能として何らかのサポートを加えることで回避できることがあります。

「このパターン通りに書け」だと意識の高い人しか実践してくれませんが、「この文法使うと便利だよ」なら割とみんな使ってくれます。

C#にはそんな、パターンだったものを文法化したものがいくつかあります。

イベント

いわゆるイベント駆動型のプログラムを作る場合、オブザーバー パターンというものを使います。C#のイベント構文を使わずに書くと、以下のようなパターンになります。

object _progressSync = new object();
Action<int> _progress;

// イベント ハンドラーの登録
public void AddProgressHandler(Action<int> handler)
{
    lock (_progressSync)
        _progress += handler;
}

// イベント ハンドラーの解除
public void RemoveProgressHandler(Action<int> handler)
{
    lock (_progressSync)
        _progress -= handler;
}

デリゲート(マルチキャスト可能な関数オブジェクト)の概念を持っていない言語だと、もうちょっとめんどくさいパターンになります。

C#の場合、オブザーバー パターンに相当する機能は、イベント構文という専用の構文があって、1行で書けます。

public event EventHandler<int> Progress;

これで、上記のコードと同じ挙動が得られます(具体的な実装はちょっと違いますが)。

イテレーター

データ処理、例えば、データ列の中から特定の条件を満たす要素だけを取り出すというような処理を考えてみましょう。一般に、この手の処理にはイテレーター パターンというものを使います。これも、C#の専用の言語機能に頼らず書くと、以下のようになります。

class WhereEnumerator<T> : IEnumerator<T>, IEnumerable<T>
{
    IEnumerator<T> _e;
    Func<T, bool> _cond;
    public WhereEnumerator(IEnumerator<T> e, Func<T, bool> cond)
    {
        _e = e;
        _cond = cond;
    }

    public T Current { get { return _e.Current; } }

    public bool MoveNext()
    {
        while (_e.MoveNext())
            if (_cond(_e.Current))
                return true;
        return false;
    }

    public IEnumerator<T> GetEnumerator()
    {
        return this;
    }
// 
一部省略
}

public static IEnumerable<T> Where<T>(IEnumerable<T> source, Func<T, bool> cond)
{
    return new WhereEnumerator<T>(source.GetEnumerator(), cond);
}

C#にはイベント構文というものがあって、このパターンを劇的に簡素化できます。以下の通りです。

public static IEnumerable<T> Where<T>(IEnumerable<T> source, Func<T, bool> cond)
{
    foreach (var x in source)
        if (cond(x))
            yield return x;
}

 

それでもまだまだパターン

そんなC#でも、まだまだパターンを書かないといけないことは多いです。

あるオブジェクトのあるプロパティの値が変化したことを、他のオブジェクトに伝えたいことがあります。データベース エンティティ変更追跡とか、GUIへの反映なんかが主だった例です。

この場合、結構面倒なコードを書く必要があります。例えば、XとYという2つのプロパティを持つだけの単純なクラスすら、以下のようになります。

class Point : INotifyPropertyChanged
{
    private int _x;
    public int X
    {
        get { return _x; }
        set { _x = value; RaisePropertyChanged(“X”); }
    }

    private int _y;
    public int Y
    {
        get { return _y; }
        set { _y = value; RaisePropertyChanged(“Y”); }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void RaisePropertyChanged(string name)
    {
        var d = PropertyChanged;
        if (d != null)
            d(thisnew PropertyChangedEventArgs(name));
    }
}

 

メタプログラミング

こういう時、例えば、以下のような簡素なクラスから、先ほどのようなパターンで煩雑化したクラスを自動生成したかったりします。

class PointDefinition
{
    int X;
    int Y;
}

 

こういう発想を、プログラムからプログラムを作るという意味で、メタプログラミング(meta-programming)と呼びます。

こういうのは、動的言語と呼ばれるようなタイプのプログラミング言語が得意とするところです。

まあ、C#でも、少し煩雑ですが、動的コード生成で対処できないことはないです。が、以下のような理由で敬遠されます。

  • なんだかんだ言って結構煩雑
  • この手の処理なら、コンパイル時コード生成でもできるんだから、動的(実行時)に生成したくない
    • コンパイル時にエラーがわからない(実行して初めてわかる)のが嫌
    • 性能を落とすのが嫌

 

なので、「C#にも、C言語の#defineみたいな置換マクロが欲しい」みたいな意見も時々聞きます。ただ、C#の場合には、Visual Studioとの連携も必要なので、マクロ(単純な文字列置換)では難しいです。もし導入しても、Visual Studioの補助が受けれなくなるデメリットの方が大きいです。

T4テンプレート

C#でのメタプログラミングは、本格的にやるならRoslynを待った方がいいかもしれません。

まあ、今現在の次善の策としては、T4テンプレート(Text Template Transformation Toolkit)というものを使うのがいいでしょう。コンパイル時のコード生成用のツールで、Visual Studio 2010からは標準で入っています。

結構慣れるまでが大変、かつ、そんなに使いやすい文法でもないんですが、以下のような感じです。

using System.ComponentModel;

public class <#= Def.Class #> : INotifyPropertyChanged
{
<#
foreach (var p in Def.Properties)
{
#>
    private <#= p.Type #> _<#= p.Name #>;
    public <#= p.Type #> <#= p.Name #>
    {
        get { return _<#= p.Name #>; }
        set { _<#= p.Name #> = value; RaisePropertyChanged(“<#= p.Name #>”); }
    }

<#
}
#>
    public event PropertyChangedEventHandler PropertyChanged;

    private void RaisePropertyChanged(string name)
    {
        var d = PropertyChanged;
        if (d != null)
            d(this, new PropertyChangedEventArgs(name));
    }
}

これで、簡素な定義クラスから、煩雑なパターン生成できます。上記のテンプレートをNotifyPropertyChanged.ttincというファイル名で作ったとして、それとは別に、以下のようなテンプレートを作ります。

<#@ template debug=”false” hostspecific=”false” language=”C#” #>
<#@ output extension=”.cs” #>
<#
var Def = new
{
    Class = “Point”,
    Properties = new[]
    {
        new { Type = “int”, Name = “X” },
        new { Type = “int”, Name = “Y” },
    },
};
#>
<#@include file=”NotifyPropertyChanged.ttinc” #>

その結果、先ほど作ったようなPointクラスが自動生成されます。

メタプログラミングする上で

メタプログラミングするに当たっては、難儀な問題がいろいろあったりします。いくつか説明していきましょう。

覚えるコスト

メタプログラミングは、既存の言語の文法の中に、別の文法を作るようなものです。新しい言語を覚えるコストが余計にかかります。

ツールとの連携

「パターンを覚えるにしても、新しい文法を覚えるにしても、(あるいはライブラリでやるなら)新しいクラスやメソッドを覚えるにしても、覚える量には変わりがないじゃないか」と思うかもしれません。

一般にはその通りなんですが、C#の場合、Visual Studioという強力な補助ツールがあるわけです。クラスやメソッドの追加の場合、Visual Studioの補完を使えます。しかし、新しい文法を考える場合、Visual Studioまで対応させるのは結構難しいです。

先ほどのT4テンプレートが微妙なのも、Visual Studioの補助がいまいちだからというのが大きいです。

読みやすさと書きやすさ

プログラミングという作業では、書きやすさよりも、読みやすさの方が大事です。書いた本人とは違う人がコードを読む機会が多いです。また、自分の書いたコードであっても、ちょっと前のことはすぐに忘れます。

メタプログラミングも、後から読んで困るほどの省略はしてはいけません。

まあ、煩雑なパターン コードよりは読みやすいはずなので、変な略語とかだけ使わなければ大丈夫でしょう。

なんでもはやろうとしない

メタプログラミングは、汎用プログラミング言語の構文に漏れるような、ある特定の領域に対して特化した構文を作るのに使います。

「特定の領域に絞る」というのは非常に重要です。メタプログラミングで何でもはやろうとしないでください。それは、汎用言語構文の役割です。

補足

 

悪貨に駆逐されないために

本題と外れるのでいったんスルーしましたが。

「ダメな人のコードにできる人が疲弊する」というので、本当にまずいのは、組織的な問題の部分です。組織運用の在り方から考えなおす必要があります。

「ダメの人のコード」というように、個人に帰着した問題になっている時点でアウトです。コード レビューなどを通してダメなコードをはじく仕組みを、チーム全体でやっていく必要があります。コードの責任を個人のものにしてはいけません。その責任はチームで持つべきです。