メタプログラミング¶
CrystalでのメタプログラミングはRubyの場合とは異なります。このページのリンクは、それらの違いと、それらを克服する方法についての洞察を提供することを願っています。
RubyとCrystalの違い¶
Rubyは、実行時にコードを修正するために、send
、method_missing
、instance_eval
、class_eval
、eval
、define_method
、remove_method
などを多用しています。また、実行時に新しいクラスまたはインスタンスメソッドを作成するために、モジュールを別のモジュールに追加するためのinclude
とextend
もサポートしています。ここに、2つの言語の最大の違いがあります。Crystalでは、実行時のコード生成は許可されていません。すべてのCrystalコードは、最終的なバイナリを実行する前に生成およびコンパイルする必要があります。
したがって、上記にリストしたメカニズムの多くは存在すらしていません。上記にリストしたメソッドのうち、Crystalはマクロ機能を使用してmethod_missing
のみを部分的にサポートしています。マクロを理解するために公式ドキュメントを読んでください。ただし、マクロはコンパイルステップ中に有効なCrystalメソッドを定義するために使用されるため、すべてのレシーバーとメソッド名は事前に知っておく必要があります。文字列またはシンボルからメソッド名を作成してレシーバーにsend
することはできません。send
のサポートはなく、コンパイルは失敗します。
Crystalはinclude
とextend
をサポートしています。ただし、インクルードまたは拡張されたすべてのコードは、コンパイルするために有効なCrystalである必要があります。
RubyのトリックをCrystalに翻訳する方法¶
しかし、勇敢なメタプログラマーにとってはすべてが失われたわけではありません。Crystalには、コンパイル時のコード生成のための強力な機能がまだあります。Crystal環境で動作するように、Rubyのテクニックを少し調整するだけで済みます。
extend
経由での#newのオーバーライド¶
Rubyでは、クラスのnew
メソッドをオーバーライドすることで、強力なことを実行できます。
module ClassMethods
def new(*args)
puts "Calling overridden new method with args #{args.inspect}"
# Can do arbitrary setup or calculations here...
instance = allocate
instance.send(:initialize, *args) # need to use #send since #initialize is private
instance
end
end
class Foo
def initialize(name)
puts "Calling Foo.new with arg #{name}"
end
end
foo = Foo.new('Quxo') # => Calling Foo.new with arg Quxo
p foo.class # => Foo
class Foo
extend ClassMethods
end
foo = Foo.new('Quxo')
# => Calling overridden new method with args ["Quxo"]
# => Calling Foo.new with arg Quxo
p foo.class # => Foo
上記の例でわかるように、Foo
インスタンスは通常のコンストラクターを呼び出します。extend
してnew
をオーバーライドすると、プロセスにあらゆる種類のものを注入できます。上記の例は、最小限の干渉のみを示し、オブジェクトのインスタンスを割り当てて初期化するだけです。このインスタンスはコンストラクターから返されます。
次の例では、new
をオーバーライドして、まったく異なる種類のクラスを返します。
class Bar
def initialize(foo)
puts "This arg was an instance of class #{foo.class}"
end
end
module ClassMethods
def new(*args)
puts "Calling overridden new method with args #{args.inspect}"
Bar.new(allocate) # return a completely different class instance
end
end
class Foo
extend ClassMethods
def initialize(name)
puts "Calling Foo.new with arg #{name}"
end
end
foo = Foo.new('Quxo')
# => Calling overridden new method with args ["Quxo"]
# => This arg was an instance of class Foo
p foo.class # => Bar
これにより、実行時に非常に強力なメタプログラミングが可能になります。クラスをプロキシとして別のクラスでラップし、この新しいプロキシオブジェクトへの参照を返すことができます。
Crystalでも同じ種類の魔法が使えるのでしょうか?それが不可能であれば、このセクションを書かなかったでしょう。ただし、後で説明するいくつかの注意点があります。
これは、Crystalの元のクラスと期待される動作です。
module ClassMethods
macro extended
def self.new(number : Int32)
puts "Calling overridden new added from extend hook, arg is #{number}"
instance = allocate
instance.initialize(number)
instance
end
end
end
class Foo
extend ClassMethods
@number : Int32
def initialize(number)
puts "Foo.initialize called with number #{number}"
@number = number
end
end
foo = Foo.new(5)
# => Calling overridden new added from extend hook, arg is 5
# => Foo.initialize called with number 5
puts foo.class # Foo
この例では、macro extended
フックを使用しています。このフックは、クラス本体がextend
メソッドを実行するたびに呼び出されます。このマクロを使用して、置換用のnew
メソッドを記述できます。
(メソッドシグネチャの詳細について明確にする必要があります。@number型の宣言Fooを削除すると、オーバーライドがサイレントに失敗します。"number : Int32"をFooクラスのinitializeシグネチャに追加すると、オーバーライドも失敗します。ここには、私が理解していないメソッドオーバーロードに関するいくつかの微妙な点があります。より多くの実験が必要です。上記の例はまだ機能しますが...)
method_missing
マクロによるメソッドの生成¶
以下は、レシーバーJSONオブジェクトのキーの存在に基づいて、欠落しているメソッドを作成するためにmethod_missing
マクロを使用する方法を示す非常に簡単な例です。
class Hashr
getter obj
def initialize(json : Hash(String, JSON::Any) | JSON::Any)
@obj = json
end
macro method_missing(key)
def {{ key.id }}
value = obj[{{ key.id.stringify }}]
Hashr.new(value)
end
end
def ==(other)
obj == other
end
end
record
と生成されたルックアップテーブルを使用してsend
を模倣する方法¶
サンプルコード+説明
alias_method
に対するCrystalのアプローチ¶
クラスを再オープンして、以前に定義したメソッドに新しい動作をさせたい場合があります。さらに、元のメソッドにもアクセス可能にしたいでしょう。Rubyでは、この目的のためにalias_method
を使用します。例
class Klass
def salute
puts "Aloha!"
end
end
Klass.new.salute # => Aloha!
class Klass
def salute_with_log
puts "Calling method..."
salute_without_log
puts "... Method called"
end
alias_method :salute_without_log, :salute
alias_method :salute, :salute_with_log
end
Klass.new.salute
# => Calling method...
# => Aloha!
# => ... Method called
Crystalで同じ作業を実行するのは非常に簡単です。Crystalには、以前に定義したバージョンのメソッドにアクセスできるprevious_def
というメソッドが用意されています。Crystalで同じ例を機能させるには、次のようになります。
class Klass
def salute
puts "Aloha!"
end
end
# Reopen the class...
class Klass
def salute
puts "Calling method..."
previous_def
end
end
# Reopen it again for kicks!
class Klass
def salute
previous_def
puts "... Method called"
end
end
Klass.new.salute
# => Calling method...
# => Aloha!
# => ... Method called
クラスを再オープンするたびに、previous_def
は以前のメソッド定義に設定されるため、これを使用して、Rubyと同じようにコンパイル時にエイリアスメソッドチェーンを構築できます。ただし、チェーンを拡張するたびに、元のメソッド定義へのアクセスは失われます。古いメソッドに別の場所で参照できる明示的な名前を付けているRubyとは異なり、Crystalはその機能を提供していません。
一般的なリソース¶
Ary Borenszweig(gitterでは@asterite)が2016年の会議でマクロに関する講演を行いました。 こちらで見ることができます。