Closures vs Objects
It is well-known that closures and objects are equivalent. So are actors. Which one is the most fundamental? よく知られたように、クロージャとオブジェクトは等価です。アクターも同様です。どの概念が最も基本的なのでしょうか? 二変数以上の関数のほとんどは、どの引数のオブジェクトに属しているともアプリオリには言えないし、オブジェクト指向は不自然 オブジェクトと引数は別物だし、もし関数を特定のオブジェクトに属するようにしたいなら、クロージャでラップすれば簡単にできる 二変数以上の関数のほとんどは、どの引数のオブジェクトに属しているともアプリオリには言えないし、オブジェクト指向は不自然 オブジェクトと関数は別物だし、もし関数を特定のオブジェクトに属するようにしたいなら、クロージャでラップすれば簡単にできる 2変数関数をクロージャにラップして1変数関数にしても、メソッドチェーンはできないよね [1, 2, 3, 4, 5]にメソッドが属してれば [1, 2, 3, 4, 5] .filter(hoge) # ←戻り値もオブジェクト .map(piyo) # ←これも .sum() みたいにできるけど、配列がオブジェクトじゃなかったら map([1, 2, 3, 4, 5], hoge) の戻り値はただの配列なので、繰り返し関数適用するには、Lispみたいな醜い入れ子にするしかない sum( map( filter([1, 2, 3, 4, 5], hoge), piyo)) >>8-9 これでどうよ chain = (obj) => { return (f, ...args) => { if (!f) return obj return chain(f(obj, ...args)) } } chain([1, 2, 3, 4, 5])( filter, (x) => x % 2)( map, (x) =>x * x)( sum)() // => 35 一時期メタプログラミングとかいって、動的にクラスやメソッドを作れるのがすごいって言われてたけど、クロージャをデータ構造として使うならそれは自明にできることでは やり方が複数ある場合は、一番綺麗に書けるコードを採用するのが基本 入れ子にしなくても関数合成でいいのでは? メソッドチェーンっぽく書くには Reverse-application operatorってのもあるけど 個人的には関数合成のほうがスッキリしててすこ https://ideone.com/gCWxLP let hoge n = n mod 2 = 1 let piyo = ( * ) 2 let () = print_int (List.fold_left (+) 0 (List.map piyo (List.filter hoge [1;2;3;4;5]))) (* function composition *) let (<<) f g x = f (g x) let f = print_int << List.fold_left (+) 0 << List.map piyo << List.filter hoge let () = f [1;2;3;4;5] let (>>) f g x = g (f x) let f = List.filter hoge >> List.map piyo >> List.fold_left (+) 0 >> print_int let () = f [1;2;3;4;5] (* @@ Application operator *) let () = print_int @@ List.fold_left (+) 0 @@ List.map piyo @@ List.filter hoge [1;2;3;4;5] (* |> Reverse-application operator *) let () = [1;2;3;4;5] |> List.filter hoge |> List.map piyo |> List.fold_left (+) 0 |> print_int chainl = (obj) => { return (f, arg) => { if (!f) return obj return chainl(f(arg, obj)) } } cons = (x, xs) => [x, xs] foreach = (lst, f) => { if(!lst) return [x, xs] = lst f(x) foreach(f, xs) } linkedList = chainl(null)( cons, 1)( cons, 2)( cons, 3)() chainl(linkedList)(foreach, console.log)() // 3 // 2 // 1 いろいろ書けるんだな カリー化もクロージャと可変長引数があればできるね currying = (f, ...args) => (...rest) => f(...args, ...rest) これで、mapなどが map(f, col) と、関数を先にとる形でも、>>16 のchainlを用いて chainl([1, 2, 3, 4, 5])( currying(filter, (x) => x % 2))( currying(map, (x) => x * x))( sum)() と書けるわけか いや、単にchainlを chainl = (obj) => { return (f, ...arg) => { if (!f) return obj return chainl(f(...arg, obj)) } } とすればいいのか クロージャの真価はコンテキストを導入できること (let ((a 1) (b 2)) (+ a b)) ((lambda (a b) (+ a b)) 1 2) は等価 なので、上のa + bの変わりに関数を返却すれば独立したスコープの状態をもつ何かを作れる クラスベースのオブジェクト指向でも同様だが、そのようなコンテキストのテンプレート(= class)は静的にしか作れない なんでも自由を許すと無秩序になる >>10 だって、シグネチャを無視することが前提の設計になってる (最後に結果を呼び出す時は第一引数のfを省略する) TypeScriptが使われるのもそれが理由 今やPythonやRubyにも型がある 型なんて、プログラマがちまちま書かずとも、コンパイラや型チェッカが実行前に検査してくれればいいと思う currying = (f) => { args = [] apply = (a) => { if (a === undefined) return f(...args) args.push(a) return apply } return apply } クロージャはポリモーフィズムができない オブジェクト指向言語ではたとえば比較演算子の動作を型によって変えられる a.compare(b)とソースコードのどこに書いても、aの型に応じて適切にcompareの動きが選択される 変数の型によって実行する関数を変えるにはどうする? まさか関数定義に型による条件分岐をいちいち書くわけじゃあるまいし class構文が入るまでどうやってJSでクラスを定義してたのか調べれば分かるよ >>27 a.hoge()のようにしてダックタイピングするなら当然できる hogeがa(or a.prototype)に属さないただの関数の場合でも hogeの中で型による条件分岐を書く以外の方法でできるか >>26 ,28 a, bではなくcompareのほうを、状態をもった関数オブジェクトとして生成する 一例として、 compareのクロージャには、「型名=>その型の引数を受け取った時に呼び出す関数」のテーブルを保持する そのテーブルに関数を動的に登録するための関数を作成する 例: IComparable = () => { dispatch = {} compare = (self, other) => { /*dispatchテーブルから関数を検索して実行*/ } implement = (type, fn) => { /* dispatchテーブルにfnを登録 */ } return [compare, implement] } もちろんdispatchの条件は型名じゃなくても、別の判定用関数を用意してもいい インタフェースの実装は、型定義に書くよりも使用時に動的にできた方が自然だ たとえば木構造にイテレータを実装するとして、深さ優先探索にするのか幅優先探索にするのかは、その時々によって変わる。木構造の定義だけからは決まらない 最近のフレームワークはどんどん脱オブジェクト指向・関数型化してないか? Pythonのフレームワークは機能のMixinは継承ではなくデコレータ(ようはクロージャ)によるものが多い Reactなんか思想は元々関数型的だったけど、ついにクラスコンポーネントやめちゃった cps = (f) => (...args) => args.pop()(f(...args)) eq = cps((a, b) => a === b) sub = cps((a, b) => a - b) mul = cps((a, b) => a * b) fact = (n, k) => { eq(n, 0, (isZero) => { if(isZero) { return k(1) } else { return sub(n, 1, (prevn) =>{ fact(prevn, (prevf) => { mul(n, prevf, k) }) }) } }) } fact(5, console.log) Closures are functions with contexts. Objects are structures with contexts. A structure can have functions as its member, and vice versa. So both are equal. オブジェクトよりも、クロージャよりも、第一級継続のほうが強い >>37 1. ハードウェア/コンパイラの技術向上に期待する 2. ネックになる部分だけ最適化する 3. そもそもパフォーマンスが必要ならC++やRustなどで書く オブジェクトとクロージャが使えることは、コードブロック(+ ローカル変数)を第一級オブジェクトに持っているのだと思える 継続が第一級オブジェクトなのは、プログラム実行時の任意の状態が第一級オブジェクトということ >>41 昨日からどうした なにか嫌なことでもあったか? 継続はクロージャで表現されるけど、オブジェクトとクロージャが同値なら、オブジェクトによる表現もできるの? : fact dup 1 = if drop else dup 1- rot rot * swap recurse then ; 1 5 fact cr . インタフェースが静的に決まれば保守性はおちないと思う >>31 これ継承の問題点を全部解消してる上に継承よりも強力だな >>47 使いにくい上に動的ディスパッチとなっている 継承の問題点を解消している好例はRust その上で使いやすく静的ディスパッチによる単相化も可能で速い >>49 どのへんが使いにくいのかがわからないし、動的ディスパッチだと何がよくないのかもわからない Rustのほうが早いのはそりゃ当たり前 >>43 できるだろうけど、継続自体が手続き/関数的な概念なので、オブジェクトによる表現をするメリットは少ないと思う 継続とコルーチンなら、継続のほうがprimitiveなの? >>52 継続が第一級なら保存した継続を何度も呼び出せるが、コルーチンはyieldしたらもうその時点には戻れないので、継続のほうが根本的 なんでもクロージャだと戻り値全部関数だけど、型情報を持つことはできる? ディスパッチするのに必要だよね? >>54 type属性をもったオブジェクトとして返却すればいいんじゃないんですかね それを毎回書きたくないなら、生成用関数を受け取って新しい生成用関数を返す関数 makeConstructor = (f) => (type, ...args) => {type: type, value: f(...args)} みたいなのを使ってみてはどうでしょうか >>55 これ、オブジェクトからtype属性つきオブジェクトへの関数、を持ち上げる関数書けばモナドになるから自動的に変換できるんだな そもそもオブジェクトの型を動的に判定することは不可能なのだ ただのシンボル'0'が整数であるとは数学的に決して言えない 整数の性質を満たす集合に属しているから整数と言えるのだ だからオブジェクトが型を授かるためにクラスは絶対に必要なのだ そもそもふだんそんなにポリモーフィズムしてるか? 型ヒント書いたら実行前にチェックできるとか、中間コードにコンパイルされる時高速になるとか、それでよくないか? モダンなプログラミング言語 Go、Rust、Zig、Nim、Julia、Elixirなどは クラスおよびその継承を言語仕様から排除している それぞれの言語は全く異なる方針を採っているがクラス排除だけは全てで一致している クラスとその継承は悪手であるとプログラミング言語界では結論が出ている xの型がT y = Foo(x)なら yの型はFoo(T) というシステムでいいと思うの Length ::= Float Angle ::= Float Point ::= Tuple(Float, Float) Triangle ::= Triangle(Length, Length, Length) | Triangle(Length, Length, Angle) | Triangle(Length, Angle, Angle) | Triangle(Point, Point) >>57 前半はそのとおり だがクラスは不要 変数の出自が辿れればいい Natural = fn [0] 0 [n: n >= 0] n end succ = fn [Natural(n)] Natural(n + 1) end add = fn [n, Natural(0)] n [n, succ(m)] succ(add(n, m)) end mul = fn [n, Natural(0)] Natural(0) [n, succ(m)] add(mul(n, m), n) end AdditiveMonoid = Monoid() AdditiveMonoid.implimentsIdentity(Natural, fn [] Natural(0) end) AdditiveMonoid.implimentsAppend(Natural, add) MultiplicativeMonoid = Monoid() MultiplicativeMonoid.implimentsIdentity(Natural, fn [] Natural(1) end) MultiplicativeMonoid.implimentsAppend(Natural, mul) たしかに「この関数から返ってたらこの型」というのはそうか インタフェース型を返す関数から返った値は、その実体がなんであってもインタフェース型として受け取らないといけないのか Pythonの型ヒントも、内部的にやってることは型の階層構造を保存したオブジェクト作ったりしてるだけだし、型をクロージャで表すというのは十分に現実的 なんでもクロージャでは構造に基づくサブタイピングができないから、ポリモーフィズムするなら必然的に名前に基づくサブタイピングをする必要がある 型注釈はクロージャで実現できるけど、肝心の個別のオブジェクトに型を付与するには、処理系に手加えないと無理だね ただのクロージャにこれはLinkedListだみたいな型情報をつけるなら JavaScriptのようにすべてのオブジェクトがフィールドを持てるようにして、クロージャもtype属性みたいなのを持てるようにする Pythonのように__call__()メソッドを実装すればオブジェクトも関数として呼び出せるようにして、オブジェクトにラップする おとなしくにハッシュテーブルにラップするとかして、関数としてのインタフェースを保つのは諦める 型定義 Counter = Type(fn [n] fn [i] n += i end end) f = Counter(0) f is Counter # true f(1) # 1 f(2) # 3 f(3) # 6 エイリアス Adder = Counter f is Adder # true 多相型 Pair = Type(fn [T, S] fn [t: T, s: S] (t, s) end) p = Pair(Int, Str)(1, 'Hello') p is Pair(Int, Str) # true クロージャはその外の変数をキャプチャできてこそ本領を発揮する 実行のたびに無名関数を生成すると Pair(Int, Str)の実体が毎回変わってしまう 同じ型として扱うならキャッシュしとかんといかん Foo()の結果がFoo型になるので、自然にヒエラルキー Pair(Int, Str) ⊂ Pair ⊂ Type ができる 子はすべての親を知っているが、親は直接の子しか知らない ユニオン型 Male = Type() Female = Type() Sex = Male | Female 代数的データ型 Nil = Type() Cons = Type(fn [T] fn [x: T, xs: List(T)] (x, xs) end end) List = Type(fn [T] Nil | Cons(T) end) c = Cons(Int)(1, Nil()) c is Cons(Int) # true nl = Nil() nl is Nil # true nl is List(Int) # ( = Nil | Cons(T) ) true l = Cons(2, c) l is List(Int) # true クロージャは (1) 評価可能な任意のコードブロックをオブジェクト化できる (2) 新しいコンテクストを導入できる (3) 評価を遅延可能 (4) 即時生成可能 クロージャは、評価可能な任意のコードブロックを第一級オブジェクトとして扱える 継続は、プログラム実行時の任意の状態と処理を第一級オブジェクトとして扱える >>75 汎用的な制御構造として使うには難解すぎること 言語の高速化は処理系の仕事なのだから、特定の処理系で実行速度を最適化するために冗長な書き方を強いられるのは馬鹿げている >>80 たとえばコルーチンとか、非決定的なバックトラックとかなら >>79 例外処理こそ継続とパターンマッチを使うべきだよなあと思う >>81 Pythonでいうとこのジェネレータ内包表記みたいなのがあれば、多くの場面で十分なのかな >>84 コルーチンの中でコルーチンを呼び出したら、yieldしたら最上位の呼び出し元まで返ってくるのが望ましい >>85 非同期コルーチンAの中で非同期コルーチンBを呼ぼうとしたら自動的にAはyieldして戻ってからコルーチンBを呼び出し そのBについても全て終えるか他コルーチン呼び出しで自動的にyield それぞれyieldして待機中の非同期コルーチンは呼び出し先が解決するとyield時点から再開 というスタックレス非同期コルーチンが何万も稼働しています 「コ」ルーチンなんだから上位という概念がそもそもない >>87 スタックレス非同期コルーチンは上位ランタイムがCPUコアスレッド数をフルに使って幾万個の軽い非同期コルーチンをスケジューリングすることでCPU性能を使い尽くすことができる 的外れとは? 現在のネットインフラは>>88 の通り動いている現実を認めたくない? スタックフルのコルーチンは、内部で呼び出したコルーチンがyieldしてるかどうか呼び出し元が知っていないといけない つまりカプセル化を破壊する 複数のスレッドが協調して何かするなんてのは、もはやプログラミング言語の領分じゃないんだよなぁ Erlang OTPみたいなアプローチが良いと思う 重要なのはプロトコル 定められた型のメソッドを持つことが重要 関数とオブジェクトなんて、要はどっちも値を入れたら何かを返す箱なのだから、内部構造を考えなければ共通化できるはず 配列も、機能だけ見れば、キーが数値なだけのただのオブジェクト リストやジェネレータなどをイテレートするのも、関数を引数なしで呼び出すようなもの Pythonは__call__()メソッドを実装することでオブジェクトも関数のように振る舞える これは小規模なプログラムでは関数として実装しておき、大規模になったらシームレスに移行できるように Iterable⊂Callable⊃Associative だが、Iterableとしての呼び出し方とAssociativeとしての呼び出し方が異なる x = [1, 2, 3]をIterableとしてCallableだと思う場合、x() = next(x) AssociativeとしてCallableだと思う場合、x(n) = x[n] になる マシン語を知らないと妙なこだわりを言い出すからおもしろいよなw >>101 コンピュータサイエンスにおいて低レイヤーが上位という価値観があるのって、おそらく日本だけだと思うんだけど、どうしてこういう逆行現象が起こるんだろう? 数学だと基礎論は馬鹿にされてるよね 通常の数学やるときに選択公理だとか不完全性定理だとかいってるのは素人だけ そりゃ形式的に「基礎論のほうが原理に近い」というだけならべつに数学を知らなくても分かるからね >>103 マシン語わかる でもコンピューターサイエンスの素養なし って感じのロートルが必至にマウントとろうとしてるだけだろな ふつうは情報学科でも昔ならSICPでやったようなプログラミングの概念や機能に関する講義があるはずで、 ちゃんと勉強している人で実装と機能を分けて考えるということができない人はいないよ 日本で低レイヤーが偉そうな顔してるのは単に、大学等で体系的に学ばずに昔からパソコンに触ってただけのおじさんが言ってるだけ 日本ではプログラマが高等教育が必要な専門技術職として見なされておらず、パソコンしか取り柄のない社会不適合者を低賃金でこきつかう職種だと見なされていたのが原因 read.cgi ver 07.5.1 2024/04/28 Walang Kapalit ★ | Donguri System Team 5ちゃんねる