ニート終わりにS式でErlangライクなプログラミング言語作った

はじめに

会社を辞めてからそこそこの期間ニートでいたのですが、そろそろ社会復帰が必要になってしまいました。
貯蓄的な意味で。

だけれども何の成果もなくニートを終えるのは寂しいので、プログラミング言語を作ることにしました。

で、実装が一段落している訳ではないですが、ニートイムリミットに合わせて紹介記事を書いているところです。

プログラミング言語 Navi

Naviという名前の言語を作っています。
名前の由来は秘密です。
Githubにあります。

f:id:aharisu:20220124174122p:plain:w150

Hey!!



特徴

  • Rustで実装。
  • 構文はS式を採用。
  • 機能はだいたいErlang
  • オブジェクトの概念がある。
  • 暗黙的なFutureを持つ。

サンプルコード

だいたいこんな感じ

(let obj (spawn))

(object-switch obj)
(def-recv "Hey" "What's up?")
(return-object-switch)

(let result (send obj "Hey"))
(print result) ; => What's up?



Rustで実装

まとまった時間が取れるので勉強も兼ねて、一切触れたことがなかったRustで実装しました。

勉強期間は3日ほど。
書いたコードの量0でいきなり言語の実装に取り掛かってしまったので、最初の方はボロボロだったと思います。
そんな訳で苦労したところは多かったですが、Rustはとてもいい言語ですね。

ある程度アプリケーションとしての下地が整ってくると、rust-analyzerが提示してくれる型から自分が目的とする型へ、関数を通して変換を繰り返すと機能の追加が完了しています。
なかなかにHaskell味も感じられて書き心地が良いです。

Rustといえばの部分で、ボローチェッカとライフタイムによるエラーは今でも頭が痛くなることが多いです。
ですが基本的にはコンパイラの指摘が正しい訳で、素直に従った方が良いということで落ち着きました。
最悪unsafeで逃げられますし :-)

構文はS式を採用

いわゆるLispの構文です。

言わずもがなカッコを多用しています。

構文としてはLispのS式を採用しましたが、機能的にはLisp的要素はほとんどありません。

Lispに飢え、Lispを求めている方には満足いただけないと思います。

Not Lisp yeah.

機能はだいたいErlang

Erlangは並列・分散指向のプログラミング言語です。

多数のプロセスがメッセージのやり取りで協調しながら動作するところが非常にかわいらしい。

そんな言語を作りたかったので、NaviはErlangを参考にしている機能が多いです。

オブジェクトの概念がある

Erlangでいうところのプロセスを、Naviではオブジェクトと呼んでいます。

Erlangのプロセス同様、Naviのオブジェクトはそれぞれ完全に独立しています。
全てのオブジェクトは非同期的に動作して、メモリ領域(ヒープ・グローバル変数)もそれぞれのオブジェクト固有のものです。

独立しているオブジェクトはメッセージングによってやり取りするわけですが、
他のオブジェクト指向言語でのメソッド呼び出しのことをメッセージングと呼んでいると思ってもらえればほぼ間違いありません。

メッセージングとメソッド呼び出しとを同一視するためにNaviのsend関数は戻り値を持ちます。
ここがErlangと大きく違うところです。

; サンプルコードのこの部分。
(let result (send obj "Hey"))
(print result) ; => "What's up?"

もちろんsend関数はブロッキングなしで処理を返します。
この仕掛けは次の特徴によって成り立っています。

暗黙的なFutureを持つ

Naviで一番特徴的な機能だと思います。
ほかの言語では、FutrueとかPromissとかDelayとか呼ばれる機能です。
send関数がFuture値を返します。

Future自体は珍しくはありませんが、一般的なFutureと違いNaviではFutureに対して処理が完了したかどうかを確認する必要はありません。
値を使用するとき自動的に確認され、完了していなければ結果が取得できるまで待ちます。

(let result (send obj "Hey")) ;; ← ここはブロックなしですぐに終了
(print result) ;; ← print関数にresultが渡される前に、完了チェック。まだ完了していなければここで処理がブロックされる

この動作はすべて暗黙的に行われるためソースコード中にFutureに関する記述は必要なく、これらが非同期で動作していることを気にする必要もありません。

普通にオブジェクトを定義して、メソッド呼び出しをしながら実行するだけで、全て非同期で動作するようになります。

オブジェクトの生成と複製

; 空のオブジェクトを作成
(let obj1 (spawn))

; obj1の中に移動
(object-switch obj1)
; グローバル変数xを定義
(let x 1)
; メッセージレシーバーを定義
(def-recv {:add-x @n} (+ x n))
; obj1から最初のオブジェクトに戻る
(return-object-switch)

; obj1を複製して新しいオブジェクトを作成
(let obj2 (spawn obj1))
(print (send obj1 {:add-x 1})) ; => 2
(print (send obj2 {:add-x 1})) ; => 2

; obj2の中に移動
(object-switch obj2)
; グローバル変数xを変更
(let x 2)
; obj2から最初のオブジェクトに戻る
(return-object-switch)

(print (send obj1 {:add-x 1})) ; => 2
(print (send obj2 {:add-x 1})) ; => 3



ざっくりとその他の特徴

  • 組込み型一覧

    • 整数 1, 2, 3
    • 実数 3.14
    • 文字列 "Hello!!"
    • Bool true / false
    • シンボル a b symbol
    • キーワード :a :b :keyword
    • 関数 / クロージャ (fun (arg1 arg2) ...)
    • Object
    • リスト '(+ 1 2 3)
      • ペアではない。
      • Lispでcdrに当たる部分は必ずリストになっている。
    • 配列 [+ 1 2 3]
    • タプル {+ 1 2 3}

  • パターンマッチを持つ

    • 実のところオブジェクト間のメッセージsend/recvは単なるパターンマッチ
(match x
  (1         :the-one)
  ((1 2 3) :list)
  ({}        :empty-tuple)
  ([@one @two] :array-bind-one-two)
  (else      :other))

(def-recv {:add-one @n} (+ n 1))
(def-recv {:add-two @n} (+ n 2))
;;上二つのメッセージレシーバーは以下のmatch式と同義
(match msg
  ({:add-one @n} (+ n 1))
  ({:add-two @n} (+ n 2)))
  • GCはCompactionとCopying

  • 完了していないFuture値かどうかの判定処理が超速い

    • ポインタの最下位ビットを見るだけ

  • プリエンプティブ方式のマルチタスク

  • 値はすべてイミュータブル

おわりに

紹介としては雑ですが疲れたので終わり。

まだ開発は続きますが、久しぶりの言語実装は非常に楽しかったです。

ニート最高!!