— Baku⌇麦 (@_baku89) 2024年7月24日
Sainaさんとちょっと話したりもしましたが、色々思うことあるので書きたいなと思いました。
が、どうにも私の語彙が圏 (あと Haskell) に寄りすぎていて丁寧に説明するのがほぼ不可能でした。
なので、ある程度は諦めつつ、事例ベースで書いてみます。
あと文章にするのも難しかったので、箇条書きスタイルで書きます。
本当に雑多ですが…
Sainaさんのツイート
独り言メモ
— Saina(さいな) (@SainaKey) 2024年7月25日
う~ん、突っ込んでくれる人がいたら突っ込んで欲しいです... pic.twitter.com/R7UB41FPZP
題材にちょうど良いツイートがあったのでそこから考えてみます。
コードベースとノードベース
- 厳密には「手続き型 (imperative)」と「宣言型 (declarative)」の対比だと思います
- ここで paradigm が違うと翻訳しにくいのはそれはそう
プログラミングとは何なのか
- 別にコードを書かずとも、「求める動きを実現する手段がある」ならそれで十分だと思う
- ただノードにはノードの、コードにはコードの利点がある
- ノード: 位置による演算の関係性の表明
- コード: 編集のしやすさ、閲覧性の高さ
- それを両立できるなら確かに嬉しいだろう
- ただノードにはノードの、コードにはコードの利点がある
- non-programmer と programmer の違い
- 「道具を作ること」かなと
- non-programmer でも複雑なアニメーションを作ったりはできる
- でもプログラミングによってそれをシンプルに記述できたりする
- 道具を使って道具を作ることができる → composable な世界
- 単純なところから始めて世界を広げていくことができる
- そういう「道具の合成」ができないと自由度の高い世界とは言えない感覚がある
- 「道具を作ること」かなと
関数型言語とは何なのか
- この言葉自体はちょっと未定義語っぽくて、やはり「宣言型」を指しているかなとは思う
- 第一級関数とかはむしろノードベースと相性が悪そう (後述)
- Apply はいいけど Lambda をどうする?
- 第一級関数とかはむしろノードベースと相性が悪そう (後述)
- 手続き型と宣言型、どっちが理解しやすいのか
- 私は宣言型だと思っている
- メモリや状態の概念が無い
- 代わりに契約の概念が必要になってくるけど
- 「誰かがよしなにやってくれる」ことを許容できるなら大丈夫だろうか
- 現実的には手続き型が流行っている
- コンピュータの動作がそうであるから、が1つ
- そして「私達には時間の感覚があるから」がもう1つの理由か
- 時間の概念があると厄介であるというのは composability をわかってる人しかわからないかもしれない
- ある程度の手続き性は、確かに理解しやすいことではあるだろう
- フローチャート程度なら
- 現代のプログラミングは多層構造になっていると思う
- primitive な部分は手続きで書く
- module 単位では宣言的に書ける
- それを組み合わせて最終的に実行するときは手続きの気持ちで書く
- → 範疇が違う以上、違う paradigm を採用するのは自然なことかなと思っている
- 私は宣言型だと思っている
実行ピンによる副作用の表現
- 現代における典型手法として認識している
- 結局のところこれは Monad の類似物だと思っている
- ちなみに Sequence の挙動 (実行が終わると最後の Sequence に戻って来る) には納得していない
- callback stack があり、そしてそれに対する操作が暗黙に挟まることで実現されているはず
- 進む線がないと暗黙で ret になるのがなんだか (Monoid 解釈を考えると) おかしいと思う
- 理論的に美しくなくともまぁ現実は、というのはそれはそれ
- これなしで自由な副作用が表現できるか?
- まぁ副作用はもっと自由なものなのでこれでも不十分に感じているが
- declarative に全てが記述できるか?
- 理論的には Yes なわけだけど、例えば Haskell は強い型システムの加護があることによって成立している感はある
- 「プログラミングの扱う範疇」が根本的に普通の言語と違う感じがする
- 理論的には Yes なわけだけど、例えば Haskell は強い型システムの加護があることによって成立している感はある
- predefined なオブジェクトを宣言する形式はなんだかモデル (モデル理論とかの方) を考えている感じがありますね
- オブジェクトを差し替えてもきっと動くのでしょう
- 内部の言語からは外側にアクセスできないだろう
- 良い点: 違う領域が違う言語で記述されている
- 悪い点: composable ではない
- 私は reactive programming を実は通っていないけど、「値と値に線が引いてある」くらいの意味ならそれは declarative な感じがします
- Houdini の HScript は reactive programming の一種だろうか?
- それで済む領域は結構あるでしょう
- 本当にやりたいことは何なのか?
- 例えば
x++;
がやりたい、というのならもうそれは imperative にするしかない - だけど
x++;
を行う目的が「数を数える」なら、それは declarative にできる可能性がある - 言語は目的に応じて設計して良い
- 例えば
declarative な枠組み
- 宣言するもの、というのはまぁ何でも良い
- だから declarative な programming というのは自然に範疇が広くなると思っている
- 逆に言うと逐次実行は狭すぎという気持ちがある
- DSL (domain-specific language)
- 書きたいことを記述し、後になってそれを解釈する、という枠組み
- declarative な言語だと自然に発生する感じがする
- 副作用でももちろん良いし、それ以上でも良い
- Expression Templates はその一種だったような気がする
- 式の解釈を後から組み立てられる (ことで高速化を図る)
- Forward mode 自動微分の簡易な実装とかも結構こんな感覚がある
- Free Monad 周辺 (文法と解釈の完全な分離が自然に行われる)
- Expression Templates はその一種だったような気がする
- 書きたいことを記述し、後になってそれを解釈する、という枠組み
- ブロック線図ってかなりDSLっぽいなって思う
- 微分方程式も declarative な挙動記述だろう
- 物体挙動を
x += v * dt;
で書くよりもdx/dt = 1;
で書いたほうが「正しい」のではないか?- ランタイムが解いてくれるんだろう
- 実際そんな言語があった気がするけど忘れてしまった
- あなたは毎フレームの挙動が書きたいのか?
- 本当に欲しいものは
Update()
か?
- 本当に欲しいものは
- 物体挙動を
- 方程式を書いたら勝手に解いてほしいと思うことが屡々ある
- 線形なら解けるし四次方程式なら公式がある、勝手に使ってほしい
[a,b] = the_solutions_of(x^2 - 8x + 15 = 0);
- 更に言うと
make_sure(a^2 - 8a + 15 == 0)
で勝手にa
が定まると面白そう- 非決定性計算で似たようなことはできる
- 拘束条件 + Solver とかも似たような感じ
- そんなことを考えていたら書かれていた scrapbox.io
- isomorphism があれば動く!
- この辺は lens の気持ちがあります
- 「行き戻り」を表現する構造
- 最近どうなってるんだろ… 私は6年前で知識が止まっています
- この辺は lens の気持ちがあります
- 実装がUIレベルまで至っていて素晴らしすぎる
- isomorphism があれば動く!
- 線形なら解けるし四次方程式なら公式がある、勝手に使ってほしい
- Houdini DOP
- 動的シミュレーションを記述するネットワーク
- モノを作るのではなく挙動を組む
- 1ステップのシミュレーションを複数の microsolver を合成して作る
- microsolver は「状態を受け取って状態を返す関数」だと思っている
- endofunction (A → A) は関数合成に関して Monoid を成す → merge が可能
- なんだか「上下が逆転している」感覚がある
- ジオメトリ目線で見るとそうなる
- 実行ピンが左右反転しているのと似たような現象だと思う
- microsolver は「状態を受け取って状態を返す関数」だと思っている
- State ベースの挙動記述としてよくできてるな~と思う
- 動的シミュレーションを記述するネットワーク
- アニメーションとか
- キーフレームが主流みたいだけど composable でないのが私は気に食わない
- 複数の micro-animation を合成することで大きな animation を作るということがしたい
- 加算合成や乗算合成があるだろう
- 複数オブジェクトに対し、index によって delay 量を変えることで「順番に animation する」という animation を合成したい
- 何なら time を弄りたい
- 「時間を受け取って値を返す関数」と見るならこれは自然な発想
- 自然な発想がキーフレームという実装側の都合によって制約されていることが気に食わない
- キーフレームは曲線を有限データに収める為の手段の1つというだけ
- 評価が amortized O(1) になる*1のは便利
- でもこれは実装側の問題
- 少なくとも exponential が自然に表現できないのはおかしい
- キーフレームは曲線を有限データに収める為の手段の1つというだけ
- 未来が確定しているなら最初から未来を全部記述させてほしい気持ち
- 微分方程式記述と組み合わせることもできるだろう
- 一次元的記述 (メソッドチェーンでできる範疇) は狭いかなと思っている
- 実数上の演算のほとんどが使えるはずなのに (ただの関数なら)
ノードベースとは何なのか
- 逆にコードベースは何なのかというと… 直列化と入れ子を基礎とした表現技法?
- 私はノードネットワークは string diagram の一種だと思っています
- 自由モノイド圏 を考えているということになるのかな
- 多入力多出力が自然に表現できる枠組みなイメージ
- 単なるDAG (有向非巡回グラフ) とほとんど同じではあるけど、こちらは代数的操作に基づいている
- 例えば traced monoidal category にするとループ(再帰)が表現できる
- 「線が交差する」場合はちゃんとそこに「交差させる」演算子が必要になる
- これがノードベースとして嬉しいかどうかは別、ただ面白い話題はある…
- ある種の「グラフの正規形」みたいな印象もある
- 結び目理論だと結び目を「計算」するために一度こういう代数的表現に落としたりする (braid による表示)
- 自由モノイド圏 を考えているということになるのかな
- ノードでラムダ式を表現できるか
- 1 単純にデカい制御構造の一種としてラムダを導入することはできそう
- だけど「特殊な構文」になってしまう (まぁそれで現実的には十分だろうが)
- 2 「逆向きの矢印」を許容するとラムダ自体もノードにすることができる
- 関数適用すると、与えた引数がラムダの上側から入ってきて出力へ向かうイメージ
- 3 しかしその argument-dependent な領域の値は好きに「外」に出てはいけない
- 逆に言うと、argument-dependent な領域の値を使おうとするとそこも argument-dependent になる
- コンテキストが伝染する
- これは async/await の伝染とかと似たような話
- そのために argument-dependent 性を可視化しなければならない
- 逆に言うと、argument-dependent な領域の値を使おうとするとそこも argument-dependent になる
- 私が言いたいのは「ノードベースに領域の概念を持ち込むと表現力が上がる」のでは、ということ
- Functorial boxes in string diagrams とか圏論側の話も (いっぱい) ある
- 現在の例は ] 函手の話 (即ち Reader Monad の話)
- Houdini の Block Begin/End はこれで解釈できる
- なので最初に見たときに感動しました
- List 函手みたいなイメージ (
a → b
の演算を[a] → [b]
に持ち上げる) - (ちなみに Monad は関係ない)
- 領域の外から中に値を入れることはできる (tensorial strength) が、出すことができない
- 領域の線と値の線の交差は明確にチェックされるということ
- 1 単純にデカい制御構造の一種としてラムダを導入することはできそう
- Reader Monad は可換 Monad
- つまり「値を読み取る順序は関係ない」という世界
- ここでは読み取る値は定数なので順序無関係
- そうするとわざわざ構文 (視覚表現) を考えなくても変な現象を起こさない
- Houdini の time-dependent 性とかはコレに近いものな気がする
- ノード空間を3次元にすると紐の交差が無くせるので順序交換を自然に表現できる説を昔考えたことがある
- 「コンテキストを読み取る」は結構重要な操作なんじゃないかなと思っています
- 当たり前のようにあるものだけれども
- 時間、引数、その辺りが全て統一した枠組みに乗るべきなんじゃないかと
- Houdini はかなり良い線行ってるように見えます
- つまり「値を読み取る順序は関係ない」という世界
- 実はプログラミングは2次元で行うべきものなのかもしれないし、3次元で行うべきものなのかもしれない
- どっちにしろテキストは不十分だなあと思う
- もしかしたら双曲空間で行うべきものなのかもしれない
- 木構造が自然に埋め込めるので
高次元ノードネットワーク
- ノード自体の次元を上げてみる (string diagram 的にはただ1次元上げるだけ)
- つまり「ノードネットワーク」と「ノードネットワーク」の関係性を表すものが出てくる
- それはノードネットワークの「編集」なのかな、と思った
- 「1つのノードが2つのノードになる」という変化を表す青色のノードの図
- 即ちノードネットワークの差分を表すノードを考えてみる
- 歴史管理
- 例えば Git はテキストによる serialization が前提の処理を行う
- 対してノードネットワークの変化は (空間的に)「局所的」なので、これに適した「差分」の概念があるはず
- 同値性の表現
- 同じ処理を書く方法は複数存在する
- それらが同じであることを、ノードによって表現する
- 例: 最適化前の動作と最適化後をどちらも保持しておくことができる
- 多段階計算
- 「ノードを生成するノード」を自分で記すことができる
- ネットワークをノードを用いて自動生成できるかもしれない
- 歴史管理
- 別にテキストでできないことも無いだろうけど、ノードの持つ「空間の次元」が本質的な例ではあると思った
私の考えた最強の言語
- 今まで書いたものはできたら面白いなあと思うだけで、何も実装とかには手をつけられていない
- けどいつかやりたいとは思っている
- 私なら副作用をどうするだろうか?
- 少なくとも declarative になるだろう (これは利点がありすぎる為)
- monadic action をそのまま1つのノードとして表現するだろう
- その糖衣構文として functorial box に似た構造を使えるようにするだろう
- それは本当に便利なんだろうか、よくわからない…
- Monad の join がうまく (労力無く) 記述できるのかどうかがわからない
- それは本当に便利なんだろうか、よくわからない…
- Effect System までまとめるのも無いわけではない気がする
- が、システム側で用意した primitive としての態度はなんだか違うと思う
- そういう複雑な世界自体を作ることができる環境を作りたい
- そんなに副作用を進んで書きたいと思っていない
- 出来る限り副作用が見えないようにできるならそうする
- Applicative スタイルとかは感覚として近い
- (アクションの自然な合成があるという解釈)
- Applicative スタイルとかは感覚として近い
- 出来る限り副作用が見えないようにできるならそうする
- 全ては composable であってほしい (祈り)
- 合成するという操作が自然に行える環境であってほしい
- 現実的にはこの辺の話はさっさと終えてライブラリ充実をするほうが実用的である
- まぁそれもやりたいことではある
- 私達の行う「記述」というのは本当に自由に作って良いものなのだ、ということがもっと広まってほしい
- この話がSainaさんに役立つだろうか?
- そんなことはないと思います
- 私の中にあるものを外に出しただけですね…
- 提案: paradigm を考え、実例を大量に考えてうまくいくかを検証する
- 結局使うときのことを考えないとなかなか考えが先に進まないような気がしています
おわり。
*1:ランダムアクセスすると O(log n) 掛かると思う