翻译自:https://gist.github.com/ohanhi/0d3d83cf3f0d7bbea9db
原做者: Ossi Hanhinen, @ohanhi
翻译:Integ, @integ
爱心支持 Futurice .
受权协议 CC BY 4.0.react
不久之前一个好朋友给我安利了 响应式编程(Reactive Programming)。不写 函数式响应编程 简直就是犯罪 -- 很明显函数式方法大幅弥补了响应编程的不足。它如何作到的,我并不知道,因此我决定学一下这些东西。git
经过了解本身,我很快发现只有用它解决一些实际的问题,我才能领会它的观念模式。写了这么多年 Javascript,我原本早就能够开始使用 RxJS 的。但再一次,由于我了解本身,而且我发现它会给我太多空间来违背常理。我须要一个强制我用函数式思惟来解决任何问题的工具,正在这时 Elm 出现了。github
Elm 是一种编程语言,它会被编译为 HTML5: HTML, CSS 和 JavaScript。根据你显示输出结果的不一样,它多是一个内置了对象的 <canvas>
,或者一个更传统的网页。让我重复一遍,Elm 是一种语言,它会被编译为 三种语言 来构建 web 应用。并且,它是一个拥有强类型和 不可变(immutable)数据结构的函数式语言。web
好了,你能够猜到我并非这个领域的专家,为了防止你走丢,我专门在这篇文章的最后列出了下面的术语解释:附录:术语表.express
我决定尝试使用 Elm 制做一个相似《太空侵略者》的游戏。让咱们站在玩家的视角思考一下它是怎么工做的。编程
在屏幕下部有一艘表明着玩家的飞船canvas
玩家能够经过相应的方向键控制飞船左右移动segmentfault
玩家能够按向上键发射子弹射击数组
好了,咱们切换到飞船的视角,再来看下数据结构
飞船有一个一维的位置坐标
飞船能够得到一个速度(向左或向右)
飞船根据它的速度改变位置
飞船可能被击中
这些基本上给了我一个飞船的数据结构的定义,或者说一个 Elm 术语中的 记录。尽管并不是必须,我仍是喜欢把它定义为一个 aliases 类型,这样就可使用 Ship
来表示它的类型了。
type alias Ship = { position : Float -- just 1 degree of freedom (left-right) , velocity : Float -- either 0, 1 or -1 , shooting : Bool }
太棒了,如今让咱们建立一个飞船吧。
initShip : Ship -- this is the type annotation initShip = { position = 0 -- the type is Float , velocity = 0 -- Float , shooting = False -- Bool }
因此,咱们已经到了一个有趣的地步。再看一遍上面的定义,它是一个简单的陈述仍是一个函数定义?无所谓!initShip
既能够被认为只是字面量的定义纪录,也能够看做一个永远返回这些纪录的函数。由于函数是纯函数,而且它的数据结构是不可改变的,因此也没有办法区分他们,Wow,cool。
旁注:若是你像我同样,你会思考若是试着从新定义
initShip
会发生什么。好的,会发生一个编译时错误:“命名冲突:只能有一个对foo
的定义”。
好,咱们来开始移动飞船!我记得高中时学过 s = v*dt
,或者说距离等于速度乘以时间差。因此这就是我如何改变个人飞船。在 Elm 中会像下面这样实现。
applyPhysics : Float -> Ship -> Ship applyPhysics dt ship = { ship | position = ship.position + ship.velocity * dt }
类型标记描述了:给出一个 Float
和一个 Ship
,我会返回一个 Ship
,甚至:给出一个 Float
,我会返回 Ship -> Ship
。例如,(applyPhysics 16.7)
实际上会返回一个能够传入一个 Ship
参数的函数,而且获得应用了物理方程的飞船做为返回值。这个特性叫作 柯里化 并且全部 Elm 函数自动这样运做。
旁注: 然而,这一切有什么意义呢?好吧,假设我要建立一个由两列数据组成的表格。我知道如何构建它相似“给出一个列表和一个简单的值,从列表中找出匹配的项”或者直接写做
findMatches : List -> Item -> List
。可是我须要把一些先前已经知道的列表映射到新的列表中。这就是柯里化伟大的地方:我能够仅仅写出crossReference = map (findMatches listA) listB
就能够实现了。(findMatches listA)
是一个Item -> List
类型的函数,彻底就是咱们想要的。
如今,回到实际的话题,applyPhysics
建立了一个新的纪录,使用提供的 Ship
做为基础,设置 position
为一些其余的值。这就是 { ship | position = .. }
句法的含义。更多的,请参考 Updating Records。
更新飞船的其余两个属性也是相似:
updateVelocity : Float -> Ship -> Ship updateVelocity newVelocity ship = { ship | velocity = newVelocity } updateShooting : Bool -> Ship -> Ship updateShooting isShooting ship = { ship | shooting = isShooting }
把这些拼在一块儿,咱们就获得了一搜完整的飞船,像下面这样:
-- represents pressing the arrow buttons -- x and y go from -1 to 1, and stay at 0 if nothing is pressed type alias Keys = { x : Int, y : Int } update : Float -> Keys -> Ship -> Ship update dt keys ship = let newVel = toFloat keys.x -- `let` defines local variables for `in` isShooting = keys.y > 0 in updateVelocity newVel (updateShooting isShooting (applyPhysics dt ship))
如今,假设我只是调用 update
30 次每分钟,传给他距离上次更新的时间差、被按下的键和先前的 ship
,我已经有了一个完美的小游戏模型了。除了我看不到任何东西,由于没有进行渲染... 可是理论上它是可行的。
让咱们来总结一下目前为止发生了什么。
aliases 类型定义了数据模型
全部数据是不可变的
类型标记分清了函数的目标
全部函数都是纯函数的
事实上,这个预览里根本没有办法意外地改变状态。也没有任何循环。
我已经讲了不少关于这个游戏的底层的东西。定义了一个 model 和全部用于更新它的函数。惟一的麻烦是全部函数依赖于飞船的上一次更新。记住,在 Elm 里,任何状况下,你都不能在共享的做用域中保存状态,包括当前的 module -- 没有办法改变任何已经定义过的东西。那么,如何在程序中改变一个状态呢?
有一些毁三观的事情将要发生了。在面向对象编程中,程序的状态是分散在一些实例中的。这里的 Ship
是算是一个类,并且 myShip
应该是这个类的实例。在程序运行的任何一个时间 myShip
都知道本身的位置和其余属性。但在函数式编程中并非这样,在程序运行时 initShip
与刚开始时彻底同样。为了获得当前的状态,我须要知道过去发生了什么。我须要使用那些事情做为参数传递给已经定义好的函数,只有这样我才能获得 Ship
当前应该处在的状态。这与曾经的玩法彻底不一样,因此我要详细讲解这个过程。
在刚开始时 initShip
有一个默认的值: 0, 0, False
。还有一些函数能够转换一个 Ship
成为另外一个 Ship
。详细地说,有个 update
函数,它获得用户输入和一个 ship 返回一个更新过的 ship。我要再写一遍这个函数,因此你不用向上翻页找它了。
update : Float -> Keys -> Ship -> Ship update dt keys ship = let newVel = toFloat keys.x isShooting = keys.y > 0 in updateVelocity newVel (updateShooting isShooting (applyPhysics dt ship))
若是 initShip
是这个 model 初始的状态,至少,我能够向前走一步了。Elm 程序定义了一个 main
函数,整个程序经过它开始运行。因此,首先让咱们试着显示 initShip
。我引入了 Graphics.Element
库来调用 show
函数。
import Graphics.Element exposing (..) -- (other code) main : Element main = show initShip
这给了咱们
{ position = 0, shooting = False, velocity = 0 }
如今,若是我想再前进一步,我能够在显示飞船以前调用一次 update
函数。我试了一下,看到了 keys
,因此左右键被按下时已经有效果了(x
是 -1,y
是 1)。
dt = 100 keys = { x = -1, y = 1 } main = show (update dt keys initShip)
咱们有了
{ position = 0, shooting = True, velocity = -1 }
很好!搞定了!按下向上键时个人飞船开始射击了,而且它有一个负的速度说明向左键也被按下了。请注意这时 position
尚未改变。这是由于我定义的更新的顺序是:先应用物理属性,而后才更新其余属性。 initShip
的速度是 0,因此改变物理值并无移动它。
如今我但愿你拿出一些时间来读一下 Elm-lang 的 Signals,若是你感兴趣,甚至能够看一两个关于 Elm Signals 的视频。从如今开始我假设你已经知道什么是 Signals 了。
再来总结一下:一个 signal 就像一个 stream,在任何一个时间点,都有一个简单的值。因此一个鼠标点击的 signal 的计数永远是一个整数 - 换句话说,它是一个 Signal Int
类型。若是我愿意,我也能够搞一个飞船的 signal: Signal Ship
,它能够一直保存着当前的 Ship
。可是我须要重构以前全部的函数并记录下那些复杂的值,事实上是那些值的 signals... 因此我遵从了来自 Elm-lang.org 的建议:
使用 signals 最常犯的错误是过多的使用它们。它会引诱你用 signals 作任何事情,但在你的代码中尽可能不使用它们才是坠吼滴!
因此,个人飞船能够再前进一步,可是它没有那么使人激动了。我想要当我按下向左键时它向左移动,反之亦然。更重要的是,我要按向上键时发射子弹!
事实上我已经用一种伟大的方法构建了个人 models 和逻辑,由于那里正好有个已经搞好的 signal 叫作 fps n
, 它更新 n
次每秒。它告诉咱们距离上次更新的时间差。这就是我须要的 dt
。并且,还有一个内置的 signal 被称做 Keyboard.arrows
,它保存了当前的方向键信息跟我定义的 Keys
彻底同样。不管什么时候只要发生变化,这些都会被更新。
好了,为了获得一个有趣的输入 signal,我会不得不联合这两个内置的 signals,以便 “当每次改变 fps
时,检查 Keyboard.arrows
的状态,并报告它们两个的值”。
"它们俩" 听起来像一个组合,(Float, Keys)
"在每一次更新" 听起来像 Signal.sampleOn
在代码中,这应该是下面这样:
import Time exposing (..) import Keyboard -- (other code) inputSignal : Signal (Float, Keys) inputSignal = let delta = fps 30 -- map the two signals into a tuple signal tuples = Signal.map2 (,) delta Keyboard.arrows -- and update `inputSignal` whenever `delta` changes in Signal.sampleOn delta tuples
碉堡了,如今我须要作的是只是接通个人 main
以使得用户输入能真正的被 update
函数得到到。为了实现它,我须要 Signal.foldp
,或者想个办法"抱紧过去"。这个跟搞个简单的 fold 差很少:
summed = List.foldl (+) 0 [1,2,3,4,5]
这里咱们从 0 开始,而后把它加上 1,再加上 2,以此类推,直到全部的数字被加在一块儿,最后咱们获得返回值为 15。
简单的说,这个颇有意义。foldp
一直记录着 "开始时间" 的值,而且整合全部 signal 的过去状态,直到当前这一刻 -- 整个应用完整的过去一步一步迭代到当前的状态。
个人天.. 让我喘口气。好了,至少如今好点了。
不管怎样,让咱们看看它在代码中是什么样的。如今,既然我有了 main
函数来更新它的结果,它应该也会在它的类型上反映出来,因此我会用一个 Signal Element
代替以前的 Element
。
main : Signal Element main = Signal.map show (Signal.foldp update initShip inputSignal)
这里发生了一些事情:
我使用 Signal.foldp
来更新 signal,初始值是 initShip
。
Folding
仍然返回一个 signal,由于它要继续更新 "folded 状态"。
我使用 Signal.map
把当前的 "folded 状态" 映射到 show
中。
只作这些会致使类型错误,尾部会有下面的报错:
Type mismatch between the following types on line 49, column 38 to 44: Temp9243.Ship -> Temp9243.Ship Temp9243.Keys It is related to the following expression: update
呃... 好吧,至少我知道了问题出在哪里。个人函数的类型签名看上去像这样:update : Float -> Keys -> Ship -> Ship
。然而,实际上我传给它的参数是 (Float, Keys)
和 Ship
。嗯,我只须要稍微修改下函数的签名...
update : (Float, Keys) -> Ship -> Ship update (dt, keys) ship = -- the same as before
... 嗒嗒,搞定了!
个人游戏如今有了一个完整的函数模型,须要的更新和其余任何东西,一共才 50 行代码!完整的代码在这看: game.elm。若想要看它的效果,你能够复制粘贴到 Try Elm 这个交互编辑器中(点击编译按钮按,在右边的屏幕上按下方向键)。
再来总结一下刚才发生了什么:
一个信号是一个时间的函数
每一个时间点都对应着一个 signal 纯粹的值
Signal.foldp
最后迭代出结果的原理与 List.foldl
同样
程序的每一个状态都是明确的起源于全部以前发生的事情
这些尝试让我学到了不少。我但愿你也同样能有所收获。我我的的主观感受是:
类型(Types)的确很是漂亮,并且有用
不可修改的数据结构(Immutability)和对全局状态的限制并无听起来那么难以接受
函数式编程在 Elm 中很是简洁,可读性很强
函数式编程使输入和输出清晰明确
由于全部的这些关于状态的想法是那么的不同凡响,它有些难以掌握,可是它确实颇有意义
由于每一个状态都是一个输入的直接结果,因此不须要担忧那些混合了各类状态的 bug
响应式地监听各类更改, 而不是主动地触发修改,这种感受很幸福
最后一句:若是你喜欢这篇文章,请把它分享给你的好基友。分享就是真爱!
不可变数据(Immutable data) 意思是一旦你给一个东西赋了值,它再也没法改变。拿 JavaScript 的 Array
来举个反例。若是它是不可变的,myArray.push(item)
就没法修改 myArray
已有的值,但它会返回一个新的追加了一个值的数组。
强类型 这种编程语言试图防止不可预知的行为致使的错误发生,例如:把一个字符串赋值为一个整数。当出现类型不匹配时 Scala、Haskell 和 Elm 这些语言使用 静态类型检查 来阻止编译经过。
纯函数(Pure functions) 给相同的输入永远给出相同的输出,并且没有任何反作用的函数。本质上,这些函数绝对不能依赖输入参数以外的任何东西,而且它不能修改任何东西。
函数式编程 特指以纯函数为主要表现形式的一种编程范式。
响应编程(Reactive programming) 归纳地说就是组件能够被监听,而且根据事件作出所须要的反应。在 Elm 中,这些可被监听的东西是 signals。使用 signal 的组件知道如何利用它,可是 signal 彻底不知道组件或组件们的存在。