Give Codeship’s CI/CD Platform a Try

Want to learn more?

In Part I of this series, I talked about the core concepts of functional programming and gave a few examples of how they come into play. The list of core concepts of functional programming (again from Part I) is as follows:

Usage of functions as input to and output from other functions, higher order functions

Usage of map, filter, and reduce type functions instead of looping

Immutable state

Recursion in place of looping

Composing functions from other functions

Distinguishing “pure” functions from functions with side effects

I gave a few examples using the first four items, so let’s get a little more down and dirty with those, adding in Number 5 as well.

Building an Efficient Chess AI with Elm

But first, a little information about chess programming:

Writing an AI for a chess-like game is quite an interesting undertaking. Real chess AIs have to be very complicated. Computers play chess in a much more brute force manner than humans do. They try to look at every possible sequence of moves and assign a value to the resulting position. This brute force approach is so work-intensive that without significant optimizations, an AI would take days to move and require huge amounts of memory as well.

The challenge of optimizing chess AI code has generated some wonderful algorithms like alpha-beta pruning and min-max searches. Deep Blue, the AI that beat world champion Garry Kasparov, had specialized “chess” chips built into it that made it many times faster than software-only solutions. Also, the Encyclopedia of Chess Openings contains the best moves for all of the popular openings, and these are hard coded into chess programs as data requiring no work on the part of the AI for the first ten to twenty moves of each game.

The AI for my pet project, Wounds (see Part I of this series), is very primitive by comparison. When evaluating a given position on the board, it will compare the strength of the armies (including wounded pieces), and it will compare their respective mobility just with a raw count of how many legal moves each team can make.

We will see how many moves we can look into the future without running out of some resource (including human patience).

For simplicity, let’s assume that the number of legal moves on each consecutive turn stays the same (it doesn’t; it usually increases with every move in the first part of the game). The first player, which in Wounds is the Red player, has about 30 legal moves at the start of the game. The AI would try each of these moves for ply 1, and then it would make each of the Blue team’s responses (ply 2) which would be 30 times 30, or 900. You see where this is going. It would next consider Red’s 27,000 second moves (ply 3) and Blue’s 810,000 replies to those (ply 4). It’s easy to run out of time and memory when you start looking farther than that.

Comparing Objective-C and Elm

When I wrote the code for this in Objective-C, before I had encountered functional programming, I saw that recursion would allow me to write clean code for this. When I implemented the recursive code, I found that at 5 plys deep the code would crash my iPad. There were two reasons for that: one, a copy of each board position needs to be saved before making the next move on a board, and two, Objective-C wasn’t optimized for recursion and so incurred additional overhead. Recursive calls (without optimization) eat up stack space, so that’s usually the weakest link.

Here we see a couple of calls to List.map, which takes a function as input. The function passed in also gets parameters if needed. And finally, a list is passed. The function will be called on every item in the list. List.map will return a list of results. In this case, I am passing around lists of moves. First it’s all of the legal moves, then those moves scored for mobility, then adding scores for material, then sorting so that we can grab the head of the list which will have the best score.

Notice how the result of each successive call is put into its own variable which gets passed to the next function. Here is my next attempt, where I utilize chaining to eliminate those temporary variables:

In Elm, the |> operator passes the output of the function preceding it as the last parameter to the function following it. If you compare the above code snippets, you will see that in the second snippet the parameter at the end of each line is missing. This parameter passing order is not standardized among languages.

For example, chaining functions in Elixir sends the output parameter as the first input parameter to the following function rather than as the last parameter. This looks similar to using a pipe operator that is used to connect UNIX command-line utilities. But under the hood so to speak, there is something more powerful going on.

!Sign up for a free Codeship Account

Partially applied functions

I lied a little bit when I said the output of one function is passed to the next function. What is actually being passed is called a partially applied function. The act of passing these around is called currying.

Currying is named in homage to Haskell Brooks Curry, a pioneer in combinatory logic. Not only does the verb ‘to curry’ refer to him and his work, but there are three functional programming languages also named in his honor: Haskell, Brooks, and Curry. Of these three, only Haskell has a significant following.

