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

# TL; DR

Rubyでブロックの代わりにシンボルを渡せる記法を、[Pangaea](https://github.com/Syuparn/Pangaea)でもできるようにした！

```ruby:ruby
# どちらも同じ！
names.map {|name| name.upcase}
names.map(&:upcase)
```

```pangaea:pangaea
# どちらも同じ！
names.map {|name| name.uc}
names.map('uc)
```

# はじめに

2020年から「Pangaea」というプログラミング言語を自作しています。とにかくコードを短くしたい方におすすめ（？）です。

https://github.com/Syuparn/Pangaea

言語の詳細については過去の記事をご覧ください。

- [ワンライナー向け自作言語「Pangaea」の紹介](https://qiita.com/Syuparn/items/87cafc7fd206016a0f8d)
- [自作言語のオンラインチュートリアルを作ってみた【Pangaea Travel Guide】](https://qiita.com/Syuparn/items/fbd81da157faa33c62da)
- [Pangaea言語のプロトタイプチェーンについて](https://qiita.com/Syuparn/items/bbf1241ab804949eb690)

冒頭のコードからもお分かりの通り、PangaeaはRubyの言語機能から ~~たくさんパク~~ 大きな影響を受けています。ショートコーディングに使える `map(&:foo)` もかねてから取り入れたいと思っていました[^1]。

先日の `v0.8.0` リリースで使えるようになったので、実装までに考えたことをまとめたいと思います。

# Ruby本家の仕組み

まずは、Ruby本家の `map(&:foo)` の仕組みを見ていきます。大まかには

- ブロックの代わりにProcオブジェクトを引数に渡せる
- オブジェクトに `&` を付けることで、 `to_proc` メソッドが呼ばれ Procオブジェクトに変換可能
- `Symbol#to_proc` は、第一引数のselfという名前のメソッドを呼ぶ

という仕組みで `map {|o| o.foo}` と同じことをしています。

https://docs.ruby-lang.org/ja/latest/method/Symbol/i/to_proc.html

特に `Symbol#to_proc` が肝で、**第一引数の「selfという名前のメソッドを呼ぶ**」、言い換えると「**シンボル自身と同じ名前のメソッドを呼ぶ**」ことができます。

```ruby:イメージ（実際は組み込みメソッド）
class Symbol
  def to_proc
    Proc.new do |obj, *args, **kwargs, &blk|
      # self はシンボル自身
      obj.send(self, *args, **kwargs, &blk)
    end
  end
end
```

以下の解説記事がとても分かりやすかったです。

https://www.xmisao.com/2014/02/09/ruby-array-map-idiom-symbol-to-proc.html

# Pangaeaではどうやったか

基本的な仕組みは同じです。`Str#call` で、第一引数 `obj` の `self` (文字列自身)という名前のメソッドを呼んでいます。

```pangaea:native.pangaea
Str := {
  # メソッドobj[self]は単なる関数オブジェクト。()で呼び出す場合にはレシーバを第一引数に指定する
  call: m{|obj| obj[self](obj)},
  # ...
}
```

```pangaea: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.foo` は `foo` がcallableの場合メソッド呼び出し、そうでない場合プロパティ取得**としています。
（「メソッドと関数を同一視したい」「メソッド呼び出しで煩雑なかっこを省略したい」の両方を満たすためにこのような仕様となりました。詳しくは [以前の記事](https://qiita.com/Syuparn/items/87cafc7fd206016a0f8d#%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3) で紹介しています）

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

```pangaea:native.pangaea（改良版）
Str := {
  # obj[self](obj) とすると、obj[self]がcallableでない場合エラーになってしまう！
  call: m{|obj| {}.callProp(obj, self)},
  # ...
}
```

```pangaea
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` で使ってほしくないので、エラーを発生させています。

```pangaea: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` と書いたほうが短いです ([参考](https://qiita.com/Syuparn/items/87cafc7fd206016a0f8d#%E3%83%81%E3%82%A7%E3%83%BC%E3%83%B3%E3%82%B3%E3%83%B3%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88))