いくつかの標準マクロ


アレもコレもマクロだった

次のコードにはマクロが含まれています。どれがマクロか予想してみてください。

mutable sum = 0;
for (mutable i = 1; i <= 10; i++)
{
   when (i % 2 == 0)
   {
      sum += i;
   }
}

このコードには 4 つのマクロが含まれています。 for++when+= です。このように,いかにも基本のような構文であってもも Nemerle ではマクロにより拡張実装されているのです。これだけを見てもマクロが強力であることが垣間見えるでしょう。

既に出てきた構文では,他に if-elseforeachwhile がマクロです。 matchtry はマクロではありません。しかしマクロはコンパイル時に解決されるため,マクロであるかマクロでないかということは実際には気にすることはありません。

リソース処理

IDisposable インターフェイスを実装するクラスは,使用後に Dispose メソッドを呼ぶことが期待されます。 using マクロを用いることで,毎回 Dispose メソッドを呼ぶコードを記述負担を減らすことができます。

using (d = GetResource())
{
   // リソース処理
}
catch
{
   | _ is SomeException => // 例外処理
}

catch 節は省略可能です。 try-catch と同じように,最後に評価された値を返します。

レコード

コンストラクターのパラメーターでフィールドを初期化するということが良くあります。

public class Point
{
   private x : int;
   private y : int;

   public this(x : int, y : int)
   {
      this.x = x;
      this.y = y;
   }
}

_ = Point(2, 3);

レコードマクロを使うとこのコンストラクターを自動生成できます。

[Record]
public class Point
{
   private x : int;
   private y : int;
}

_ = Point(2, 3);

明示的に指定しなければすべてのフィールドがコンストラクターで初期化される対象になりますが,初期化するフィールドを指定,あるいは初期化しないフィールドを指定することができます。また,初期化しないフィールドに RecordIgnore マクロを注釈するという方法もあります。以下の 3 つは同じです。

[Record(Include = [name, age])]
public Person
{
   private name : string;
   private age: int;
   private nickname : string;
}
[Record(Exclude = [nickname])]
public Person
{
   private name : string;
   private age: int;
   private nickname : string;
}
[Record]
public Person
{
   private name : string;
   private age: int;
   [RecordIgnore]
   private nickname : string;
}

レコードマクロを使う場合でデフォルトコンストラクターが必要である場合は,明示的にデフォルトコンストラクターを宣言してやる必要があります。

レコードマクロに関連する重要なマクロにアクセッサーマクロがあります。ここでは割愛しますので,詳しくは『Nemerle の自動実装系のマクロ』をご覧ください。

未実装

実装されていないコードでは NotImplementedException を投げます。これを自動化するのが NotImplemented マクロです。以下のように使用します。

using Nemerle;

[NotImplemented]
public GetGreetingMessage(name : string) : string
{
   // TODO: 何か素晴らしいメッセージを考える
   def message = "...";
   $"Hello, $name. $message";
}

NotImplemented マクロは現在の関数の実装を破棄し, NotImplementedException を投げるコードに差し替えます。大体以下のような感じになります。

public GetGreetingMessage(_name : string) : string
{
   throw NotImplementedException("...");
}

オブジェクト修飾

オブジェクト修飾マクロは,オブジェクトのプロパティを変更したり,要素のコレクションを追加したりするための構文を提供します。

まずプロパティの初期化です。次のような座標クラスを考えます。

public class Point
{
   public X : int { get; set; }
   public Y : int { get; set; }
   public override ToString() : string { $"($X, $Y)"; }
}

このクラスで座標 (3, 4) の点を定義してコンソールに出力し,次いで座標 (1, -1) の点に移動して再度コンソールに出力ためには,通常次のようにします。

def p = Point();
p.X = 3;
p.Y = 4;
System.Console.WriteLine(p);
p.X = 1;
p.Y = -1;
System.Console.WriteLine(p);

オブジェクト修飾マクロを使うと次のように書けます。

using Nemerle.Extensions;

def p = Point() <- {
   X = 3;
   Y = 4;
};
System.Console.WriteLine(p);
_ = p <- {
   X = 1;
   Y = -1;
};
System.Console.WriteLine(p);

コレクションの場合は次のようになります。

using SCG = System.Collections.Generic;
def list = SCG.List() <- [1, 2, 3];
_ = list <- [4, 5];

辞書の場合はキーと値を = で区切る記法が利用できます。

def dictionary = SCG.Dictionary() <- ["X" = 1, "Y" = 2];
_ = dictionary <- ["Z" = 3];

コレクションの操作はクラスに Add という名前のメソッドが定義されていることが使用の条件です。この記法では Add メソッドの返り値を無視するため, Nemerle.Collections 名前空間内のコレクションのように Add の返り値がコレクションである不変コレクションには不適です。あくまでオブジェクトに変更を加えるためのマクロです。

