WIP:俺言語2
方針
インクリメンタルに開発
メタプログラミングしやすく
一般的な文法っぽいけど、Lispっぽいマクロの両立
(静的検証を自由にできる)
(少ない機能の組み合わせで多くの機能を表現する)
(俺しか使わないから、少ない機能だけをメンテするだけで済ましたい)
構文
code:text
# 関数呼び出し
func arg1 arg2
func1 arg1 (func2 arg2 arg3)
(func
arg1
arg2)
# 配列の作成
Array.new 1 2 3
# 環境定義
name = 1
this.name = 1
this が現在の環境を意味するようにする
ピリオドなしの関数呼び出しは、現在の環境に対してピリオドをつけて呼ぶのと同じ
# ブロック定義
{}
{+ 1 1}
{
set adder 1 1
+ x adder
}
作成時の環境の参照を引き継ぐ
# Code 定義(:: は仮仕様)
Code::Symbol.new aaa
Code::Command.new add 1 2
Code::Block.new (add 1 2) (add it 3)
# Code 定義マクロ
code symbol
code (add 1 2)
code {
add 1 2
add it 3
}
# Code 定義マークマクロ
:symbol
:(add 1 2)
:{
add 1 2
add it 3
}
# Code 実行サンプル
set x 1
code x
it.run // 1
code add 1 2
it.run // 3
# Code 環境変更
set x 1
set code :x
code.run this // 1
code.run (env x 2) // 2
# 無名関数
Function.new x y {}
fn x y {x + y}
add = fn x y {
x + y
}
#1 2 3.map (fn x {x * 2}) # メソッド
ほぼ関数と同じだが、メソッドは this を使える。
this は定義元の環境にアクセスできる。
a = 1
double_a = method { this.a * 2 }
# マクロ定義
fn add :x :y {
add x.run y.run
}
fn unless condition :arg1 :arg2 {
if (not condition) arg1 arg2
}
fn unless condition :arg1 :arg2 {
CodeCommand.new :if condition
it.push arg1 arg2
it.run
}
fn unless condition :arg1 :arg2 {
:[].push :if condition arg1 arg2
it.run
}
下記は一旦なしで良さそう
fn unless condition :arg1 :arg2 {
}
# 配列定義
Array.new 1 2 3
# マップ定義
Map.new 1 2 3 4
# 中置演算子の記号定義
define_infix + left right {
left.run.+ right.run
}
define_infix = left right {
set left.text right.run
}
# 中置演算子の処理定義
class Int {
+ = method other {
add this other.to_i
}
}
# 即時評価
メソッド呼び出ししか構文が存在しない
immediate 1
immediate x
$x // これに記号を消費するのもったいないので保留
# オブジェクト
プロパティは露出させないが、オブジェクト自身でプロパティは自由に弄れる
外部からはメソッドを通じてのみプロパティを弄れる
→ プロパティとメソッドを完全に区別している
→ Ruby がまさにそう。インスタンス変数に直接アクセスすることはできない。必ずメソッドを通して取得している
→ Ruby はプロパティアクセスはなくて、必ずメソッド呼び出しになる
→ private / public にちょっとだけ近い
# オブジェクトの定義
obj = Object.new {
name = "kekemoto"
age = 124
public greet = method { "私は $(this.name) です。${this.age}歳になります。" }
}
obj.greet // => 私は kekemoto です。124歳になります。
obj.name // => private なプロパティにアクセスしているのでエラー
# オブジェクトの委譲
参照渡し
obj2 = Object.new {
delegate obj obj.props
public debug = method { "name = $(this.name), age = $(age)" }
}
# Proxy
async_obj = Proxy.new obj {
get = fn obj name {
data = obj.send name
if data.thread_safe? {
data
} else {
wrap data
}
}
}
# オブジェクトへの動的なプロパティの追加
あり。静的型付けは TS のようにすれば良い。
# クラス定義
クラスとは、オブジェクトのコンストラクタである
Human = Class.new n w {
name = n
weight = w
public eat = lm food {
weight += food.weight
}
}
# インスタンスの作成
human = Human.new "kekemoto" 50
# クラスへのモンキーパッチ
追加は許容するが、変更、削除を禁止する。
更に影響範囲を定義したスコープのみに限定する。
プロパティを追加するのは、スコープを抜けた時に戻す処理が大変&メリットが想像出来てないので保留
User.add_method blank? {}
# クラスの委譲
class Musician n w {
human = Human.new n w
delegate human "eat"
public play {}
}
文字列で動的に指定出来ちゃうと、静的に困る
# クラスの継承
class Musician n w {
human = Human.new n w
delegate human human.props
public play {}
}
# エラー
class Error m {
message = m
return_trace = []
}
# Null
class Null {
error = Error.new "Null"
delegate error error.props
}
パラダイム
コンパイル時に自由に動かせる必要があるので動的型付け
動的型付けで変更に強くするならオブジェクト指向が良い(データ構造を隠蔽してメソッドのみでやり取りする)
殴り書き(オブジェクトのアクセス)
Ruby のように全てメソッド呼び出しにする
代入をどうするか
= を使う場合、どんな構文の解釈すれば良いのか分からん
普通に中置演算子のままで、上書きしたい場合はプロキシを使うか?
set_hoge にするか?
スコープとしての扱いどうするか
オブジェクト内では自由に読み書きできるから問題ない?
getter とプロパティアクセスの違いは?
this 問題をどうするか
定義元のスコープを見るように固定したら良さそう
固定したらメソッドをデータとして扱っても問題なくなるか?
構文的に普通に呼び出すとメソッド呼び出しになるわけだから、あんま気にしなくて良いか?メソッドをデータとして代入した時にエラーでも出すか?
殴り書き(オブジェクトとは)
データ(構造)とメソッドがセットになったもの
動的にデータ構造を変更できるので、クラスのようにデータ型で識別する意味がない。
あえて言うなら全部オブジェクト型
データは隠蔽し、データの責任をオブジェクトが持ち、メソッドだけをインターフェースとして露出させる
継承とかプロトタイプとかいらない
マージとかコピーとか埋め込みとかすればいい
クラス自体もオブジェクトで作る
殴り書き(Ruby のオブジェクト)
全てがメソッド呼び出し
セッターは問題だけど、無理くりできそう
プロパティアクセスが @ みたいなの必要そう
それこそ this みたいなのでいいか?
上記の色々から構文が複雑になりそうだし、余程上手く設計しないと自然にならなそうなので止めたい。Ruby すごい。
殴り書き(メソッドのデータ化問題)
this にオブジェクトを束縛する案
クロージャーとかそうだし、メソッドじゃなくても使えそうなルールで良さそう
ドット呼び出しで this を変える。JS パターンなのでダメ
殴り書き(エラー)
例外処理はなし。return で返す。
データとしてはクラスかオブジェクト
データ構造としてはメッセージと return トレース
メッセージいらんやろか?
殴り書き(スコープ)
プロトタイプ継承するオブジェクトみたいな感じ
ブロック作成時にその時のスコープの参照を含める
ブロック実行時には明示的にどのスコープで実行するかを指定する
ブロック作成時のスコープを継承した新しいスコープにする(クロージャーなど)
ブロック作成時のスコープの延長とするか(if 文のブロックなど)
別のスコープを持ってくるか(クラスや、メタプログラムなど)
(スコープの破棄はGCに任せるか、参照カウンタ)
殴り書き(JS の Proxy)
Proxy みたいなのがあれば、プロトタイプ継承も、並列用スコープも、クラスも、色々なものが表現できそう
Object 自身のメソッドとして get, set を定義して、これを上書きする形でプロキシれそう
= は Object のプロパティに値をセットしているだけ。(変数宣言に見えてもスコープになっているObjectにセットしている)
殴り書き(継承)
継承で本当にいいのか?
ミックスイン
別のオブジェクトに定義してある関数を取り込む
super が呼べなくなる
委譲を中心としてDSLを充実させる
これいいな
継承より柔軟性も高そうだし、多重継承だろうと問題起きないし。
記法を簡単にできるかどうかかな
委譲の機能で継承作ってもいいし
殴り書き(マクロと関数)
io のように引数の評価を遅延させるけど、普通に引数を使った場合は普通に評価される形式にするのはあり。 関数の引数宣言になんかプレフィックスつけても良いかも
殴り書き(モジュール、名前空間)
後からの拡張でできるか?
環境とモジュールを同一にしても良いかも
環境を1つ作って、その環境のデータを代入していって、好きな環境を構築する
メインの環境から他の環境もマージできる
名前の重複などの解消になってない。
js のインポートみたいにするか?
オブジェクトと環境とモジュールと名前空間を同じにできるかも
振る舞いを共通にして、実装は分けた方が良さそう
殴り書き(コンパイル時に自由にできるプログラム)
要件
コンパイル時に動く
静的情報を自由に扱える
やってみたい
四則演算
型システム
所有権
多重ディスパッチ
不変保証
例外の網羅
副作用
メモリの破棄
シンボル、コマンド、ブロックごとにメタデータをつけるか
殴り書き(エディタ補完)
Ruby の REPL で出来るなら実行しちゃえば良いんじゃないか?
殴り書き(メタデータ)
実行中のデータは普通にプロパティに持たせればいい
コンパイル中にアクセスできるものをメタデータとする
殴り書き(並列処理)
フリーズ(不変)設定
並列用だけであればプロパティの追加は可能にするか?
並列用スコープ
フリーズしてあるオブジェクトは参照可能
フリーズしてないオブジェクトはラップしてあって直接参照は不可能
並列を考えてないスコープがあるが大丈夫か?
ロックはできそうだが、STMはできなさそう
ラップもせずただ禁止にしても良いかも
ディープコピーでも良さそう
ダーティリードもありか?
クラスすら最初からフリーズできない
モンキーパッチしたいから
並列に使うときにディープコピーしてしまう
並列処理前にフリーズさせておく
並列処理できないからモンキーパッチを使わないマナーができそうなのが嫌(杞憂か?)
モンキーパッチの項で考える
スコープからデータを参照するときに判定する(遅延させる)
Go のチャンネル、JSのプロミス、JSのAsync
並列用スコープでいつでも同期/非同期実行可能
呼び出し側で選ぶのか、呼び出される側で選ぶのか
並列スコープの特殊性から、呼び出される側で選ぶしかない
返り値の受け取り方を同期的にするか、非同期的にするかは選択できる
非同期処理の返り値はプロミス(チャネル)になる
実行しないで良い時がわかるか?
プロミス(チャネル)の受信待ちしているタスクは無視してよい
スリープや、ネット通信はプリミティブな所で受信待ちする
タスクの粒度
ネイティブスレッドとグリーンスレッドの多対多
メモリは共有している
切り替えタイミング
殴り書き(スケジューラー)を参照
タスクの中断
連鎖的な中断
エラー処理
return で返すだと受け取らない可能性がある
wait を強制させる?
失敗時のコールバックを強制させる?
放りっぱなし?
future でもデッドロックする
プロミス vs チャネル
並列用スコープとスケジューラーさえ決まれば、両方ライブラリで作れそう
プロミスの方が簡潔
チャネルの方が表現力はスゴそう
ジェネレーターとか、遅延ストリームとか、コルーチンとかできそう
クロージャーでジェネレーター作れるっぽい
チャンネルは後から作れそうだが、スケジューラーに組み込むとなると大変じゃないか?
競合データの扱い
不変データ
バージョン付け
Rust の読み込みだけならいくつでも参照可能と同じ
ディープコピー
大体はこれでええやろ。
複数からの書き込みをどうするか。
スレッドセーフなデータ構造を使う
プロミスで上手いことやる
複数のプロミスを受け取って、必ず一つずつ処理できるようなやつとか
移動
Rust の書き込みできる参照は1つだけと同じ
STM
並列を気にしないスコープがあるので難しそう
cas
アトミックなデータ型
ロック。問題が多い
殴り書き(スケジューラー)
タスクのキューを用意する
タスクの追加は後ろからやっていく
実行順序
先頭から待機してないタスクを実行していく。
最後まで行ったらまた先頭から実行する。
タスクがなくなったらプログラムの終了
タイムスライスなし
タスクが終了するか、待機中になったら次のタスクにいく
待機中かの判断方法
待機中となるシステムコールなどの関数のメタデータに書き込む
コンパイル中に対象のメタデータを読み取ったら、切り替えの関数を仕込む
殴り書き(言語の名前)
FL : Flat Language
cella : CELL LAnguage(意味わからん)
statica : 静的検証を自由にできるから
BLS : Block Line Symbol
linp : LINe Processor
アーカイブ
中置演算子
ドットがあるので禁止するのも今更
優先順位はやりたくないが、代入とドットをやるなら必要になる
クラス外で中置演算子の定義は禁止
引数2個以外は禁止
中置演算子よくない
code:text
show 1 + 1
[show 1 1]
1 + 1
1 1
show 1 + show 2
[show 1 show 2]
殴り書き(メソッドチェーン)
it で頑張る
clojure の thread マクロみたいなの作る
これ作れそうだから後から考えるで良さそう
haskell の $ みたいにする
パイプライン演算子みたいにする。
Haskell の do 記法みたいなの
メソッドチェーン?現状だと解析できない
code:text
# ドットの意味をパイプラインに
1 . add 2 . add 3 . add 4
Array . new 1 2 . push 3
下記ができない
print User.new.name
# メソッドチェーンは出来なくていい
データに対してメッセージを送ることは必要
data . message args
左に対して右のメッセージを送る中置演算子
殴り書き(モンキーパッチ)
モンキーパッチがあるとクラスをフリーズできない
並列スコープで直接使えない
モンキーパッチの功罪
Rails の blank? は最高
仕事でも結構使ってる
外部のライブラリになんらかのバグがあるがそれが修正されたバージョンがリリースされていない場合
それは元コードをコピーして修正してローカルからロードすべきな気がする
Rspec でもモンキーパッチがあって苦労した話があったけど、詳細はあんまり語られてなかった気がする
「Object に except を追加した件はゴメンね」って作者が言ってた気がする
モンキーパッチに制限をかける
Ruby の refinements みたいな
オリジンのクラスはフリーズできる
モンキーパッチ後と前で別のデータにする
認知範囲外まで変更できるから良いのか?
制限かけるなら最初から継承で拡張すればよくね?
明示された特定のスコープでのみ、クラス名で参照するデータが入れ替わる
これであの悲劇を防げるのか?
無意識に入ってしまうことはない気がする
それが本当なら良さそう
明示するときにクラス名も指定するか?
フレームワークとかだと無意識に入りそう
フレームワークなら無意識でも良い気がする
ほんとうか?
既存のプロパティを変更、削除するモンキーパッチを禁止する
並列スコープに関しては解決しそう
無意識に依存することもなさそう
悲劇のサンプルが少なすぎる
でも追加のみ許すのは筋が良さそう
追加のみとスコープの制限の両方適用で良さそう
mathn も妥当なレベルまで軽減される
Rspec みたいな話もなくなりそう
仕事で気軽に使えるのはライブラリ開発じゃなく影響が限定されてるから
欲しかったらロードすればいいし
blank? や仕事で使ってるやり方もできる
でも特定のメソッドをフックしたいとかありそう
cas とかのバージョン管理とか
いやそんな副作用バリバリなの許さない方が良さそう
後からキツくするより、後から緩める方が簡単
関連リンク
バイトコード
Ractor
スケジューラー
影響を受けた言語
tcl
Lisp
io, Ruby, Go, Zig
その他色々