機胜的な宣蚀スタむルでJSで曞く





はじめに



関数型蚀語は、そのシンプルさ、明快さ、および予枬可胜性が倧奜きです。 私は䞻にElixir / Erlang / OTPで曞いお、他の蚀語を詊したしたが、Erlangずそのアクタヌは、䟋えばLispやHaskellよりもずっず身近です。 ご存じのように、Erlang == webであり、Web甚に䜜成されたものには、クラむアント偎のWebむンタヌフェむスhtml、css、js-contentが含たれるこずがありたす。 残念ながら、jsは珟代のWebの暙準です。ほずんどすべおの堎合、ほがすべおのタスクに察応するラむブラリがあり、これが倚かれ少なかれ、クラむアント偎のブラりザヌで䜕かを行うための唯䞀の利甚可胜なツヌルです。 したがっお、ただjsが必芁です。 最初は、「ラムダず高階関数があるので、jsで曞くのは簡単です。 Erlang / Lisp / Haskellで曞いおいるように構文を孊び、曞いおいきたす。 私がどれほど間違っおいたか。





蚀語を遞択しおください



そもそも、玔粋なjsはコヌドの蚘述にはたったく適しおいたせん。 豊富なブラケットずセミコロンが目の波王から。 関数の本䜓のどこかに曞かれた「return」ずいう蚀葉は、蚀語の呜什性を暗瀺する完党に䞍透明なヒントであり、私の最高の信念を砎壊したす。 を含む倚くの蚀語がありたす jsでコンパむルされた機胜的 purescript 、 fay 、 clojurescript 。 しかし、私自身のために、私はcoffeescriptを遞択したした。これは、機胜者ず倧将軍の䞡方が理解できるかなり劥協したオプションです。 郚分的に、この遞択は、私がプロゞェクトを構築するためにcoffeescriptで曞かれたbrunchを䜿甚するずいう事実によっお正圓化されたした。 たずえば、fayず比范するず、jsからcoffeescriptに切り替えるオヌバヌヘッドはほが0です。



フレヌムワヌクの遞択



2番目の興味深い質問は、コヌドずhtmlペヌゞを接続する方法です。 本圓に倚くのオプションがありたす。 実際にはかなり党䜓的に芋える巚倧なフレヌムワヌクがたくさんありたす。 私自身はしばらくの間angularjsを䜿甚しおいたしたが、いく぀かの緎習の埌、明らかな欠点が明らかになりたした䜕かをするためには、ディレクティブが必芁です。そうでない堎合は、独自のバむクを曞く必芁があり、角床の内郚構造を理解するこずは芋た目よりも難しいです率盎に芋おください-最も䞀般的に䜿甚されるng-modelディレクティブは双方向デヌタずプレれンテヌションバむンディングを提䟛したす。ファンクタの芳点からは、これらはすべおの角床アプリケヌションに加えお、むデオロギヌ的に完党に䞍正確であり、䞀般的にカプセル化に違反したす スクヌタヌなどは、コヌドに倧きな重みを付けおいたす。 はい、ずころで、角床の性胜は本圓にたあたあです。 少し前に私はリアクションjsに䌚いたした-そしお、私の遞択は圌に萜ちたせんでした。 このアむデアは、そのシンプルさず倚かれ少なかれ機胜的な宣蚀スタむルに感銘を受けたす。 アプリケヌションの操䜜䞭に䜕らかの圢で倉化する可胜性のある状態がありたす。 レンダリングのために時々jreactに枡すだけです。



widget = require("widget") do_render = () -> React.render(widget(state), domelement) if domelement? render_process = () -> try do_render() catch error console.log error setTimeout(render_process, 500)
      
      







それだけです 非垞にシンプルで効果的。 React自䜓がレンダリングの最適化を凊理したすが、この問題にはたったく関心がないかもしれたせん。 機胜パラダむムず䞀臎するように状態オブゞェクトが倉曎されるこずを保蚌するために残りたす。 そしお、ここから楜しみが始たりたす。



キッチンのトラブル



最初の、それほど重芁ではない問題は、゜フトjs型です。 アヌランではもちろん柔らかくもありたすが、jsでは衚珟が倧倉申し蚳ありたせん。 たずえば、このトピックに関するあたり有益ではないが、かなり面癜いビデオです。 しかし、実際には、型倉換はめったに行われたせん少なくずもコヌドが良い堎合。だから、私は゜フトjs型を倚かれ少なかれそのたた受け入れたした。



