先日、このような記事がでました。
すごく書いてみたいと思ったので今日はTreesに挑戦した話を記事にしました。
そうだ、メソッドチェーンもどきを書いてみよう
私の所属しているコミュニティには、このようなネタOSSが存在しています。
言語機能を活かして「前前前世」を出力しよう、というものです。
Treesでなにか書きたいな、と思っていたのでこのRADWIMPSに挑戦しようと思った次第です。
つまりこういうことをしました
┌─────┐
│ seq ├───────────────────────────────────────┐
│ ├──────────────────────────┐ │
└─┬───┘ │ ┌─┴──┐
┌─┴─────┐ │ │then│
│defproc├─────┐ │ └─┬──┘
└┬──────┘ ┌─•───┐ │ ┌─•──┐
┌┴─────┐ │ seq ├──────┐ │ │then│
│"then"│ └─┬───┘ ┌┴───┐ │ └─┬──┘
└──────┘ ┌─┴───┐ │exec│ │ ┌─•──┐
│print│ └┬───┘ │ │then│
└─┬───┘ ┌┴─┐ │ └─┬──┘
┌─┴────┐ │$0│ │ ┌─•──┐
│"then"│ └──┘ │ │ se │
└──────┘ │ └────┘
┌──────────────────────────────┘
┌─┴─────┐
│defproc├─────┐
└┬──────┘ ┌─•───┐
┌┴─────┐ │ seq │
│"se" │ └─┬───┘
└──────┘ ┌─┴───┐
│print│
└─┬───┘
┌─┴────┐
│ "se" │
└──────┘
Treesとは
Trees は、ブロックプログラミング言語で、以下の特徴を備えています!
分かりやすい (easy to understand)
読みやすい (readable)
曖昧性がない (clear)
(公式README)より
というわけで、かなり読みやすい言語です
書くのはs
実行手順
Repoをクローン
インタプリタをビルド
cargo build --release
お好みのファイルを実行
<path_to_Trees>/target/release/trees <trees_file>.tr
基本的な構文
詳細はWikiに掲載される予定らしいので、今回使ったところを紹介します
主観もちょっと入ってるかもしれないです
ブロック
罫線を用いたブロックが基本単位になります。
このブロックは関数として扱われます。
例えば、定数3を表す場合、
┌───┐
│ 3 │
└───┘
のように書きます。これは、
() => 3
とみなすことができます。
プラグ
ブロック同士をつなぐ結線の接合のことで、Treesはこのプラグをもちいて実行を制御できます。
プラグはブロックプラグと引数プラグに分類ができ、ブロック上部のただ一つのプラグをブロックプラグ、それ以外の辺から生えるプラグを引数プラグと呼びます。
これらのプラグは交差してはいけません。
また制約として、単一のプログラムにおいて
- ブロックプラグを持たないブロックがただ一つ存在すること
- 他のブロックは必ずブロックプラグを持つこと
この2つが存在します。ブロックプラグを持たないブロックが、プログラムのエントリポイントとなり実行開始に最初に評価されるブロックになります。
順次実行 seq
引数プラグは反時計回りに順番が定義され、seq
(sequence)は引数プラグに渡されたブロックをその順番で実行します。
手続き的な書き方をしたい場合、このseq
は必須でしょう。
プロシージャ定義 defproc
もちろんTreesには標準で用意されているブロックキーワードがいくつか存在しており、自作関数を作成するキーワードがdefproc
(define procedure)です。
┌───┐
│seq├────────────────────┐
└─┬─┘ ┌┴────┐
┌─┴─────┐ │print│
│defproc├─────┐ └┬────┘
└┬──────┘ ┌•┐ ┌┴────────┐
┌┴──────────┐│*├─┐ │two times│
│"two times"│└┬┘┌┴─┐ └┬────────┘
└───────────┘┌┴┐│$0│ ┌┴┐
│2│└──┘ │3│
└─┘ └─┘
引用: Wiki
先程までのプラグとseq
の解説も同時にすると、
- ブロックプラグ(上部の結線)を持たないブロック(最上部の
seq
ブロック)が存在するので、そこから開始される -
seq
ブロックは反時計回りに引数プラグ先のブロックを実行するので、下部のdefproc
が実行される -
defproc
は第0引数に関数名、第1引数にプロシージャが実行するブロックを受け取る -
defproc
処理が終わったので、右に伸びる引数プラグ先のprint
ブロックに移る -
print
ブロックは引数プラグ先のブロックを実行し、評価された値を出力する -
two times
ブロックは3を第0引数に受取、2倍する - 結果、6が評価される
-
print
ブロックにわたり、「6」が出力された!
ざっとこのような処理になるでしょうか
ちょっと見慣れない記法が出てきましたね(そもそも全部見慣れない)
defproc
に渡しているプラグに•
が存在しています。これが今回の記事で目玉のブロッククォート機能です。
ブロッククォート
最初のブロックの紹介でもお話したように、ブロックは関数として扱われています。そして、この•
は、ブロックを遅延評価する機能を持っています。つまり実行時まで下部含めたブロックが実行されない、ということです。
まさにdefproc
はこの機能を利用しており、関数本体の記述としてブロック群をそのまま(評価させずに)渡すためにブロッククォートを使っているのです。
ちなみに、ブロッククォートを使わない場合実行時に評価されてしまいvoidが返るようになってしまうためエラーが発生します。
最初のコードの解説
さて、基本的な構文を抑えたうえで再度本題のコードを見ていきましょう。
┌─────┐
│ seq ├───────────────────────────────────────┐
│ ├──────────────────────────┐ │
└─┬───┘ │ ┌─┴──┐
┌─┴─────┐ │ │then│
│defproc├─────┐ │ └─┬──┘
└┬──────┘ ┌─•───┐ │ ┌─•──┐
┌┴─────┐ │ seq ├──────┐ │ │then│
│"then"│ └─┬───┘ ┌┴───┐ │ └─┬──┘
└──────┘ ┌─┴───┐ │exec│ │ ┌─•──┐
│print│ └┬───┘ │ │then│
└─┬───┘ ┌┴─┐ │ └─┬──┘
┌─┴────┐ │$0│ │ ┌─•──┐
│"then"│ └──┘ │ │ se │
└──────┘ │ └────┘
┌──────────────────────────────┘
┌─┴─────┐
│defproc├─────┐
└┬──────┘ ┌─•───┐
┌┴─────┐ │ seq │
│"se" │ └─┬───┘
└──────┘ ┌─┴───┐
│print│
└─┬───┘
┌─┴────┐
│ "se" │
└──────┘
正直なところ、見るだけで何がしたいかがぱっとわかってしまうのがこの言語の恐ろしいところです。
(exec
は引数のブロックを実行するものです。)
なのでもう解説しなくていいですか?と思うんですが、解説します
本体
┌─┴──┐
│then│
└─┬──┘
┌─•──┐
│then│
└─┬──┘
┌─•──┐
│then│
└─┬──┘
┌─•──┐
│ se │
└────┘
シンプルですね。他の言語のように書くとすれば
then().then().then().se()
のようになります。
ブロッククォートを使っているのは、then
の実装によるものです。
then
┌─┴─────┐
│defproc├─────┐
└┬──────┘ ┌─•───┐
┌┴─────┐ │ seq ├──────┐
│"then"│ └─┬───┘ ┌┴───┐
└──────┘ ┌─┴───┐ │exec│
│print│ └┬───┘
└─┬───┘ ┌┴─┐
┌─┴────┐ │$0│
│"then"│ └──┘
└──────┘
"then"を出力したあとに、$0
のブロックを実行します。
つまり、本体でthenのあとにブロッククォート付きで渡された関数が実行される、ということです。
se
┌─┴─────┐
│defproc├─────┐
└┬──────┘ ┌─•───┐
┌┴─────┐ │ seq │
│"se" │ └─┬───┘
└──────┘ ┌─┴───┐
│print│
└─┬───┘
┌─┴────┐
│ "se" │
└──────┘
"se"を出力します。
実行結果
本来のレギュレーションは日本語出力なので、少しブロックの体裁が崩れてしまいますが、このように実行されます。
まとめ
罫線を入れるのがめちゃくちゃしんどいです。
追記: フィボナッチ数を出力するやつ(2024年3月30日)
コード
┌─────┐
│ seq ├─────────────────────────────────┐
└──┬──┘ ┌────┐ ┌─┴───┐
┌──┴────┐ │ ┌─•───┐ │print│
│defproc├─┘ │exec ├────────────┐ └─┬───┘
└──┬────┘ └─────┘ ┌─┴───┐ ┌─┴───┐
┌──┴──┐ ┌───────────────────┤if0 │ │ fib │
│"fib"│ ┌─┴──┐ └─┬─┬─┘ └─┬───┘
└─────┘ │ $0 │ ┌──────────────┘ │ ┌─┴──────────┐
└────┘ ┌─/─┐ ┌──┴─┐ │ str to int │
│ 0 │ ┌──────┤ if │ └───┬────────┘
└───┘ ┌─┴─┐ └┬──┬┘ ┌─┴─────────┐
┌─────────────────┤ = │ ┌──┘ │ │ read line │
┌─┴──┐ └─┬─┘┌─/─┐ ┌/──┐ └───────────┘
│ $0 │ ┌────────────┘ │ 1 │ │ + │
└────┘ ┌─┴─┐ └───┘ └┬─┬┘
│ 1 │ ┌────────────────┘ │
└───┘ ┌──┴──┐ ┌────┴┐
│ fib │ │ fib │
└──┬──┘ └──┬──┘
┌──┴──┐ ┌──┴──┐
│ - │ │ - │
└┬───┬┘ └┬───┬┘
┌┴─┐┌┴┐ ┌┴─┐┌┴┐
│$0││1│ │$0││2│
└──┘└─┘ └──┘└─┘
(2024年4月1日修正、追記)
リテラルに論理値を追加したことでフィボナッチのコードを修正しました
if0
とifn0
は条件式の結果が0かどうか、で判断しています(これはこれで便利)
if
は他の言語と同じくtrue
/false
で判断します
既存の等価演算に対して破壊的変更になりましたが、作者にPRをお出ししたところ通りました(PR)
(2024年5月10日修正)
クロージャとクォートが分離したため、クォート内のスコープの変数空間は外部と独立することになりました(上位スコープであっても変数を参照できない) 代わりにクロージャブロック(ブロックプラグの接合子として/
)を用いることで、外部スコープから変数空間をキャプチャして$0
を上位から参照する実装に修正
修正PR