今月の活動 (ミローネ言語など)
- 前回 (2022-02-28) https://vain0x.github.io/blog/2022-02-28/diary/
ミローネ言語
ドキュメントを増やした:
- 名前解決の諸概念について説明を書いた:
milone-lang/name-resolution.md
- 名前空間とスコープの区別をはっきりしておかないと混乱するという感覚があるので、そのあたりを明記した
- 肝心のルーチンの部分はまだ書いていない。構文木をたどって適切に環境を更新するだけなので難しくはない
- ネイティブコードを埋め込む機能の説明を書いた:
milone-lang/x_native_code.md
- FFIの仕組みが整うまでのつなぎだから書いていなかった
- 自分用としての価値もあるので書いた
線形型
ミローネ言語に線形型を導入しようとしていて、実装を試している
純粋でない操作の表現と式の評価順
(追記 (2022-10-22): 用語の使いかたが微妙だったため記述を大幅に変更)
背景として、純粋でない操作の表現の話を書く
(いろいろと十分でない。もっと調査をするべきだった。知るとよさそうなことがあれば教えてください:pray:)
実用的なプログラムを作るには純粋でない操作を行う関数がいる:
- ファイルの読み書きやネットワークアクセスなど
- 純粋でない操作を行う演算子やライブラリ関数だけでなく、それらをもとにAPIを構築する仕組みとして、純粋でない操作を持つ関数を定義できる必要がある
- 一方、純粋でない操作を言語にまったく含めないという案もある
- Elmは純粋でない操作をまったく含んでいない
- 純粋でない操作は、ランタイムやホスト言語にやらせる
- ランタイムやホスト言語とミローネ言語を統合する手間が発生する
- そういう言語を作りたいわけではないのでここでは採用しない
- Elmは純粋でない操作をまったく含んでいない
純粋でない関数の性質:
- 純粋でない関数は、数学的な意味での関数とは性質が異なる
- 純粋でない関数の呼び出しはちょうど1回行う必要があり、順番通りに行う必要がある
- 数学的な意味での関数なら以下のことがいえる
- 返り値を使わないなら関数の呼び出し自体を削除してもいい
- 関数の呼び出しを2回以上行ってもいい
- 関数の呼び出しの順番を入れ替えてもいい
純粋でない操作の表現:
- 純粋でない操作の表現は、式の評価順につながる (たぶん)
- 純粋でない操作があると、ある式を評価することに他の式が依存する、という状況が発生する
- 例1: 「名前を入力してください」と表示した後に入力の読み取りを行う、という処理
- 当然、順番を変えたり除去したり複数回行うとまずい
- 前者 (
print msg
) が評価されることに後者 (scan ()
) が依存している - 前者の返り値には依存していない
- 例2: 現在日時を取得してから10分待つ、という処理
- 現在日時の取得は副作用を持たない
- 実際、返り値 (日時) が使われないなら取得処理を除去することはできる
- 日時の取得は実行するたびに異なる値を返すので、純粋ではない
d = now(); f(d); g(d)
とf(now()); g(now())
は異なる
- 順番に関していえば、「10分待つ」の後に移動すると意味が変わってしまう
- 現在日時の取得は副作用を持たない
- 逆に評価順序の依存関係があれば、純粋でない操作を表現したことになる (たぶん)
- 返り値ではなく「評価されること」への依存は式が純粋でないせい
式の評価順に関する他の言語の方針:
- 評価順を固定する
- C言語、F# を含めて多数の言語がこれ
- 評価順は基本的に構文で決まる
- 評価順は基本的に式の意味に影響されない
- 関数が実際には純粋であっても、そのことが処理系からみて明らかなケースを除いて、呼び出しは削除されないし順番も入れ替えられない
- メリットは実装しやすく、オーバーヘッドも少ないこと
- デメリットは依存関係が過剰なこと
- 特定の順序で評価しないといけないのか、たまたまそういう順番で書いただけなのか、区別できない
- 一見、無害そうなコードの移動や削除でバグが起こった、というのはよくある
- 計算を合成する
- (HaskellのIO型など)
- 評価順は構文に依存しない
- 純粋でない関数は操作の結果を返すのではなく、操作を表す値 (アクションという) を生成する
- アクションは順番つきでのみ合成できる
- (F# 風に書くと)
a1 |> bind (fun x -> a2 x)
- do構文を使うと短く書ける
- (F# 風に書くと)
- アクションはあくまで関数の返り値なので、操作の順番は値の依存関係によって明確に表される
- エフェクトシステム
- (Kokaなど)
- 関数が純粋かどうかを関数のシグネチャの一部とみなす
- 線形型
- 型を線形型とそれ以外に分類する
- 線形型の値はちょうど1回だけ使える
- そのことをコンパイル時に検査する
- 例えば線形型の値を代入した変数が参照されなかったり、2回以上使われるようなパスがあったらコンパイルエラーにする
- リソース管理などに使える
線形型を使うことで、式の評価の依存関係を値の依存関係として表せる (たぶん):
let greet (io: IO) : IO = // IO: 線形とする
let io = print "名前を入力してください: " io
let name, io = scan io
let io = print ("こんにちは、" + name + "さん!") io
io
- print→scan→print という依存関係は値の依存関係で表現できている順番に決まる
- 最後のprintの返り値が関数全体の返り値なので、これらの呼び出しは除去できない
- ioは線形なので、関数はちょうど1回ずつ評価される
線形型
そういうわけで線形型を実装しようとしている
- モチベーションは純粋でない操作を表現する汎用的な方法を提供すること
- なおassertとかprintfはすでにある
- ただしコンパイラ自身の現在の実装は、関数オブジェクトが普通に副作用を起こしてたりしている
- なおassertとかprintfはすでにある
- 一定の制限をかければ簡単に実現できることに気づいた
- 多相関数の型変数が線形型になれない、という制限
- その型変数を使っている部分が線形かどうか判定しなくていい
- この制限のせいで
failwith ""
の返り値が線形型になれないのが興味深い
- レコード型は線形型になれない、という制限
- レコードはフィールドにアクセスできるので部分的に使われた状態になりうる
- タプルや判別共用体はパターンマッチングで分解する以外の使用方法がない
- 変数が使用済みかどうかマークするだけで済む
- 線形な判別共用体があれば線形なレコードはなくても困らない
- 関数型は線形型になれない、という制限
- もし関数 (クロージャ) が線形型のローカル変数をキャプチャしていたら次のような作業が発生する
- 関数オブジェクトの構築に際し変数を使用済みとみなす
- 関数の型を通常の関数とは異なる型 (
T -o U
) にする
- クロージャ変換と似たような処理が型検査の時点で発生する
- 線形な関数は当面のユースケースでは使わない
- もし関数 (クロージャ) が線形型のローカル変数をキャプチャしていたら次のような作業が発生する
- 多相関数の型変数が線形型になれない、という制限
- ファイル:
- ちなみにミローネ言語で試す前に別のプロジェクトとして仮実装した: languages/linear
- 前述のレコードなどに関する問題もこの時点で気づけたので意味はあったはず
Reactのデータの内部表現をクラスで隠蔽するサンプル
playground/2022-03-19-react-using-class
- Reactのデータをプレインなオブジェクトで持つと、いたるところで操作できて手に負えない、という話をみた
- クラスを使えば内部表現を隠蔽できるはずだと感じて、サンプルを書いてみた
- サンプルの題材選びがいまいち
- これだとクラスを使うメリットがあまりない
- useReducerで十分にみえる
- めんどくさいが可能なことは分かった
- C#+WPF で似たようなことをやっていたのを思い出した
読んだ記事など
- プログラム入門:抽象化の話 - なーんだ、ただの水たまりじゃないか (2019-06-12)
- 抽象を作って問題領域の言葉で記述するという方法の話。いい解説な気がする
- No Ghosts! · sunfishcode’s blog (2022-03-17)
- グローバルなオブジェクトによって解決されることを想定した参照を他のシステムに渡すようなAPIはよくないという話
- 参照を解決するのに使うオブジェクトが明示的に出現しないことをもってゴーストと称している