TL;DR
-
GitHub - Syuparn/Pangaea: A programming language for one-liner method chain lovers!
- ワンライナー向けオブジェクト指向言語作った
- メソッドチェーンの
.
にコンテキストを持たせた(.
,@
,$
) - fizzbuzzが34バイト!
# 1から100までのfizzbuzz
100~@{['fizz][\%3]+['buzz][\%5]}@p
はじめに
プログラミング言語「Pangaea」を自作しています。2020年の1月から開発をはじめ、1年半でようやく動作するようになりました(まだまだimportもできないですが...)。
GitHub - Syuparn/Pangaea: A programming language for one-liner method chain lovers!
ホスト言語はGolangで、設計は大部分「Go言語でつくるインタプリタ」のMonkey言語を参考にしています。
goreleaserで各OS向けのバイナリを配布しておりますので、ダウンロードしてすぐに起動できます。
# repl
$ pangaea
Pangaea 0.3.0
multi : multi-line mode
single: single-line mode (default)
>>> "Hello, world!".p
Hello, world!
nil
>>> ^C
# run script
$ pangaea hello.pangaea
Hello, world!
作った背景
とにかくプログラミング言語が作りたかった
「ワンライナーが気持ちよく書ける言語」を目指して作っています。そのため、メソッドチェーンやパイプライン的な構文を使って、改行せずに処理を繋いでコーディングができることを一番重視しています。その結果、以下のような特徴の言語になりました。
[{"name": "Taro", "age": 22}, {"name": "Jiro", "age": 19}, {"name": "Hanako", "age": 21}].select {.age >= 20}@name.join(" and ").{|s| "#{s} can drink."}.p
# Taro and Hanako can drink.
- メソッド名は限界まで短く
- printは
p
、strへの変換はS
- printは
- 関数ではなくメソッドを提供してチェーンを切らないように
-
len([1, 2, 3])
よりも[1, 2, 3].len
-
- チェーンコンテキスト(後述)でコード短縮
- コレクションの各要素のメソッド呼び出し
["a","b","c"]@uc == ["A", "B", "C"]
- コレクションの各要素のメソッド呼び出し
- チェーンを使って直接関数を適用可能
- ElixirやStreemのパイプラインに触発されました
2.{|i| i * 3}
「Pangaea」という名前も、「パンゲア大陸のように一続きのコードが書ける」というところから付けています。
言語の特徴
- 初見で理解しやすいワンライナー
- 短いけどOOPっぽい構文
- インタープリター型
- プロトタイプベースオブジェクト指向
- 全てのものがオブジェクト
- オブジェクトは全てimmutable 1
- 第一級関数+レキシカルスコープ
- コンテキストを持ったメソッドチェーン
- マジックメソッドによる演算子オーバーロード
- 使い捨てコード用なので速度は求めない
動的型付け言語にしたのは、型宣言を無くしてコードを短縮したかったからです。他にも、「最近のイケイケ静的型付け言語は企業が作るイメージがあるので、手作り感のあるスクリプト言語がいいな」という思いもあります。偉大なP言語(Perl,Python,Ruby,PHP)の構文をパクりまくったあげく「P」の頭文字まで取ってすみません
プロトタイプベースにしたのは、言語仕様をシンプルにしたかったから と、クラスベースにするとほぼRubyになってしまうから です。
immutableにしたのは、メソッドチェーンを値の変換とすることで状態管理しなくて済むようにしたかったからです。
影響を受けた主な言語
(多い順に)
- Ruby
- Python
- Elixir
- JavaScript
- Io
- Perl
- Streem
文法
チェーンコンテキスト
オブジェクト指向で空気のように使っているレシーバの後の .
、文字数がもったいなくないですか?
というわけで、Pangaeaでは .
以外の文字もチェーンに使うことでチェーンのコンテキストを表しています。
この「コンテキスト」はPerlのコンテキストを意識していますが、データ型ではなくメッセージの側に適用されます。
記号 | チェーン | 意味 | 例 |
---|---|---|---|
. |
Scalarチェーン | レシーバに対してメソッド呼び出し | 1.S # "1" |
@ |
Listチェーン | レシーバの各要素に対してメソッド呼び出し | [1,2].S # ["1", "2"] |
$ |
Reduceチェーン | レシーバの各要素に対してメソッドを呼び出して畳み込み | [1,2,3]$+ # 6 |
listチェーン、reduceチェーンによって、コレクションの繰り返し処理を可能な限り短く表現可能にしました。
また、listチェーンは nil
を無視するため、if式と組み合わせることで要素のフィルタリングも可能です(Pythonのリスト内包表記やStreemに着想を得ました)。
(2:20)@{|n| n if n.prime?}.p # [2, 3, 5, 7, 11, 13, 17, 19]
# 専用のメソッドも用意(内部実装は↑と同じ)
(2:20).select('prime?).p
また、チェーンには付加的なコンテキストを付け足すことが可能です。
記号 | チェーン | 意味 | 例 |
---|---|---|---|
& |
Lonelyチェーン | レシーバがnilの場合チェーンを評価しない | nil&.even? # nil |
~ |
Thoughtfulチェーン | 戻り値がnil or 例外発生の場合レシーバを返す | 6~./(0) # 6 |
= |
strictチェーン | listチェーンでnilを無視しなくなる | (1:5)=@{\1 if \1.even?} # [nil, 2, nil, 4] |
エラーハンドリングやnilチェックを長々と書かずに済むようになっています。
プロパティ
フィールドもメソッドもオブジェクトのプロパティという形で直接持たせます。
オブジェクトが肥大化しそうですが、プロトタイプベースなので親(プロトタイプ)のメソッドを呼び出せばそれぞれのオブジェクトにメソッドを持たせる必要はありません。
myObj := {
name: "Taro",
sayHi: m{|| "I am #{.name}".p},
}
myObj.name.p # "Taro"
myObj.sayHi # "I am Taro"
selfの扱い
メソッドを扱うときに悩むのが、self
(または this
) をどう扱うかです。
- selfを明示し、メソッドと関数を同一視する(Python, Perl等)
- 言語仕様がシンプル
-
メソッドの引数に
self
が必須、メソッド内のプロパティ参照にもself
を付ける必要がある
- selfを省略可能にし、メソッドと関数を別物にする(Ruby等)
- メソッドが簡潔に書ける
- 言語仕様が複雑、メソッドを関数オブジェクトとして扱う際に変換が必要
- メソッドと関数を同一視し、メソッド呼び出しの時のみselfにレシーバを束縛(JS等)
- 上記2つのいいとこどり
- 関数呼び出しのときはselfに引数を渡せない(メソッドはメソッドとしてしか使えない)
Pangaeaでは、なるべく関数をシンプルに扱いたかった(パイプライン的に無名関数を多用する)ので、1.を採用しました。self
が必須になる問題は、以下2つのシンタックスシュガーで対処しています。
- メソッド構文
m{|| }
# {|self| "I am #{.name}"} のシンタックスシュガー。selfはただの第一引数名
m{|| "I am #{.name}"}
Pythonのf-string f"..."
から着想を得ました。
- 匿名チェーン
# レシーバを書かない場合、第一引数をレシーバとして評価する
m{|| "I am #{.name}"} # .name は self.name
# もちろん関数の中でも使用可能
(1:6)@{.even?} # [false, true, false, true, false]
(1:6)@{|i| i.even?} # と同じ
# ちなみに引数は宣言しなくても\{n番目引数}で参照可能
(1:6)@{\1.even?}
こちらはjqから頂きました。
メソッド呼び出しのかっこ
メソッドチェーンをつなぐと、カッコをいちいち書くのが煩雑です。一方、カッコを省略可能にすると「メソッド呼び出し」と「メソッドそのものの取得」を区別できなくなってしまいます。
res := myObj.hello # resはhelloメソッドの戻り値?それともhelloメソッドそのもの?
そこで、Pangaeaでは、
- チェーンでのメソッド呼び出しはかっこ省略可能
- メソッドそのものを取得する場合は
myObj['hello]
- 関数の呼び出しはかっこ必須(でないと関数を代入できなくなる)
という規則を採用しました。
継承
プロトタイプベースなので、プロパティ呼び出しはプロトタイプチェーンを辿って評価されます 2 。
myObj := {
name: "parent",
sayHi: m{"I am #{.name}".p},
} # Objの子供
myObj.sayHi # I am parent
# myChildの子供
myChild := myObj.bear({name: "child"})
# sayHiはmyObjのもの、nameはmyChildのもの
myChild.sayHi # I am child
組み込みオブジェクトもすべてプロトタイプチェーン上に存在します。
>>> myChild.ancestors
[{"name": "parent", "sayHi": {|self| "I am #{ .name() }".p()}}, Obj, BaseObj]
>>> true.ancestors
[1, Int, Num, Obj, BaseObj]
>>> "abc".ancestors
[Str, Obj, BaseObj]
プロトタイプベースではクラスはありません。Int
も Obj
もあくまでその型のプロトタイプにすぎません。そこで、組み込みオブジェクトはリスコフの置換原則を守れるよう、その型のゼロ値として扱えます。
>>> Int + 3 # Int は 0 として扱える
3
>>> Arr.len # Arr は [] として扱える
0
ただ、プロトタイプベースとはいえ毎回bear
を書くのも大変なので、コンストラクタ用の関数 Kernel._init
を用意しています。
Person := {new: _init('name, 'age)}
taro := Person.new("Taro", 20)
taro.name.p # Taro
taro.age.p # 20
マジックメソッド
- 演算子
全てメソッド呼び出しのシンタックスシュガーです。
HourInt := {
new: _init('n),
'+: m{|other| HourInt.new((.n + other.n) % 24)},
}
(HourInt.new(19) + HourInt.new(6)).p # {"n": 1}
- 要素参照
[]
atメソッドのシンタックスシュガーです。
evens := {
at: m{|indices| indices[0].{\ * 2 if .kindOf?(Int)}},
}
evens[2].p # 4
evens[10].p # 20
evens['i].p # nil
_iter
listチェーン、reduceチェーンは、_iter
メソッドが返すイテレータの各要素を評価しています。これにより、オブジェクトをコレクション的に扱うことができます。
# Str#_iterは各文字をyieldするイテレータを返す
>>> "abc"@p
a
b
c
# Arr#_iterは各要素をyieldするイテレータを返す
>>> ['a, 'b, 'c]@p
a
b
c
# Int#_iterは1からselfまでの整数をyieldするイテレータを返す
>>> 3@p
1
2
3
_name
repr等で表示する際のエイリアスとして使用できます。Obj
などの組み込みオブジェクトでも使用しています。
>>> Obj
Obj
# もし_nameが無いとプロパティがずらずら出てきて見づらい...
>>> Obj
{"A": {|self| @{|| \}}, "B": {|| [builtin]}, "S": {|| [builtin]}, "acc": {|self, f, init: nil...
fizzbuzzコード
以上の機能で、冒頭のfizzbuzzコードが作られています。
# 1から100までのfizzbuzz
100~@{['fizz][\%3]+['buzz][\%5]}@p
100
~@ # thoughtful listチェーン:100._iterの各要素(1から100までの整数)を評価、ただしnilかエラーの場合レシーバを返す
{ # 各要素に対し無名関数を呼び出し
# \は\1(1番目の引数)のシンタックスシュガー
['fizz][\%3]+['buzz][\%5] # Arr.atはout of indexの場合nilを返すので
# 15の倍数: 'fizz + 'buzz
# 3の倍数: 'fizz + nil
# 5の倍数: nil + 'buzz
# それ以外: nil + nil
# nilはゼロ値として扱える(Str#+はnilを""として扱い、Nil#+は第二引数をそのまま返す)ので
# 15の倍数: 'fizzbuzz
# 3の倍数: 'fizz
# 5の倍数: 'buzz
# それ以外: nil (thoughtfulチェーンで\に戻される)
}
@ # listチェーン:各要素を評価
p # Obj#p:stdoutに表示
これからやりたいこと
- ソースコードのimport、モジュール管理機能
- 各種メソッド追加
- ドキュメント作成(今はドキュメントがテストコードしかありません...)
辛うじて言語の形にすることができましたが、普段使いするにはまだ機能が足りず厳しいです。これからもゆるゆると開発を続けていきたいと思います。
ここまでお付き合いいただきありがとうございました。