>儘管JavaScript一直支持某些功能性編程技術,但它們在過去幾年中才真正流行,傳統上也沒有對不變數據的本地支持。 JavaScript仍在學習很多方面,最好的想法來自已經嘗試並測試了這些技術的語言。 在編程世界的另一個角落,Clojure是一種功能性編程語言,致力於真正的簡單性,尤其是在數據結構的情況下。 Mori是一個庫,允許我們直接從JavaScript中使用Clojure的持久數據結構。 >本文將探討這些數據結構設計背後的基本原理,並檢查一些使用它們來改善我們的應用程序的模式。我們也可以將其視為對使用Clojure或Clojurescript進行編程的JavaScript開發人員的第一個墊腳石。
>什麼是持續數據?
>> clojure在無法更改的
持續的值之間進行區分,而
>可能有助於查看這種區別在理論編程語言中的外觀。
<span>// transient list </span>a <span>= [1, 2, 3]; </span>b <span>= a.push(4); </span><span>// a = [1, 2, 3, 4] </span><span>// b = [1, 2, 3, 4] </span> <span>// persistent list </span>c <span>= #[1, 2, 3] </span>d <span>= c.push(4); </span><span>// c = #[1, 2, 3] </span><span>// d = #[1, 2, 3, 4] </span>
>我們可以看到,當我們將一個值推向其上時,瞬態列表被突變了。 A和B都指向相同的可變值。相比之下,在持久列表上呼叫推送返回了一個新值,我們可以看到c和d指向不同的離散列表。
>>這些持久的數據結構無法突變,這意味著一旦我們提到了一個值,我們也可以保證它永遠不會被更改。這些保證通常可以幫助我們編寫更安全,更簡單的代碼。例如,將持久數據結構作為參數的函數無法突變它們,因此,如果該函數想要傳達有意義的更改,則必須來自返回值。這導致編寫引用透明的純函數,易於測試和優化。
更簡單,不變的數據迫使我們編寫更多功能代碼。什麼是mori?
>就像Clojure一樣,Mori的功能與它們操作的數據結構分開,與JavaScript面向對象的趨勢對比。我們會發現這種差異改變了我們編寫代碼的方向。
莫里還使用結構共享來通過盡可能多的原始結構共享對數據進行有效的更改。這使得持續數據結構幾乎與常規瞬態效率一樣有效。這些概念的實現在此視頻中更詳細地介紹了。
><span>// standard library </span><span>Array(1, 2, 3).map(x => x * 2); </span><span>// => [2, 4, 6] </span> <span>// mori </span><span>map(x => x * 2, vector(1, 2, 3)) </span><span>// => [2, 4, 6] </span>為什麼有用?
> 首先,讓我們想像我們正在嘗試在我們繼承的JavaScript代碼庫中追踪一個錯誤。我們正在閱讀代碼,試圖弄清楚為什麼我們最終獲得了獎學金的錯誤價值。
>>
>不運行代碼或閱讀deleteperson()的定義,就無法知道。它可能是一個空數組。它可能具有三個新屬性。我們希望這是一個刪除第二個元素的數組,但是由於我們通過可變的數據結構通過,因此無法保證。<span>const fellowship = [ </span> <span>{ </span> <span>title: 'Mori', </span> <span>race: 'Hobbit' </span> <span>}, </span> <span>{ </span> <span>title: 'Poppin', </span> <span>race: 'Hobbit' </span> <span>} </span><span>]; </span> <span>deletePerson(fellowship, 1); </span><span>console.log(fellowship); </span>更糟糕的是,該函數可以保持參考並在將來異步進行突變。從這裡開始對獎學金的所有參考都將與不可預測的價值合作。
將其與Mori的替代方案進行比較。
<span>// transient list </span>a <span>= [1, 2, 3]; </span>b <span>= a.push(4); </span><span>// a = [1, 2, 3, 4] </span><span>// b = [1, 2, 3, 4] </span> <span>// persistent list </span>c <span>= #[1, 2, 3] </span>d <span>= c.push(4); </span><span>// c = #[1, 2, 3] </span><span>// d = #[1, 2, 3, 4] </span>
>不管deleteperson()的實施如何,我們知道原始向量將被記錄,僅僅是因為可以保證它不能突變。如果我們希望該函數有用,則應返回並刪除指定項目的新向量。
在可變數據上運行的函數並不總是返回值,它們可以突變其輸入,有時還留給程序員再次在另一側拾取值。
更簡單,不變的數據可實現可預測性的文化。
>我們將研究如何使用MORI來構建具有撤消功能的像素編輯器。以下代碼可作為Codepen可用,您也可以在文章的腳下找到。
>
我們假設您要么跟隨Codepen,要么在ES2015環境中使用MORI和以下HTML。
讓我們開始通過破壞Mori名稱空間所需的功能開始。
<span>// standard library </span><span>Array(1, 2, 3).map(x => x * 2); </span><span>// => [2, 4, 6] </span> <span>// mori </span><span>map(x => x * 2, vector(1, 2, 3)) </span><span>// => [2, 4, 6] </span>這主要是一種風格偏好。您也可以通過直接在Mori對像上訪問MORI(例如Mori.list())。
>當我們需要檢查Mori的數據結構時,我們可以將此功能用作cons.log()的替代方法。
<span>const fellowship = [ </span> <span>{ </span> <span>title: 'Mori', </span> <span>race: 'Hobbit' </span> <span>}, </span> <span>{ </span> <span>title: 'Poppin', </span> <span>race: 'Hobbit' </span> <span>} </span><span>]; </span> <span>deletePerson(fellowship, 1); </span><span>console.log(fellowship); </span>接下來,我們將設置一些配置值和實用程序函數。
>希望您注意到我們的to2d()函數返迴向量。向量有點像JavaScript數組,並且支持有效的隨機訪問。
構造數據
<span>import <span>{ vector, hashMap }</span> from 'mori'; </span> <span>const fellowship = vector( </span> <span>hashMap( </span> <span>"name", "Mori", </span> <span>"race", "Hobbit" </span> <span>), </span> <span>hashMap( </span> <span>"name", "Poppin", </span> <span>"race", "Hobbit" </span> <span>) </span><span>) </span> <span>const newFellowship = deletePerson(fellowship, 1); </span><span>console.log(fellowship); </span>>我們將使用我們的to2d()函數來創建一系列坐標,該坐標將代表畫布上的所有像素。
>我們使用range()函數來生成一個數字序列在0和高度*寬度之間(在我們的情況100)之間,並且我們使用map()將其轉換為使用to2d的2D坐標列表( )輔助功能。
<span><span><span><div</span>></span> </span> <span><span><span><h3</span>></span>Mori Painter<span><span></h3</span>></span> </span><span><span><span></div</span>></span> </span><span><span><span><div</span> id<span>="container"</span>></span> </span> <span><span><span><canvas</span> id<span>='canvas'</span>></span><span><span></canvas</span>></span> </span><span><span><span></div</span>></span> </span><span><span><span><div</span>></span> </span> <span><span><span><button</span> id<span>='undo'</span>></span>↶<span><span></button</span>></span> </span><span><span><span></div</span>></span> </span>>可能有助於可視化坐標的結構。
>
<span>// transient list </span>a <span>= [1, 2, 3]; </span>b <span>= a.push(4); </span><span>// a = [1, 2, 3, 4] </span><span>// b = [1, 2, 3, 4] </span> <span>// persistent list </span>c <span>= #[1, 2, 3] </span>d <span>= c.push(4); </span><span>// c = #[1, 2, 3] </span><span>// d = #[1, 2, 3, 4] </span>
>我們正在使用repot()函數來創建“ #FFF”字符串的無限序列。我們不必擔心會填充內存並崩潰我們的瀏覽器,因為Mori序列支持懶惰評估。我們只在稍後要求時才計算序列中項目的值。
>最後,我們想以哈希地圖的形式將坐標與我們的顏色相結合。
<span>// standard library </span><span>Array(1, 2, 3).map(x => x * 2); </span><span>// => [2, 4, 6] </span> <span>// mori </span><span>map(x => x * 2, vector(1, 2, 3)) </span><span>// => [2, 4, 6] </span>>我們使用zipmap()函數來創建哈希地圖,將坐標作為鍵,顏色作為值。同樣,這可能有助於可視化數據的結構。
與JavaScript的對像不同,Mori的Hash Maps可以將任何類型的數據作為鍵。
繪製像素<span>const fellowship = [ </span> <span>{ </span> <span>title: 'Mori', </span> <span>race: 'Hobbit' </span> <span>}, </span> <span>{ </span> <span>title: 'Poppin', </span> <span>race: 'Hobbit' </span> <span>} </span><span>]; </span> <span>deletePerson(fellowship, 1); </span><span>console.log(fellowship); </span>為了更改像素的顏色,我們將把哈希地圖中的一個坐標與新字符串相關聯。讓我們寫一個純淨的函數,為單個像素著色。
>
hash映射,而不是突變舊的。
繪畫圖片<span>import <span>{ vector, hashMap }</span> from 'mori'; </span> <span>const fellowship = vector( </span> <span>hashMap( </span> <span>"name", "Mori", </span> <span>"race", "Hobbit" </span> <span>), </span> <span>hashMap( </span> <span>"name", "Poppin", </span> <span>"race", "Hobbit" </span> <span>) </span><span>) </span> <span>const newFellowship = deletePerson(fellowship, 1); </span><span>console.log(fellowship); </span>
>現在,我們有所有需要將簡單圖像繪製到畫布上的一切。讓我們創建一個函數,該函數將坐標圖的哈希映射針對像素,並將它們繪製到renderingContext2d。 讓我們花一點時間了解這裡發生了什麼。
>最後,我們使用帆布方法將彩色矩形繪製到上下文本身上。
<span><span><span><div</span>></span> </span> <span><span><span><h3</span>></span>Mori Painter<span><span></h3</span>></span> </span><span><span><span></div</span>></span> </span><span><span><span><div</span> id<span>="container"</span>></span> </span> <span><span><span><canvas</span> id<span>='canvas'</span>></span><span><span></canvas</span>></span> </span><span><span><span></div</span>></span> </span><span><span><span><div</span>></span> </span> <span><span><span><button</span> id<span>='undo'</span>></span>↶<span><span></button</span>></span> </span><span><span><span></div</span>></span> </span>
將其接線
現在,我們需要進行一些管道,以使所有這些部分將所有這些零件一起工作。
<span>const { </span> list<span>, vector, peek, pop, conj, map, assoc, zipmap, </span> range<span>, repeat, each, count, intoArray, toJs </span><span>} = mori; </span>>我們將掌握畫布,並使用它來創建上下文來呈現我們的圖像。我們還將適當地調整它的大小以反映我們的尺寸。
>
>最後,我們將使用像素通過油漆方法繪製的像素來傳遞上下文。幸運的是,您的畫布應該像白色像素一樣渲染。不是最令人興奮的揭露,但我們越來越接近。<span>const log = (<span>...args</span>) => { </span> <span>console.log(...args.map(toJs)) </span><span>}; </span>>
>我們想收聽點擊事件,並使用它們以較早的draw()函數來更改特定像素的顏色。
><span>// the dimensions of the canvas </span><span>const [height, width] = [20, 20]; </span> <span>// the size of each canvas pixel </span><span>const pixelSize = 10; </span> <span>// converts an integer to a 2d coordinate vector </span><span>const to2D = (i) => vector( </span> i <span>% width, </span> <span>Math.floor(i / width) </span><span>); </span>
>我們將單擊偵聽器附加到我們的畫布上,並使用事件坐標來確定要繪製哪個像素。我們使用此信息使用我們的draw()函數創建新的像素哈希地圖。然後,我們將其繪製到我們的上下文中,並覆蓋我們畫的最後一幀。
在這一點上,我們可以將黑色像素繪製到畫布中,每個幀將基於上一個,創建一個複合圖像。。
>跟踪幀我們正在使用列表來存儲我們繪製的不同的“幀”。列表支持在頭部的有效添加,O(1)查找第一項,這使其非常適合表示堆棧。
<span>// transient list </span>a <span>= [1, 2, 3]; </span>b <span>= a.push(4); </span><span>// a = [1, 2, 3, 4] </span><span>// b = [1, 2, 3, 4] </span> <span>// persistent list </span>c <span>= #[1, 2, 3] </span>d <span>= c.push(4); </span><span>// c = #[1, 2, 3] </span><span>// d = #[1, 2, 3, 4] </span>我們需要修改單擊偵聽器以與幀堆棧一起使用。
我們正在使用PEEK()函數將框架放在堆棧頂部。然後,我們使用它來使用draw()函數創建一個新幀。最後,我們將conj()用於
conjoin
框架頂部的新框架。<span>// standard library </span><span>Array(1, 2, 3).map(x => x * 2); </span><span>// => [2, 4, 6] </span> <span>// mori </span><span>map(x => x * 2, vector(1, 2, 3)) </span><span>// => [2, 4, 6] </span>
儘管我們正在更改本地狀態(frame = conj(框架,newFrame)),但我們實際上並未突變任何數據。 撤消更改
>最後,我們需要實現一個撤消按鈕,以從堆棧中彈出頂框。>單擊“撤消按鈕”時,我們檢查當前是否有任何幀要撤消,然後使用pop()函數用不再包含頂幀的新列表替換幀。
這是我們最終得到的:
<span>const fellowship = [ </span> <span>{ </span> <span>title: 'Mori', </span> <span>race: 'Hobbit' </span> <span>}, </span> <span>{ </span> <span>title: 'Poppin', </span> <span>race: 'Hobbit' </span> <span>} </span><span>]; </span> <span>deletePerson(fellowship, 1); </span><span>console.log(fellowship); </span>
請參閱codepen上的sitepoint(@sitepoint)的筆莫里像素。
擴展
添加一個調色板,允許用戶在繪製
之前選擇顏色>使用本地存儲來保存會話之間的幀
允許用戶在拖動鼠標時繪製
Mori中的持續數據結構是什麼?在Mori中,是不可變的數據結構,在修改後可以保留先前版本的數據。這意味著每次您對持久數據結構執行操作時,都會創建一個新版本,並保留舊版本。
>
在JavaScript中使用MORI?>
>我如何在我的JavaScript項目中開始使用Mori?>
以上是帶有MORI的不變數據和功能性JavaScript的詳細內容。更多資訊請關注PHP中文網其他相關文章!