Although not as popular as imperative programming languages, functional programming has taught developers to write semantic code for decades. When working with imperative and object-oriented languages, developers are often obliged to think of language abstractions such as abstract classes and loops. In contrast, programmers feel empowered to think of the problem at hand when using functional languages, and there are many reasons for that.
Functional languages are much closer to mathematics, and there aren’t as many design patterns as you find in traditional programming paradigms. This absence of design patterns doesn’t mean that they are worse than popular dialects; the reason for that is there are no needs for complicating language semantics and structure.
I will criticize Kotlin here, but if you take a closer look at Kotlin complicated semantics compared with Clojure or Haskell, you’ll find that for extending imperative languages’ usefulness, language designers need to complicate the life of developers. The cool parts of Kotlin though all live inside the world of functional programming, and there is where I want to take you on this trip.
In this post, we will navigate the world of functional programming practically. We will do that by writing a straightforward rock-paper-scissor game. But we will do it differently than what most developers are used to in their daily work! We will use the concept of Domain-Specific Languages, and once we have our language designed, we will tackle the problem in a much more semantic way. I promise you won’t feel the pain of Haskell on this trip! Let’s start!
Defining your language entities
If we were to write the problem before understanding the abstractions that define it, we would probably write complicated code. In the case of a game like rock-paper-scissor, we have a move, and a move can be any of the three values. In Haskell, we can express that a move can be any of the three values as follows:
data Move = Rock | Paper | Scissor
Since we are writing a console application, there are player turns for selecting their move, so we can also think of player turns:
data PlayerTurn = Player1 | Player2
I don’t think there are more entities in a turn-based, selection-based game. We can now proceed to the next step.
Define your language interactions
Entities relate to each other, and it wouldn’t be semantic if we had to implement the details of the interactions every time we used the entities. For that, we need to understand those interactions. First, moves should be comparable to each other, which means we can say one move is equal to another, or greater than another. Haskell allows us to have these implementations with the keywords Eq and Ord (Equivalent and Ordered). What those two keywords mean, is that we can now use the compare function, as well as all the order operators (e.g. <, >, <=, >=, etc.).
There is one more aspect we need to consider. We expect users to select the moves based on their number, and if we don’t believe this data type to be enumerable, we will have to implement three conditions to transform data into our new Move data type. Another keyword enables us to use functions on top of our data types called Enum (Enumerable). The Enum keyword allows us to use two obvious functions to understand:´toEnum’ and ‘fromEnum’. They convert enum values to integers and vice-versa.
Let’s now implement these changes to our data types:
data Move = Rock | Paper | Scissor deriving (Eq, Ord, Enum)
Unfortunately, we have a problem with one of these keywords. You see, if we don’t specify a custom order for our Move sub-types, the order of the sub-types will be the authority in all comparisons, so we must implement our version of comparison. To clarify this paragraph, since Rock appears before Scissor in the data definition above, it means that Rock’s value is less than Scissor, while it beats Scissor. Let’s write an instance of the Ord class, and write our comparison function!
instance Ord Move where move1 <= move2 = move1 `tiedWith` move2 || move1 `lostTo` move2
The code above is still incomplete, but look how clear it is to understand the main idea. The benefit of top-down code writing is that anyone can read it. Now that we have the top definition, let’s dive deep and write the missing function references.
data Move = Rock | Paper | Scissor deriving (Eq, Enum) instance Ord Move where move1 <= move2 = move1 `tiedWith` move2 || move1 `lostTo` move2 where tiedWith x y = x == y lostTo x y = (x, y) `elem` [(Rock, Paper), (Paper, Scissor), (Scissor, Rock)]
There is one more code adjustment we want to bring to the table here. Still, it is the fact that it would be valuable to get the concept of converting PlayerTurn to integer and vice-versa, so let’s give it the enum class to our previously created data-type!
data PlayerTurn = Player1 | Player2 deriving Enum
Congratulations! You have now understood how to create your domain-specific language in two steps. First, you define your entities; then you define the relationships. Once it is ready, you can start writing code that glues everything together. It is prevalent for functional programmers to model state machines as DSLs, but since we are just writing a simple game, there is no need to make it complicated. Let’s now dive deep into writing the rest of the code.
The basic idea here will be to give the instructions on how to input moves to each player. We will do it turn-based, so we will first ask player 1 for their move input, and then player 2. Once both players have successfully inputted their moves, we can compare both movements and output the result. Since the idea here is understood, let’s write our ‘main’ function:
main :: IO () main = do writeInstructions Player1 player1Move <- readMove writeInstructions Player2 player2Move <- readMove writeResult player1Move player2Move
Crystal clear! There is no doubt about what we have written in the main function, but we still have some functions we need to write. They are: ‘writeInstructions’, ‘readMove’ and ‘writeResult’. The first one is straightforward to read and write as it deals with writing things on the console.
writeInstructions :: PlayerTurn -> IO () writeInstructions turn = do let playerNum = show $ (+1) $ fromEnum turn putStrLn $ "Player " ++ playerNum ++ " pick a move:" putStrLn " 0 = Rock" putStrLn " 1 = Paper" putStrLn " 2 = Scissor"
This next one is a bit more complicated. The function ‘readMove’ reads values from the terminal, and since Haskell tries to be a pure functional language, it boxes values in what we call functors. To execute operations inside the box, we need a function called fmap, also called functor map. This fmap function has an alias, which is: <$>. This unique function executes operations inside the box, so we do that by using the composition of ‘toEnum’ followed by ‘read’. I know, this was the only complicated part of this article, but I promise that won’t affect your understanding of the general code.
readMove :: IO Move readMove = (toEnum . read) <$> getLine
Finally, we can write the last function: ‘writeResult’. Since we have implemented our Ord class above, we can use the compare function output in a switch-like statement and give the outcomes that we want. Since the input suffers from the same IO problem as in the previous function, let’s separate the pure from the impure part, and write two functions:
getResult :: Move -> Move -> String getResult move1 move2 = let comparison = compare move1 move2 in case comparison of GT -> "Player 1 wins!" EQ -> "It's a draw!" LT -> "Player 2 wins!" writeResult :: Move -> Move -> IO () writeResult move1 move2 = putStrLn $ getResult move1 move2
As you can see, our code ended up like this:
module Main where data Move = Rock | Paper | Scissor deriving (Eq, Enum) instance Ord Move where move1 <= move2 = move1 `tiedWith` move2 || move1 `lostTo` move2 where tiedWith x y = x == y lostTo x y = (x, y) `elem` [(Rock, Paper), (Paper, Scissor), (Scissor, Rock)] data PlayerTurn = Player1 | Player2 deriving Enum writeInstructions :: PlayerTurn -> IO () writeInstructions turn = do let playerNum = show $ (+1) $ fromEnum turn putStrLn $ "Player " ++ playerNum ++ " pick a move:" putStrLn " 0 = Rock" putStrLn " 1 = Paper" putStrLn " 2 = Scissor" readMove :: IO Move readMove = (toEnum . read) <$> getLine getResult :: Move -> Move -> String getResult move1 move2 = let comparison = compare move1 move2 in case comparison of GT -> "Player 1 wins!" EQ -> "It's a draw!" LT -> "Player 2 wins!" writeResult :: Move -> Move -> IO () writeResult move1 move2 = putStrLn $ getResult move1 move2 main :: IO () main = do writeInstructions Player1 player1Move <- readMove writeInstructions Player2 player2Move <- readMove writeResult player1Move player2Move
It is a bit overwhelming to switch to functional programming mindset. Still, once you understand the importance of modelling you DSLs before implementing code, you will start benefitting from the grace of functional programming languages.
If you take a close look at functions individually, you’ll see they are much closer to English than using imperative languages. Some cool functional languages like the LISP family allows us to write macros and extend the language features.
I hope you have enjoyed this Haskell journey!
If you want to know more about my work, feel free to check out my LinkedIn profile: