疯狂的技术宅 前端先锋javascript
翻译:疯狂的技术宅
做者:Karran Besen
来源:cheesecakelabs
正文共:2530 字
预计阅读时间:8 分钟前端
一段时间以来,函数式编程范式比较火热,而且在互联网上有不少关于它的精彩书籍和文章,可是要找到相关程序的真实示例并不容易。所以,我决定尝试使用 Javascript(当今最流行的编程语言)并遵循其概念建立一款游戏。在本文中,我将分享一些经验,并告诉你是否值得。java
简而言之,函数式编程(FP)是试图重现数学函数概念的范式,数学概念是域集(有效输入)和共域(有效输出)之间的关系。数学函数的输出始终仅与一个输入相关,所以,只要使用相同的输入来计算数学函数,它就会返回相同的输出。这是函数式编程最重要的概念之一,也称为肯定性。git
不肯定函数示例:es6
1let x = 1 2const nonDeterministicAdd = y => x + y 3nonDeterministicAdd(2) // 3 4x = 2 5nonDeterministicAdd(2) // 4
肯定性函数示例:github
1const deterministicAdd = (x, y) => x + y 2deterministicAdd(1, 2) // 3
除了肯定性以外,FP 中的函数还寻求不引发超出其范围的修改。这些类型的功能称为 pure。最后但并不是最不重要的一点是,FP 中的数据必须是不可变的,这意味着建立后不能更改其值。这些概念使测试、缓存和并行性更加容易。编程
除了这些基本概念以外,我还尝试在游戏开发期间使用无点样式,该样式可以使代码更简洁,由于它省略了没必要要的参数和参数的使用。如下两个连接给你提供了很好的参考。数组
我推荐两本关于 FP 的优秀书籍:浏览器
咱们的项目是一个基于回合制的太空飞船游戏。在游戏中,每一个玩家有 3 艘飞船,而且每回合必须选择他们要在其可达范围内移动飞船的位置以及要朝哪一个方向射击。当飞船被射中时,它将失去部分防御罩。若是宇宙飞船没有防御罩将被摧毁,失去全部宇宙飞船的玩家将输掉比赛。缓存
比赛的初始轮
到目前为止,该游戏仅容许一个玩家参与,而且控制屏幕顶部的 3 个太空飞船,去对抗一个控制底部 3 个太空飞船的脚本,该脚本将其太空飞船的位置和目标随机化。关于图形部分,我使用了 PixiJS 程序包来控制渲染,这是该项目惟一的依赖项,而且我还使用了从OpenGameart 网站上的 UnLucky Studio 免费得到的太空飞船精灵 。
在开始,咱们先建立一个文件,其中包含几乎全部项目文件中都会用到的基本函数。其中一些基本函数是 JS 固有的,例如 map 和 reduce。JS还有一些其余功能,它们经过不更改输入值而适合FP范例,而且已在项目中使用,例如 filter, find, some, every。发现这些功能的一个很好的来源是Does it mutate。要遵循无点样式,还必须实现如下基本函数:
1const add = curry((x, y) => x + y) 2add(1, 2) // 3 3add(1)(2) // 3
1const addAndIncrement = compose( 2 add(1), // previous add result + 1 3 add // arg1 + arg2 4) 5addAndIncrement(2, 2) // 5
已经在其中实现了这些函数的几个库,例如 Ramda,可是在这个项目中,我决定实现它们以试图更好地理解它们的工做原理。这篇文章(https://medium.com/dailyjs/functional-js-with-es6-recursive-patterns-b7d0813ef9e3) 是研究它们如何工做以及如何递归实现这些功能的重要资料。
为了简化所使用的本机 JS 函数的构成,我使用 curry 建立了helper,其中条目做为参数传递。
例:
1const filter = curry((fn, array) => array.filter(fn)) 2const getAliveSpaceships = 3 compose( 4 filter(isAlive), 5 getSpaceships
关于模型的实现,咱们使用了 functional-shared 样式,其中模型实例是具备其属性和函数的对象。为了管理模型的状态,咱们建立了如下 helper,其中 getState 返回实例的状态。assignState 返回一个新实例,旧状态与新实例链接在一块儿,getProp 返回封装在 monad 中的传递属性的值。Monad 在函数式中是一种流行的构造,而且很难总结出一个简介的定义,这篇文章对其作了一个很好的解释:https://jrsinclair.com/articles/2016/marvellously-mysterious-javascript-maybe-monad/。
1const modelFunctions = (model, state) => ({ 2 getState: () => state, 3 assignState: newProps => model({ ...state, ...newProps }), 4 getProp: name => getProp(state, name), 5})
使用这个 helper,咱们能够声明模型、建立实例并使用其函数,以下所示:
1const Engine = state => ({ ...modelFunctions(Engine, state) }) 2Engine({ a: 'a' }).assignState({ b: 'b' }).getState() // { a: 'a', b: 'b' }
定义了基本函数和模板后,仍有许多工做要作。下面是项目的其它一些函数,这些函数的可读性很好。
1const removeDestroyedSpaceships = player => compose( 2 setSpaceships(player), 3 getAliveSpaceships 4)(player)
减小飞船的护罩
8)
与命令式编程相比,经过组合实现的代码一般更易于理解。例如我用 SonarQube 分析了此函数的认知复杂性,并得到了最高分。
1export const getBullets = compose( 2 either([]), 3 getProp('bullets') 4)
在这里能够省略函数参数,由于它仅由复合函数使用。还能够保证返回的值将是有效的,由于 getProp 返回一个 monad,而 either 返回一个 monad 的封装值(若是它是有效值或空数组)。
1const setPosition = curry((coordinate, bullet) => 2 compose( 3 callListenerIfExist('onMove'), 4 assignState({ coordinate }) 5 )(bullet) 6)
函数式编程的组合要求函数始终具备返回值。若是 callListenerIfExist 未返回任何值,则执行后将没法与其余函数或 setPosition 连接其余函数。
原文连接
https://cheesecakelabs.com/blog/functional-programming-game-js/