私がjsで少し緎習し始めたずき、最初はすべおが倚かれ少なかれ良かったのですが、ある時点で䜕らかの理由でアプリケヌションが思い通りに機胜しなくなりたした。 さらに深く登るず、恐ろしいものが芋えたした。



 coffee> map = {a: 1} { a: 1 } coffee> lst = [] [] coffee> lst.push map 1 coffee> map.a = 2 2 coffee> lst.push map 2 coffee> map.a = 3 3 coffee> lst.push map 3 coffee> lst [ { a: 3 }, { a: 3 }, { a: 3 } ]
      
      







この堎合、私は確かに芋るず期埅しおいたしたが



 coffee> lst [ { a: 1 }, { a: 2 }, { a: 3 } ]
      
      







本圓にショックでした。 ビンゎ、jsのデヌタは倉曎可胜です そしお刀明したように、数字、文字列、null、未定矩よりも耇雑なものはすべお参照枡しされたす



しかし、私はそれを芋たずき



 coffee> [1,2,3] == [1,2,3] false coffee> {a: 1} == {a: 1} false
      
      







それから私の髪はいろいろな堎所で動いた。 この人生は私を準備したせんでした。 jsのデヌタ型マップずリストは、倀ではなく参照によっお比范されたす。



私はどうあるべきかを考え始めたした。 たずえば、可倉性に関しお、定数デヌタをアリティれロのラムダ関数でラップするたずえば、倀を初期化するこずが決定されたした。そのような堎合、それらは䞀般に可倉ではありたせんでした。 それらを必芁ずする匏で単玔に呌び出すこずができ、それらデヌタが倉曎されるこずを恐れるこずはありたせん。



 coffee> const_lst = () -> [1,2,3] [Function] coffee> new_lst = const_lst().concat([4,5,6]) [ 1, 2, 3, 4, 5, 6 ] coffee> const_lst() [ 1, 2, 3 ]
      
      







原則ずしお、すべおのデヌタがラムダで再垰的にラップされる堎合、すべおの関数がラムダを取り、ラムダを返す堎合、蚀語は本圓に機胜するようになるず思いたした 原則ずしお、これは解決策です。 通垞の型に基づいおこれらのラムダデヌタ型を蚘述し、通垞のjs型ぞの再垰的な順方向および逆方向倉換のための関数を蚘述するだけでなく、これらのラムダ型map、reduce、filter、zipなどを操䜜するための高階関数を蚘述する必芁がありたす。 同時に、ちなみに、これらの新しいタむプは柔らかくするこずはできたせん。 原則ずしお、問題は解決可胜ですが、このラむブラリですでに実装されおいる方法などにより、かなり膚倧です。 しかし、このアプロヌチには非垞に重倧な欠点がありたす。



1私たちのコヌドは通垞空䞭に䞭断されたせんが、他のjsラむブラリに䟝存しおいるため、それらに目を向けるたびに、ラムダ型を通垞型に、たたはその逆に倉換するこずを忘れないでください。

2このアプロヌチでは、もちろん、関数の玔床ずデヌタの䞍倉性をある皋床保蚌したすが、トランザクション性はありたせん。

3このようなコヌドは、呜什型アプロヌチを奜む人にはあたり明確ではありたせん



したがっお、私はこのアむデアを攟棄したしたしかし、将来的には泚意する必芁がありたす。 明らかに、可倉性の問題を局所的に解決し、参照によっおデヌタを比范するために、jsデヌタを倀によっお再垰的にコピヌしお比范する方法を孊ぶ必芁がありたした。



クロヌン関数を曞きたす

 clone = (some) -> switch Object.prototype.toString.call(some) when "[object Undefined]" then undefined when "[object Boolean]" then some when "[object Number]" then some when "[object String]" then some when "[object Function]" then some.bind({}) when "[object Null]" then null when "[object Array]" then some.map (el) -> clone(el) when "[object Object]" then Object.keys(some).reduce ((acc, k) -> acc[clone(k)] = clone(some[k]); acc), {}
      
      







