今月の活動 (ミローネ言語、ジェネリクス引数のカッコの話など)
- 前回 (2020-12-31) https://vain0x.github.io/blog/2020-12-31/diary/
ミローネ言語
https://github.com/vain0x/milone-lang
先月から引き続き、初回リリースを目指している。(厳しい。)
ML: クロージャ変換の計算の改善
コミット: feat: Compute transitive closure of upvars
関数の upvar (キャプチャしているローカル変数) を計算する部分がいまいちだったので書き直した。いろいろと余計なことをしていた。
ML: String のヌル終端要求の緩和
コミット Merge: Change string invariants
String は必ず直後に NULL 文字が来るという不変条件を課していたが、これは撤廃した。この条件がなくなることにより、文字列のスライスを取る操作 (s.[l..r]
) が高速になる (ポインタと長さを再計算するだけ)。
ML: アリティ検査
コミット: feat: Check arity more
関数のアリティ (引数の個数) の違いのせいで不正なコードが生成される不具合があったので、ひとまずそれを検出してコンパイルする機能をつけた。
関数のアリティを考慮した型のようなもの (ArityEx) を考えて、型検査のようなものを行って、unification が失敗したらエラーにする、という手法。
値をコピーするときにコピー元の式の ArityEx とコピー先の型の ArityEx が一致しなければエラーにしている。
// ArityEx の定義
A = _ // 関数以外のアリティ
| * // hungry 関数のアリティ
| (A, A, ..., A) -> A // 関数のアリティ
// 型から ArityEx への変換
A(T)
// T = T1 -> T2 -> ... -> TN -> U のとき
= (A(T1), ..., A(TN)) -> _
// T が関数型でないとき
| _
# 式から ArityEx への変換
A(E)
// E が何個でも引数を適用できる関数のとき
= *
// E が関数として定義されたシンボルで、
// 引数の型がそれぞれ Ti、返り値型が U のとき
| (A(T1), ..., A(TN)) -> A(U)
// E の型を T とするとき
| A(T)
エラーにする代わりに、コピー元の式を適切なアリティを持つ匿名関数で包むことでコード生成を修復できるはず。
ML: LSP サーバーの改善
コミット: Merge: More efficient LSP server
当初は手抜きのため、リクエストの読み取りと処理を同期的に交互に行っていたが、あまりにも遅かった。
原因:
- ファイルの変更を通知する textDocument/didChange は大量に発行されてきて、そのたびに変更後のソースコードに対して型エラーを報告するために意味解析をしていた
- また (VSCode は) カーソルを動かすたびに hover や documentHighlight の発行とキャンセルを繰り返していて、サーバー側の処理が同期的なせいで何もキャンセルできていなかった
対処:
- リクエストの読み取りはバックグラウンドで行って、キューに溜めていくことにした
- キューからリクエストを読んで処理するというサイクルは同期だが、リクエストを読む前に過剰なリクエストを弾く処理を追加した
- 例えば同一ファイルへの didChange が2つ連続で並んでいたら前者は捨てる、など
- 性能はそこそこ改善した
その他:
- 無限にメモリを確保しながら停止するバグがあるようなので、バグが発生したら手動でプロセスを kill している (運用でカバー)
ML: サンプルアプリ
GTK+ を使って GUI アプリを書こうとしていて、ウィンドウを出すぐらいはできた。しかし GUI アプリは inRegion の手法でメモリを解放できないので、out of memory を回避できないことにいまさら気づいた。(おそらく次のリリースには含まれない。)
他に、HTTP サーバーを書いてみていて、html/css/js を送信することで静的なウェブサイトを公開するぐらいのものは作れた。データベース (SQLite など) に繋いでデータの永続化ができれば、いわゆる TODO MVC みたいなのは作れそう。
サーバーアプリはリクエストごとにデータベースからデータを取るものが多く、メモリ上にデータを持つ必要がないので、レスポンスを返した後にメモリを解放できる。そのためリクエストを読み取ってからレスポンスを返すまでの部分を inRegion で囲んでおけば out of memory にならない。
GUI アプリでもデータベースを使えばいいかというと、おそらくうまくいかない。GUI アプリの場合は「リクエスト」に相当するのがマウス移動のように高頻度なので、「レスポンスを返す」(= 画面を更新する、音を鳴らす、などを行う) までの応答時間の制限が厳しい。そのためデータベースに頻繁に接続する余裕はない。
また、いまさらながら自作言語の自作 HTTP サーバーをインターネットに晒すのは危険度が高い気がしてきた。C言語で書かれた HTTP サーバーのライブラリにつなぐ方針の方がよいかもしれない。ミローネ言語から JavaScript にコンパイルするバックエンドも作って、Electron のような方式でアプリを動かすのも試したい。
参考にしている本: ふつうのLinuxプログラミング 第2版|SBクリエイティブ
その他
その他: ジェネリック引数のカッコ
Go 言語のジェネリクスのドラフトをみたところ、ジェネリック引数のカッコが ( )
から [ ]
になっていた。
A Proposal for Adding Generics to Go - The Go Blog
ジェネリック引数を < >
で囲む構文は C++, F#, TypeScript, Rust など多くの言語で採用されているが、構文的な曖昧性があってパースが難しい。
例えば f<T, U>(a)
は関数呼び出しにみえるが、<
を比較演算だと思うと f < T
, U > (a)
という2つの式をカンマ区切りで並べたものともとれる。
f<T, U>(a)
(f < T), (U > a)
なお Rust ではこれに対処するため f::<T, U>(a)
と書ける (turbo-fish)。
また、入れ子になったジェネリック引数リストを閉じるときに >>
という並びが出現することがあり、これがシフト演算子と紛らわしい。
実際、昔の C++ では K<K<int>>
が構文エラーになるから K<K<int> >
のようにスペースを入れる必要があった。(現在は修正されている。)
なお自作言語のミローネ言語も (F# との互換性のため) ジェネリック引数リストは < >
で囲む。関数にジェネリック引数リストをつける f<T>
の構文はまだ実装していない。>>
の方は次のように対応している: 字句解析の際に >
は1文字ずつのトークンにしておき、シフト演算子が出現しうる位置に >
が連続で出現したらまとめて1個の演算子とみなして読む。
もう1つ問題があって、< >
をカッコの一種とするとエディタが混乱する。VSCode には、開きカッコ (
を入力したときに自動で閉じカッコ )
も挿入する機能がある。言語の設定として < >
がカッコとして登録されていると、<
を入力したときに >
が入力されてしまうので、比較演算を書くときにわずらわしい。(自動補完は設定でオフにできるが、それではジェネリック引数リストを書くときにも補完されないということになる。)
エディタからすると < >
はカッコだったりカッコじゃなかったりする妙な記号である。比較演算としての <
を捨てるわけにはいかないので、<
はカッコではないということにした方が都合がいいと思う。
そういうわけで、ジェネリック引数リストを < >
以外にする言語が増えてほしい。
ジェネリック引数リストの構文として [ ]
を採用するのにも一定のデメリットはある。添字式と衝突するので、f[T]
がジェネリック引数つきの関数 f[T]
なのか、配列 f に添字 T をつけているのか、が曖昧になる。すなわち [
の次に来るのが式なのか型なのか分からない。
Go 言語はすでに式と型のどちらが来るのか判別できない文脈があるので (make(T)
)、これは問題にならない。[ ]
を採用している Scala は、配列の添字を ( )
で囲むので問題ない。
なお自作言語のジャッコ言語では、ジェネリック引数リストが式の後ろに来るときだけ turbo-fish 形式を強制して、f::[ ]
と書くことにしている。(一貫性がないが、この構文を使う機会は多くないので問題ないと思っている。)
その他: HSP3 で長い文字列を高速に作る方法を書いた
HSP3 で長い文字列を高速に作る方法をコメントに書いた。記事にまとめた方がいいかもしれない。
その他: prepend コマンドを作った
vain0x/prepend: Edit a file to insert lines on head
ファイルの前方に行を足す作業をするとき適切なコマンドがないので、作ってみた。