Passing around partially applied functions like this is another reason for the name functional programming. Yet another reason this name is apt is that functional programmers are very careful about what they call side effects.

In object-oriented programming, it is commonplace for objects to contain internal state and for their methods to both read from that state and write to it. Code and data become interwoven. If this isn’t carefully designed, there can be all kinds of hard-to-foresee side effects.

Functional programmers like to be very careful about these side effects. Given the same input, a “pure” function always returns the same output. Furthermore it doesn’t affect application state. Since very few applications can work without some application state, that needs to be addressed somehow. So there is still state, but it is handled very carefully and conscientiously.

In React/Redux for example, you handle state by coding Redux functions called “reducers.” Then you create “actions.” When Redux receives an action, it carefully applies any middleware, then calls all of the reducers. Reducers can act on actions that they are interested in or just pass the buck, so to speak. React/Redux is not a purely functional toolchain, but the authors say that they have been heavily influenced by Elm’s architecture.

The elegance of immutable state

Let’s talk about immutable state again, and this time go a little deeper into the elegance of using it in your code. “Pure” functional programming tools enforce immutable state. Semifunctional tools, like React (with Redux), both strongly suggest and also assume that you will adhere to the immutable state design pattern. For example is ReactJS you are allowed to call

this.state = ‘new state’;

in a constructor() but subsequent state changes are supposed to use

this.setState(‘new state’);

This appears not to be enforced, so I can imagine myself introducing bugs by forgetting this rule. Kind of like when I have typed = instead of ==. I bet that’s never happened to you ;).

Functional languages like Elixir and Elm return an updated copy of data rather than modifying the data itself. Intuitively, this would seem expensive both in terms of processing and in terms of memory, but actually these tools are optimized so it’s actually more efficient rather than less.

Each time I search a level (ply) deeper, I make a new copy of the board, then I make each move on that copy of the board and give the resulting position a score. This code was very hard to debug, requiring all kinds of instrumentation code to be able to stop at a certain depth and board position. It’s very slow, even on my iMac, which means it doesn’t play a very strong game.

Let’s go back to Elm for now, which requires that state be immutable. My first version of makeAIMove in Elm looked ahead one move when scoring mobility, and no moves when scoring material. This is very fast, but not too smart. It plays very aggressively, not considering how the opponent might respond. Here’s a smarter version that uses recursion to search to a depth controlled by an input parameter:

In the let section at the top, I define a local function setMoveScore that will get called for each move in the legalMoves list. setMoveScore recursively calls the outer function, pickBestMove. It’s an interesting pattern to have a local/internal function call the outer function recursively. It helps to think of setMoveScore as just a block of code rather than a discreet function.

The pattern {move | score = makeMove board move |> scoreBoard player} shows how Elm responds to state changes. This snippet returns a new copy of move with the score updated. This fits in well with the List.map function call that creates a new list by applying a function to each item in the old list.

Likewise the function call nextBoard = makeMove board move returns a new board with the position changed. Player.getPlayer works the same way. This is quite useful since we want to be able to use the original board and player in the last line of this function.

We don’t need to be managing these temporary boards at each ply depth, and making sure we undo the moves we make. That’s how the Objective-C example works. That’s relying heavily on mutable state.

The immutable state version is less bug prone. Eventually it becomes easier to understand as well. When I first started to wrap my head around FP concepts, I found it harder to understand than my object-oriented code. Now that I feel more adept, I am seeing what the appeal is here: a kind of addictive quality, where a correct solution to a problem has a feeling of elegance. In that moment, everything clicks.

Coming Up

Subscribe via Email

Over 60,000 people from companies like Netflix, Apple, Spotify and O'Reilly are reading our articles. Subscribe to receive a weekly newsletter with articles around Continuous Integration, Docker, and software development best practices.

We promise that we won't spam you. You can unsubscribe any time.

Join the Discussion

Leave us some comments on what you think about this topic or if you like to add something.