むコヌル関数を曞きたす

 equal = (a, b) -> [type_a, type_b] = [Object.prototype.toString.call(a), Object.prototype.toString.call(b)] if type_a == type_b switch type_a when "[object Undefined]" then a == b when "[object Boolean]" then a == b when "[object Number]" then a == b when "[object String]" then a == b when "[object Function]" then a.toString() == b.toString() when "[object Null]" then a == b when "[object Array]" len_a = a.length len_b = b.length if len_a == len_b [0..len_a].every (n) -> equal(a[n], b[n]) else false when "[object Object]" keys_a = Object.keys(a).sort() keys_b = Object.keys(b).sort() if equal(keys_a, keys_b) keys_a.every (k) -> equal(a[k], b[k]) else false else false
      
      







思ったよりも簡単でしたが、唯䞀の「しかし」デヌタに埪環リンクがある堎合、もちろんスタックオヌバヌフロヌが発生したす。 私にずっお、これは基本的に問題ではありたせん。「埪環参照」などの抜象化を䜿甚しおいないからです。 デヌタクロヌニングのプロセスで䜕らかの方法で凊理するこずは可胜だず思いたすが、もちろんコヌドはそれほど単玔で゚レガントではありたせん。 䞀般的に、これらの関数ずラむブラリの他のいく぀かの関数を収集し、jsのデヌタの可倉性の問題はしばらくの間解決されたず思いたす。



俳優



次に、トランザクションデヌタの倉曎が必芁な理由に぀いお説明したす。 倚かれ少なかれ耇雑な状態があり、関数を実行するプロセスでそれを倉曎するずしたす。

 state.aaa = 20 state.foo = 100 state.bar = state.bar.map (el) -> baz(el, state)
      
      







実際には、状態を倉曎するプロセスはもちろん、より耇雑で時間がかかり、倖郚APIぞの非同期呌び出しなどを含むこずができたす。 しかし、芁点は、どこかで状態を倉曎するプロセスのどこかで、func状態関数が呌び出された堎合です-䜕が起こるのでしょうか 半分倉曎された状態は有効ですか たたは、半修正状態のシングルスレッドjsが原因で、たったく存圚せず、すべおが正垞ですか そうでない堎合は たた、倖郚呌び出しを行う必芁があり、状態を倉曎する間、それが䞍可欠である堎合はどうすればよいですか このような難しい質問を解決するために、状態の倉曎をトランザクション化したす。



その埌、倚くの人がミュヌテックスに぀いお芚えおいるず思いたす。 ミュヌテックスも思い出したした。 そしお、レヌス条件に぀いお。 そしお、デッドロックに぀いお。 そしお、私はこれをたったく望んでいないこずに気付いたので、ミュヌテックスを䜜成したせんが、Erlang蚀語から「actor」の抂念を借甚したす。 jsのコンテキストでは、アクタヌは特定の状態によっお初期化されるオブゞェクトの䞀皮であり、3぀のこずのみを行いたす



1アリティ関数1たたは0の圢匏で「メッセヌゞ」を受信し、キュヌに远加したす

2独立しおメッセヌゞキュヌを「レむク」し、内郚状態にアリティ関数1を適甚したすアリティ関数0が単に呌び出され、状態は倉化したせん-これらはすべお、メッセヌゞが受信された順序で厳密に行われたす。

3芁求に応じお、その内郚状態の倀を返す



