LoginSignup
2
0

More than 1 year has passed since last update.

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

Posted at

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 と書いたほうが短いです (参考)

2
0
0

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
  3. You can use dark theme
What you can do with signing up
2
0