>尽管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的数据结构时,我们可以将此功能用作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>
>我们使用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>
与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中文网其他相关文章!