圓然、デヌタの非可倉性を維持するために、倉曎するたびに状態を耇補し、get関数では状態自䜓ではなく、そのコピヌを返したす。 このため、以前に䜜成したラむブラリを䜿甚したす。 その結果、コヌドを取埗したす。



 window.Act = (init_state, timeout) -> obj = { # # priv # state: Imuta.clone(init_state) queue: [] init: () -> try @state = Imuta.clone(@queue.shift()(@state)) while @queue.length != 0 catch error console.log "Actor error" console.log error this_ref = this setTimeout((() -> this_ref.init()), timeout) # # public # cast: (func) -> if (func.length == 1) and Imuta.is_function(func) @queue.push(Imuta.clone(func)) @queue.length else throw(new Error("Act expects functions arity == 1 (single arg is actor's state)")) zcast: (func) -> if (func.length == 0) and Imuta.is_function(func) @queue.push( ((state) -> Imuta.clone(func)(); state) ) @queue.length else throw(new Error("Act expects functions arity == 0")) get: () -> Imuta.clone(@state) } obj.init() obj
      
      





*ラムダをキュヌに入れる関数は、Erlangのhandle_cast関数ず同様に、castおよびzcastず呌ばれたす



すべおのjs-dataが耇補可胜であるわけではないため埪環リンクず倖郚ラむブラリを思い出しおください、内郚状態のクロヌンを削陀し、すべおをラむブラリに収集するアクタのコンストラクタの別のバヌゞョンを䜜成したす 。



お楜しみください

 coffee> actor = new Act({a: 1}, "pure", 500) { state: { a: 1 }, queue: [], init: [Function], cast: [Function], zcast: [Function], get: [Function] } coffee> actor.cast((state) -> state.b = 1; state) 1 coffee> actor.get() { a: 1, b: 1 } coffee> actor.cast((state) -> state.c = 1; state) 1 coffee> value = actor.get() { a: 1, b: 1, c: 1 } coffee> value.d = 123 123 coffee> value { a: 1, b: 1, c: 1, d: 123 } coffee> actor.get() { a: 1, b: 1, c: 1 } coffee> actor.zcast(() -> console.log "hello") 1 coffee> hello coffee> actor.get() { a: 1, b: 1, c: 1 } coffee> global_var = {foo: "bar"} { foo: 'bar' } coffee> actor.cast((_) -> global_var) 1 coffee> actor.get() { foo: 'bar' } coffee> global_var.baz = "baf" 'baf' coffee> global_var { foo: 'bar', baz: 'baf' } coffee> actor.get() { foo: 'bar' }
      
      







すべおの状態倉曎は、キャスト関数を䜿甚しおキュヌを介しお排他的に実行されたす。 「玔粋な」バヌゞョンでわかるように、get関数から取埗した埌の状態に関係なく、状態はアクタヌ内に完党にカプセル化されたす倖郚からキャスト関数に䜕かを远加した堎合も同様です。 トランザクションはメッセヌゞキュヌによっお提䟛されたす。 jsでのみ、ほずんどErlangianコヌドを取埗したした。 䜕らかの理由で状態でクロヌンされおいないデヌタを䜿甚する堎合は、グロヌバル状態で「ダヌティ」バヌゞョンのアクタヌを䜿甚したす。 原則ずしお、そのようなオプションアクタヌを介しお厳密に状態を倉曎する堎合でも受け入れられ、デヌタのトランザクションの倉曎を提䟛したす。 トランザクションだけでなく、ある意味でアトミックでさえ、デヌタの倉曎を1぀のラムダではなく3぀たずえば、倖郚ラむブラリでの䜕らかの実行の堎合に転送するずいう考えがありたした。



 actor.cast( { prepare: (state) -> prepare_process(state) apply: (state, args) -> do_work(state, args) rollback: (state, args) -> do_rollback(state, args, error) })
      
      







しかし、私はこれがすでに過剰だず思った、特に珟圚のバヌゞョンでは、あなたは単に曞くこずができるので

 actor.cast((state) -> args = init_args(state) try do_work(state, args) catch error rollback(state, args, error))
      
      





原子性が非垞に重芁な堎合。



おわりに



Jsは私が最初に思ったほど絶望的ではないこずが刀明したした。 ラムダ関数の背埌には実際の機胜的なパワヌがあり、適切なレベルの噚甚さで、かなり宣蚀的なスタむルで蚘述できたす。 そしおたず、俳優+反応+玉+サス+匟䞞Erlang websocketsを䜿甚した簡単な䟋です。 機胜を維持し、りェブを維持したす



UPD



Habrの䜏人は、私のコヌドの倚くの重倧な゚ラヌを指摘したした。これに感謝したす:)欠点を修正したした、すなわち



1クロヌンバむンディング関数を、より適切で明癜な関数クロヌニング操䜜に眮き換えたした

2equal関数では、最初に、パフォヌマンスを向䞊させるはずのかなり明癜なチェックを远加したした:)

3倀func.toStringによる関数の愚かなチェックを削陀したした 。

4配列を比范するずき、おそらく再定矩されたフィヌルド長を䜿甚せず、単に芁玠を数えたす

5オブゞェクトを保存するずき、「Object.keys」関数を䜿甚せず、オブゞェクトからすべおのキヌを収集したす。 ある意味では、コヌドはさらにスリムで機胜的になりたした。



このコヌドの改善に貢献しおくれたすべおの人に感謝したす。 ラむブラリの珟圚のバヌゞョンは、 ここずここにありたす 。 私はあなたの垌望ず提案を聞いおうれしいです。



All Articles