Supercharge Your Flutter Apps With Apple Watch Integration
Leandro Pontes Berleze | Sep 25, 2024
There has been a lot of hype around the functional programming paradigm for some time and there are a lot of great books and articles about it on the internet, but it’s not so easy to find real examples with its application. So have I decided to create a game trying to follow its concepts using Javascript, which is the most popular programming language today. In this post, I will share some of this experience and tell you if it was worth it.
To keep it short, functional programming (FP) is a paradigm that attempts to reproduce the concept of mathematical functions, which is a relationship between the sets of domains (valid inputs) and the codomain (valid outputs). Mathematical function outputs are always related to just one input, so whenever a mathematical function is calculated with the same input, it returns the same output. This is one of FP’s most important concepts, also known as determinism.
Non-deterministic function example:
let x = 1 const nonDeterministicAdd = y => x + y nonDeterministicAdd(2) // 3 x = 2 nonDeterministicAdd(2) // 4
Deterministic function example:
const deterministicAdd = (x, y) => x + y deterministicAdd(1, 2) // 3
In addition to determinism, functions in FP seek not to cause changes beyond their scope. These types of functions are called pure. Last but not least, the data in FP must be immutable, meaning it cannot have its value changed after creation. These concepts together make testing, caching, and parallelism easier.
Beside these basic concepts, I also tried to use Point-free style during the development of the game, which aims to make the code cleaner as it omits the use of unnecessary parameters and arguments. Here¹² are a couple of good references about it.
Since the beginning of the project, the intention was to create a game that runs in the browser. Because Javascript (JS) is a language that I feel comfortable with and is a multi-paradigm language, it was language I chose for this project.
There are two excellent books about FP that I recommend:
The implemented project is a turn-based spaceship game. In the game, each player has 3 ships and on each turn the player must choose the place they want to move the spaceship within its reach range and in which direction they want to shoot. When the spaceship gets shot it loses part of its shield. If a spaceship has no shield left, it gets destroyed and the player who runs out of spaceships loses the game.
Initial turn of a match
So far, the game only allows for one player to participate, and this person controls the 3 spaceships at the top of the screen, facing a script that controls the 3 spaceships at the bottom, which randomizes the position and the target of its spaceships. Regarding the graphic part, I used the PixiJS package to control the rendering, which is the only production dependency of the project, and I also used spaceship sprites that were freely available from UnLucky Studio on the OpenGameart site.
In the beginning of the implementation, a file was created with base functions that were used in almost all project files. Some of these base functions exist natively in JS, such as map e reduce. JS also has several other functions that fit the FP paradigm by not changing their input values, and which were used in the project, such as filter, find, some, every. A good source for discovering these functions is Does it mutate. To follow the point-free style, it was also necessary to implement the following base functions:
const add = curry((x, y) => x + y) add(1, 2) // 3 add(1)(2) // 3
const addAndIncrement = compose( add(1), // previous add result + 1 add // arg1 + arg2 ) addAndIncrement(2, 2) // 5
There are several libraries in which these functions are already implemented, such as Ramda, but in this project, I decided to implement them to try to better understand how they work. This article was a great source to investigate how they might work and how you could implement these and other functions recursively.
To facilitate the composition of the native JS functions that were used, I created helpers using curry, where the entries are passed as arguments.
Example:
const filter = curry((fn, array) => array.filter(fn)) const getAliveSpaceships = compose( filter(isAlive), getSpaceships
Regarding the models’ implementations, we used the functional-shared style, in which a model instance is an object with its properties and functions. To manage the state of models, we created the following helper, where `getState` returns the state of the instance; `assignState` returns a new instance with the old state concatenated with the new one and` getProp` returns the value of the passed property encapsulated in a monad. Monad is a popular construction in FP, and is a bit hard to summarize an one-line definition, so here is an article that explains it in a friendly manner.
const modelFunctions = (model, state) => ({ getState: () => state, assignState: newProps => model({ ...state, ...newProps }), getProp: name => getProp(state, name), })
With this helper we can declare models, create instances and use their functions as follows:
const Engine = state => ({ ...modelFunctions(Engine, state) }) Engine({ a: 'a' }).assignState({ b: 'b' }).getState() // { a: 'a', b: 'b' }
Once you have the base functions and templates defined there is still a lot left to implement. Below are some of the project functions that give a good taste of readability and how easy it is to compose the functions.
const removeDestroyedSpaceships = player => compose( setSpaceships(player), getAliveSpaceships )(player)
export const reduceShield = curry((spaceship, damage) => compose( checkDestroyed, shield => assignState({ shield }, spaceship), shield => sub(shield, damage), getShield )(spaceship) )
Implementations made through composition are often easier to understand compared to imperative functional programming. This function, for example, was analyzed by SonarQube for cognitive complexity and received the best possible score.
export const getBullets = compose( either([]), getProp('bullets') )
Here it was possible to omit the function argument as it was only being used by one of the compound functions. There is also a guarantee that the returned value will be valid because ‘getProp’ returns a monad and ‘either’ returns the monad’s encapsulated value if it is a valid value or an empty array.
const setPosition = curry((coordinate, bullet) => compose( callListenerIfExist('onMove'), assignState({ coordinate }) )(bullet) )
FP composition requires functions to always have a return value. If ‘callListenerIfExist’ does not return any value, it would not be possible to chain other functions with it or with ‘setPosition’ after their execution.
This is the project repository and hosted on this link. Because I have not had any previous experience with FP, I had to refactor the project several times and also found FP hard to debug, because of things like stack tracing limit. But on the other hand, the functions are very readable and easy to be reused. I do not advise projects that have require ambition and short deadlines to be done using paradigms/technologies that you are not used to, but this project has been developed with the intention of learning. Avoiding using libraries and implementing the base functions was very helpful to understand how each one of them works, and the final package size was pretty much only the size of the PixiJS modules that were used.
A computer scientist who loves to study new technologies. Also enjoys rap, watching movies and TV shows, sports (especially soccer), and playing videogames.