TL; DR
Rubyでブロックの代わりにシンボルを渡せる記法を、Pangaeaでもできるようにした!
# どちらも同じ!
names.map {|name| name.upcase}
names.map(&:upcase)
# どちらも同じ!
names.map {|name| name.uc}
names.map('uc)
はじめに
2020年から「Pangaea」というプログラミング言語を自作しています。とにかくコードを短くしたい方におすすめ(?)です。
言語の詳細については過去の記事をご覧ください。
冒頭のコードからもお分かりの通り、PangaeaはRubyの言語機能から たくさんパク 大きな影響を受けています。ショートコーディングに使える map(&:foo)
もかねてから取り入れたいと思っていました1。
先日の v0.8.0
リリースで使えるようになったので、実装までに考えたことをまとめたいと思います。
Ruby本家の仕組み
まずは、Ruby本家の map(&:foo)
の仕組みを見ていきます。大まかには
- ブロックの代わりにProcオブジェクトを引数に渡せる
- オブジェクトに
&
を付けることで、to_proc
メソッドが呼ばれ Procオブジェクトに変換可能 -
Symbol#to_proc
は、第一引数のselfという名前のメソッドを呼ぶ
という仕組みで map {|o| o.foo}
と同じことをしています。
特に Symbol#to_proc
が肝で、第一引数の「selfという名前のメソッドを呼ぶ」、言い換えると「シンボル自身と同じ名前のメソッドを呼ぶ」ことができます。
class Symbol
def to_proc
Proc.new do |obj, *args, **kwargs, &blk|
# self はシンボル自身
obj.send(self, *args, **kwargs, &blk)
end
end
end
以下の解説記事がとても分かりやすかったです。
Pangaeaではどうやったか
基本的な仕組みは同じです。Str#call
で、第一引数 obj
の self
(文字列自身)という名前のメソッドを呼んでいます。
Str := {
# メソッドobj[self]は単なる関数オブジェクト。()で呼び出す場合にはレシーバを第一引数に指定する
call: m{|obj| obj[self](obj)},
# ...
}
# 'uc.call は {|name| name.uc}.call と実質同じ
names.map {|name| name.uc}
names.map('uc)
Rubyとの違い
Proc、ブロック、メソッドの区別が無い
Pangaeaには関数オブジェクトしかありませんので、Rubyでやっていた &
による変換は不要です。ブロックに見えるのはただの関数リテラルです(シンタックスシュガーとして、末尾の関数リテラルはかっこの外に書けます)。
mapの引数は call
メソッドさえ持っていればよいので、Func
の代わりに Str
を渡しても問題ありません。
メソッドとインスタンス変数の区別が無い
Rubyは両者の名前空間が分かれていて、インスタンス変数にアクセスしたければ attr_accessor
等で生やした同名のgetterを呼び出します。そのため、配列各要素の @foo
を取り出したいなら単に arr.map(&:foo)
とすればよいです。
一方Pangaeaはメソッドとインスタンス変数の区別が無く、メソッドを単なる関数プロパティとして持っています。そのため、obj.foo
は foo
がcallableの場合メソッド呼び出し、そうでない場合プロパティ取得としています。
(「メソッドと関数を同一視したい」「メソッド呼び出しで煩雑なかっこを省略したい」の両方を満たすためにこのような仕様となりました。詳しくは 以前の記事 で紹介しています)
困ったことに、 arr.map('foo)
すると、arr['foo]
をcallableかどうかにかかわらず呼び出そうとしてしまいます。仕方ないので、obj.foo
(プロパティコール)評価時に内部で使われているメソッド Obj#callProp
を使うようにしました。
Str := {
# obj[self](obj) とすると、obj[self]がcallableでない場合エラーになってしまう!
call: m{|obj| {}.callProp(obj, self)},
# ...
}
Circle := {
new: _init('radius),
area: m{ .radius ** 2 * 3.14 },
}
circles := [Circle.new(1), Circle.new(2)]
# 関数ではないプロパティの参照
circles.map('radius).p # [1, 2]
# 関数プロパティ(=メソッド)の呼び出し
circles.map('area).p # [3.140000, 12.560000]
メソッドとプロパティについては、どこかを効率化すると別のところが辛くなるので難しいですね...
文字列とシンボルの区別が無い
Pangaeaでは文字列 "foo"
もシンボル 'foo
も実態は Str
オブジェクトです。とはいえ、プロパティの名前として使えない形式の文字列は Str#call
で使ってほしくないので、エラーを発生させています。
Str := {
# シンボルとして使用可能(=フィールド名に使用可能)な文字列でなければエラー発生
call: m{|obj| {}.callProp(obj, self) if .sym? else ValueErr.new("`#{self}` is not a symbol")},
# ...
}
おわりに
以上、Pangaeaに Rubyの map(&:foo)
を導入してみた紹介でした。真似してみて改めてシンタックスシュガー、そしてRubyの仕様の偉大さを感じました。