瘋狂的技術宅 前端先鋒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/