LoginSignup
0

posted at

自作言語でもRubyの map(&:foo) を真似したい!

TL; DR

Rubyでブロックの代わりにシンボルを渡せる記法を、Pangaeaでもできるようにした!

ruby
# どちらも同じ!
names.map {|name| name.upcase}
names.map(&:upcase)
pangaea
# どちらも同じ!
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 で、第一引数 objself (文字列自身)という名前のメソッドを呼んでいます。

native.pangaea
Str := {
  # メソッドobj[self]は単なる関数オブジェクト。()で呼び出す場合にはレシーバを第一引数に指定する
  call: m{|obj| obj[self](obj)},
  # ...
}
pangaea
# '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.foofoo がcallableの場合メソッド呼び出し、そうでない場合プロパティ取得としています。
(「メソッドと関数を同一視したい」「メソッド呼び出しで煩雑なかっこを省略したい」の両方を満たすためにこのような仕様となりました。詳しくは 以前の記事 で紹介しています)

困ったことに、 arr.map('foo) すると、arr['foo] をcallableかどうかにかかわらず呼び出そうとしてしまいます。仕方ないので、obj.foo (プロパティコール)評価時に内部で使われているメソッド Obj#callProp を使うようにしました。

native.pangaea(改良版)
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 で使ってほしくないので、エラーを発生させています。

native.pangaea(さらに改良版)
Str := {
  # シンボルとして使用可能(=フィールド名に使用可能)な文字列でなければエラー発生
  call: m{|obj| {}.callProp(obj, self) if .sym? else ValueErr.new("`#{self}` is not a symbol")},
  # ...
}

おわりに

以上、Pangaeaに Rubyの map(&:foo) を導入してみた紹介でした。真似してみて改めてシンタックスシュガー、そしてRubyの仕様の偉大さを感じました。

  1. 実は、mapに限って言えば、Listチェーンを使って @foo と書いたほうが短いです (参考)

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
0