オブジェクト修飾マクロは,プロパティにせよコレクションにせよ,変更されたオブジェクト自身を返す形になっています。 .NET Framework の StringBuilder クラスの Append メソッドと同様の形式をとっています。連結して流れるように処理を書くことができます。

契約プログラミング

契約プログラミングはコードが満たすべき条件を,コードの主処理とは別の場所に記述する方法です。 Nemerle には契約プログラミングのためのマクロがあります。契約違反の場合には Nemerle.Core.AssertionException が投げられます。

コードが満たすべき条件には以下のものが挙げられます。

  • 事前条件: 関数の処理前に満たされるべき条件
  • 事後条件: 関数の処理後に満たされるべき条件
  • 不変条件: どのような状況下においても満たされるべき条件
  • その他

事前条件には requires キーワードを使い,事後条件には ensures キーワードを使います。事後条件で関数の返り値を参照する場合は value という変数を使います。

using Nemerle.Assertions;

public class Buffer[T]
{
   private buffer : array[T];
   private mutable count : int;
   private mutable position : int;
   
   public this(size : int)
      requires size > 0
      ensures buffer != null && buffer.Length > 0
   {
      buffer = array(size);
      count = 0
      position = 0;
   }
   
   public Size : int
   {
      get ensures value > 0
      {
         this.buffer.Length;
      }
   }
}

不変条件は invariant キーワードを使います。

public class Buffer[T]
   invariant 0 <= position && position <= count
{
   public Read() : T
      requires position < count
   {
      def item = buffer[position];
      position++;
      item;
   }
}

事前条件でパラメーターが null でないことを契約する場合には特別な記法があって, NotNull マクロを使います。 NotNull マクロを使い契約違反した場合, ArgumentNullException が投げられます。また, otherwise キーワードにより契約違反時に投げられる例外を AssetionException 以外にすることもできます。

public class Buffer[T]
   invariant 0 <= position && position <= count
{
   public Fill([NotNull] items : array[T], index : int, count : int) : void
      requires 0 <= index && index < items.Length
         otherwise throw System.ArgumentOutOfRangeException("index")
      requires 0 < count && index + count <= items.Length
         otherwise throw System.ArgumentOutOfRangeException("count")
      requires count <= buffer.Length
         otherwise throw System.ArgumentOutOfRangeException("count")
   {
      System.Array.Copy(items, index, this.buffer, 0, count);
      this.position = 0;
      this.count = count;
   }
}

遅延評価

遅延評価は結果が必要になるまで結果を返さない仕組みです。例を見てみます。

First(first : int, _second : int) : int { first; }

def good = () => 1;
def bad = () => throw System.Exception();
_ = First(good(), bad());

これを実行すると, First 関数の 2 番目のパラメーターの bad 関数が例外を投げているので,例外が発生します。しかし First の実装をよく見ると, 2 番目のパラメーターは First 関数内でも使われておらず, bad() の評価は実際には不要です。つまり不要な計算を行っていることが問題の原因なのです。

遅延評価マクロを用いると次のようになります。

First(first : int, [Lazy] _second : int) : int { first; }

def good = () => 1;
def bad = () => throw System.Exception();
_ = First(good(), lazy(bad()));

これにより bad() が遅延評価されるようになります。 bad() はどこからも必要とされていないので評価されず,結果として何もしません (例外が発生しません)。

内部的には Nemerle.LazyValue クラスを利用しています。 LazyValue クラスを使って書くと, _second の型を int ではなく LazyValue[int] と宣言する必要があり,関数内部で intLazyValue[int] 間の処理を実装する必要があります。その面倒な処理をマクロがよしなに解決してくれます。

命令型プログラミング

ラベル付きブロックを利用すれば関数内で即座に値を返したり,ループを抜けるといった処理が可能です。

def f(s : string)
{
   return: {
      when (s == null)
      {
         return(null);
      }
      when (s.StartsWith("http://"))
      {
         return("HTTP");
      }
      // ...
   }
}
break: {
   for (mutable i = 0; i < 10; i++)
   {
      when (i != 3)
      {
         when (i == 5)
         {
            break();
         }
         System.Console.WriteLine(i);
      }
   }
}

しかしこの方法はネストが深くなるし,全体を囲むラベル付きブロックを作らなければいけないので不便です。従来のような returnbreak が欲しくなってきます。これは Nemerle.Imperative 名前空間にマクロとして定義されています。このマクロを用いて先述の例を次のように書くことができます。

using Nemerle.Imperative;

def f(s : string)
{
   when (s == null)
   {
      return null;
   }
   when (s.StartsWith("http://"))
   {
      return "HTTP";
   }
   // ...
}
for (mutable i = 0; i < 10; i++)
{
   when (i == 3)
   {
      continue;
   }
   when (i == 5)
   {
      break;
   }
   System.Console.WriteLine(i);
}