How a Java Programmer Wrote Console Tetris In Haskell
And What The Learning Curve Was Like
Introduction
Project structure
Concurrency
Conclusions
What's next?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
TL;DR
Java version repo: https://github.com/shiraeeshi/jtetr-first
Haskell version repo: https://github.com/shiraeeshi/hstetr-first
- master
branch - single-threaded version
- recursive-handle-tetris-commands
- version with channels and the cycle is started using recursive function
- chan
branch - version with channels and the cycle is initiated using fold
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Introduction
An article http://eed3si9n.com/console-games-in-scala
- describes control sequences that allow to print in any arbitrary location on the screen, rather than in line-by-line manner
- describes how to read arrow button press events
decided to implement in haskell, wrote some preliminary code that reads arrow button presses from console, didn't know how to write tetris in functional style, came across a comment about fs2, been reading about iteratee, postponed tetris, lost preliminary code.
have read a book "Learn you a haskell for great good" (LYAH)
an idea was suggested to write tetris, implemented it in java, then decided to translate it to haskell.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Project structure
figure
, arena
, ConsoleView
, main
a figure is implemented as an immutable structure, instead of rewriting a field value returns another version of itself.
an arena stores playing field cells and a figure.
a ConsoleView
prints an arena to the console.
main
glues everything together: reading of input symbols, reaction to commands, showing an arena, starting and stopping a timer.
first implemented without a timer, and after that added a timer and concurrency.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Concurrency
there is a channel (concurrent queue)
a timer sends a "tick" command to the channel
a recursive keys2commands
function translates read symbols into commands, which it then sends to channel
some function reads commands from the channel and passes them to the handleTetrisCommand
function which reacts to them
handleTetrisCommand
receives game state and a command as an input, shows an arena and returns the next state of a game
two ways of repeatedly invoking handleTetrisCommand
:
- using recursive function
- using foldM
when handleTetrisCommand
is invoked by foldM
, it works because lists are lazy in haskell
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Conclusions
specific and abstract
haskell is not as strictly functional as I thought
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
What's next?
what part of code is pure functional and what part is not pure?
exit game functionality is not implemented yet, one way of exiting game is through interrupting the console
optimize changing values in cells matrix
how to introduce more functional stuff into code, like monads, monad transformers, etc.?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Introduction
There's a tradition to print Hello World to the console when beginning learning a language. Let's call printing Hello World to the console a first level of interactivity. A next level is when a program gets an input from a user and prints the result to the console. Sounds similar to the way functions work.
One of the merits of a functional style is said to be the fact that functions don't produce side effects. Side effects have the potential to over-complicate the code, making it resemble a bowl of spagetti (it is called spagetti-code), and unwanted side effects are errors, bugs. When we use functions, we don't have side effects, so from the beginning we eliminate the possibility of introducing an unwanted side effect into the code.
Sounds convincing, but what does it look like in practice when we're dealing with something more sophisticated than Hello World or a console calculator?
We want to write something complex and intertwined in functional style and check if spagetti is going to get untangled because we used functions, and if so, what will it look like?
Are we going to write some UI or a server, or maybe an operation system kernel? It would be too far of a jump on a learning curve, which seems to be pretty steep in haskell's case. We got to flatten the learning curve.
It's got to be console application. But here is a thing: how are we going to test if functions help us untangle the code, given that console works similar to functions in line-by-line input-output mode?
This article helps us resolve the problem: http://eed3si9n.com/console-games-in-scala
It describes:
- control sequences of symbols that let us print in any location on screen, rather than line by line.
- how to read arrow button pressed events.
In this way it's possible to use console on a higher level of interactivity.
After having read the article, I decided to translate the code to haskell, wrote some preliminary code that reads arrow button presses from console, and reacts to them by moving a figure on a screen.
An article proceeds by adding a code that periodically prints some text. The program's flow got divided into several threads working concurrently. I didn't know how to write something like that in haskell. The learning curve jumped high and became a wall in front of me. Actually it was me who made things more complicated, because I've been searching in the wrong place.
In the comment to an article someone mentioned fs2 library, I started searching it's equivalent for haskell and reading about the library. I've read in some discussion that the library consists of a set of instruments and there are various options of how to use each of them, developers have coded some of the options, but there are too many combinations of instruments and options to code each one, come up with a name for it, describe it in a documentation and make users memorise all the names. That strange situation discouraged me from using the library.
I kept the preliminary code only on hard disk, never uploaded it to any kind of cloud, and it so happened that it was lost. Perhaps you need to upload code into repos on cloud, even if it's just some draft code you are playing with. Sometimes such little things determine whether you keep progressing or procractinating.
Later I came across an idea of iteratees and got an impression that they are related to a functional style.
EDIT: actually, I forgot that I've read about fs2 and iteratees in this article that was written as a response to "Console games in Scala" article, in which the code was refactored to get rid of global shared mutable state: http://m50d.github.io/2018/09/12/streaming-console-game
Iteratees were implemented in haskell first, and then were adopted in other languages.
In search results I saw links to articles about iteratees on scala, skimmed through it, but I wanted to get the knowledge from the root, so to speak. I needed articles about iteratees on haskell. I followed the link from the wikipedia article to an article in "The Monad Reader" journal. "Iteratee: Teaching an Old Fold New Tricks" by John W. Lato ( https://themonadreader.wordpress.com/2010/05/12/issue-16 ).
In that article the author created a data type, created Monad, Applicative and Functor instances for the data type and suggested to figure out the flow of execution of one method (>>=) as an exercise. I tried to do that, got lost in intricacies of function invokations and now iteratees seem to me like something complicated and tricky. Are newbie's brains too OOP to understand that stuff or maybe that article is a little too advanced? I think I was lucky enough to bump into the wrong article.
Are exercices a good or a bad thing? They are a good thing if you know how to use them as a tool. They are bad when the inner perfectionist refuses to continue with the job until the exercise is done, but doesn't want to do the exercise.
This dead-end branch of learning ended by me switching focus to scala because of an interview. Just a vague feeling remained that I wanted to write tetris on haskell and didn't know how to do that; that in order to do that I needed to understand how iteratees work and finish a hard and confusing exercise. Haskell is so hard to learn! In fact, I didn't even need iteratees to write tetris. This is an example of how the wrong strategy of learning can lead to a dead end and demotivate. Seems like being able to simplify the task at hand is a skill too, and you need to learn and train yourself to simplify things.
Have read the "Learn you a haskell for great good" (LYAH) book. An easy to read, engaging and interesting book.
I heard that there aren't much books and articles like that in haskell world. Maybe I'm just not very skilled in finding good articles. Do readers have any recommendations?
I decided to take a break from anything related to programming, later I started reading books to refresh memory, but didn't code because I could not come up with any project idea, all ideas seemed boring, not interesting enough to start coding.
In some programming-related community in the comment section someone asked about things he can do to get the feeling of what programming is like and to understand whether he likes it or not. Someone answered and mentioned among other things doing a lot of small projects to practice. I commented and described my situation, that I resort to just reading books about programming and don't code, I want to use in practice the concepts I'm reading about, but can't come up with an idea interesting enough. They gave several examples of small project ideas, and the tetris was one of them. The idea of writing tetris didn't seem very interesting to me at that time, but I decided to write it anyway, not to end up as someone who asks to list ideas and then does nothing. I've been reading about concurrency in java, so I decided to use that language. Surprisingly the idea that seemed boring and not worth coding became interesting when an open editor appeared on the screen. I've coded tetris on java in three coding sessions giving in total about nine hours. This experience showed that sometimes it makes sense to start doing something that seems uninteresting, sometimes things become interesting after you start doing something and until you start they seem boring. It may seem to you that you don't want to do something, that you will spend time struggling with boredom and lack of motivation, but sometimes it only seems that way. Another conclusion is that sometimes an open editor makes things more interesting than speculative ideas. Sometimes interactive things are more interesting than hypothetical ideas.
So, somebody suggested an idea to write tetris, I implemented it in java, then decided to translate it to haskell.
Java version repo: https://github.com/shiraeeshi/jtetr-first
Haskell version repo: https://github.com/shiraeeshi/hstetr-first
Speaking in terms of MVC, I translated a model and a view from java, and wrote haskell version of a controller from scratch.
When writing a model and a view, I didn't know how to write a controller logic. Decided to code what I can, and later things will become more clear.
What is the name of such an upproach to design, "bottom up" or "from specific to abstract"?
When you don't know what to do, one approach is to build an infractructure first, build peripheral things and then try various combinations and think about how to tie them together.
Sounds like going from periphery to the center. That's one approach. Another is the opposite: going from center to periphery, or top-to-bottom.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Project structure
The program consists of several modules: Figure
, Arena
, ConsoleView
, Main
.
A figure is implemented as an immutable structure, instead of rewriting a field value it returns another version of itself.
data Figure = IShape {center :: Point, isVertical :: Bool} | JShape {center :: Point, direction :: Direction} | LShape {center :: Point, direction :: Direction} | OShape {center :: Point} | SShape {center :: Point, isVertical :: Bool} | TShape {center :: Point, direction :: Direction} | ZShape {center :: Point, isVertical :: Bool}
An arena stores playing field cells and a figure.
data Arena = Arena {figure :: Figure, cells :: [[Bool]], width :: Int, height :: Int}
A ConsoleView
prints an arena to the console.
printArena :: Int -> Int -> Arena -> IO () printArena arenaWidth arenaHeight arena = do saveCursor clearScreen drawBox 0 0 (arenaWidth + 2) (arenaHeight + 2) drawFigure arenaHeight (figure arena) drawBricks arenaHeight (getBricksOnTheFloor arena) when (hasFullRows arena) (drawFullRows arenaWidth arenaHeight . findFullRowsIndices $ arena)
A figure uses Point
and Direction
.
type Point = (Int, Int) data Direction = ToLeft | ToRight | ToUp | ToDown
Point
is a type alias representing a pair of numbers
Figures can be moved around and rotated.
How does figure return another version of itself when mutated while being immutable?
Let's start with functions that determine neighbors of a point.
neighborLeft :: Point -> Point neighborLeft (x, y) = (x - 1, y) neighborRight :: Point -> Point neighborRight (x, y) = (x + 1, y) neighborAbove :: Point -> Point neighborAbove (x, y) = (x, y + 1) neighborBelow :: Point -> Point neighborBelow (x, y) = (x, y - 1)
A pair of numbers is an immutable structure, it means that instead of changing anything in the original point we return a new point with a new value.
Those functions get called by functions that operate on figure, for example moveLeft
:
moveLeft :: Figure -> Figure moveLeft (IShape center isVertical) = IShape (neighborLeft center) isVertical moveLeft (JShape center direction) = JShape (neighborLeft center) direction moveLeft (LShape center direction) = LShape (neighborLeft center) direction moveLeft (OShape center) = OShape (neighborLeft center) moveLeft (SShape center isVertical) = SShape (neighborLeft center) isVertical moveLeft (TShape center direction) = TShape (neighborLeft center) direction moveLeft (ZShape center isVertical) = ZShape (neighborLeft center) isVertical
They work the same way as functions that find a point's neighbors: instead of changing anything in the original figure thay return a new figure with a new center.
Now let's move on to the function moveCurrentFigureLeft
from the Arena
module. This function's behaviour depends on game state: if a figure can be moved to the left, then this function returns a new arena (which stores a new figure with a new center), and if a figure can't be moved because of an obstacle, the function returns the original arena unchanged. The function setFigureIfPossible
returns an arena paired with a boolean indicator that says whether the function changed a figure or not.
moveCurrentFigureLeft :: Arena -> Arena moveCurrentFigureLeft arena = fst $ setFigureIfPossible (moveLeft (figure arena)) arena setFigureIfPossible :: Figure -> Arena -> (Arena, Bool) setFigureIfPossible figure arena = if figureIsPossible figure arena then let newArena = Arena figure (cells arena) (width arena) (height arena) in (newArena, True) else (arena, False)
The Main
module glues everything together: reading of input symbols, reaction to commands, showing an arena, starting and stopping a timer.
A figure in tetris descends periodically, let's say some timer makes a figure descend.
I've implemented a version without a timer first, because without a timer you can write single-threaded code with no concurrency.
Let's first examine the earlier version without a timer.
Reading input symbols (from https://stackoverflow.com/a/38553473/8569383)
Depends on console's buffering (hSetBuffering stdin NoBuffering
) and echo (hSetEcho stdin False
) settings.
getKey :: IO [Char] getKey = reverse <$> getKey' "" where getKey' chars = do char <- getChar more <- hReady stdin (if more then getKey' else return) (char:chars)
Reacting to commands.
In an earlier version of a module reaction to commands is implemented in withArena
function.
This function shows an arena, reads button presses and recursively calls itself passing a new arena as a parameter.
(Level
is utility module for generating new figures through invoking nextFigure
).
withArena :: Level -> Arena -> IO () withArena level arena = do printArena 20 20 arena key <- getKey when (key /= "\ESC") $ do case key of "\ESC[A" -> do -- up withArena level (rotateCurrentFigureClockwise arena) "\ESC[B" -> do -- down let (newArena, descended) = descendCurrentFigure arena if descended then withArena level newArena else do let fixedFigureArena = fixCurrentFigure newArena (newFigure, newLevel) = nextFigure level newFigureArena = fst $ setFigureIfPossible newFigure fixedFigureArena withArena newLevel newFigureArena "\ESC[C" -> do -- right withArena level (moveCurrentFigureRight arena) "\ESC[D" -> do -- left withArena level (moveCurrentFigureLeft arena) "\n" -> return () _ -> return ()
The main
function creates empty cells, an arena, configures a console, shows an arena and calls withArena
.
Here's a whole code of a Main
module's version without a timer:
module Main where import Arena import Level import ConsoleView import Figure ( Figure (ZShape) ) import System.IO (stdin, hSetEcho, hSetBuffering, hReady, BufferMode (NoBuffering) ) import Control.Monad (when) main :: IO () main = do let emptyCells = take 20 (repeat (replicate 20 False)) arena = Arena (ZShape (15,10) True) emptyCells 20 20 level = initlevel hSetBuffering stdin NoBuffering hSetEcho stdin False printArena 20 20 arena withArena level arena withArena :: Level -> Arena -> IO () withArena level arena = do printArena 20 20 arena key <- getKey when (key /= "\ESC") $ do case key of "\ESC[A" -> do -- up withArena level (rotateCurrentFigureClockwise arena) "\ESC[B" -> do -- down let (newArena, descended) = descendCurrentFigure arena if descended then withArena level newArena else do let fixedFigureArena = fixCurrentFigure newArena (newFigure, newLevel) = nextFigure level newFigureArena = fst $ setFigureIfPossible newFigure fixedFigureArena withArena newLevel newFigureArena "\ESC[C" -> do -- right withArena level (moveCurrentFigureRight arena) "\ESC[D" -> do -- left withArena level (moveCurrentFigureLeft arena) "\n" -> return () _ -> return () getKey :: IO [Char] getKey = reverse <$> getKey' "" where getKey' chars = do char <- getChar more <- hReady stdin (if more then getKey' else return) (char:chars)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Concurrency
Now there's one thing left to do: we need to refactor our single-threaded code into multi-threaded one, in other words, to add a timer into our tetris.
A timer is supposed to periodically descend the figure.
One implementation of timer functionality is in module Control.Concurrent.Timer
(https://hackage.haskell.org/package/timers-0.2.0.3/docs/Control-Concurrent-Timer.html)
Now we know how to start a timer, but at this point we face some challenges.
Let's say we started to write a code that the timer will periodically invoke. We can get a new version of an arena from an old version by invoking descendCurrentFigure
.
let (newArena, descended) = descendCurrentFigure arena
The main
function invokes withArena
function, which then keeps repeatedly calling itself, passing each time a new version of an arena to itself as a parameter.
How to make a timer interfere with this cycle of recursive calls?
Imagine that we lined up all the recursive invokations of withArena
function and numbered each invokation's arena argument: withArena arena1
- withArena arena2
- withArena arena3
- etc. (let's ignore the level
argument for clarity).
n-th invokation of withArena
function receives n-th version of an arena as an input, creates an n+1-th version out of it and passes it as a parameter to the next invokation. We can't change that new version of an arena from the outside.
At this point in our coding journey we are reminded that we are coding in functional style with no side effects and with immutable structures.
In java a timer could alter some object's state through side effects visible from other threads.
But haskell challenges us to think about a puzzle.
(If you know any interesting ways of solving this problem, let us know).
Here we can remember one of the basic patterns of inter-thread communication - queues.
There is indeed an implementation of a concurrent queue in haskell with a neat simple interface (http://hackage.haskell.org/package/base-4.14.0.0/docs/Control-Concurrent-Chan.html).
There are other implementations available, but I tried to choose the simplest options.
Creators of that module chose the word channel to describe a queue.
Although a channel is conceptually is a mutable structure, it allows us to design our architecture in such a way that a channel rids domain model structures from mutability, we can gather the mutability in one place, a channel. By the way, it resembles an actor model in that each actor have a queue attached to it.
Now that we have a channel, we don't have to make timer interfere with the cycle of withArena function recursively calling itself, instead we are going to make a timer send a message (a command) to the channel.
We are going to rewrite the withArena
function to make it read commands from the channel and react to them.
Now, before the changes we are about to make, withArena
function has several responsibilities: it shows an arena on the screen, reads button presses and reacts to them.
Let's divide it into two functions:
- handleTetrisCommand
function will show an arena on a screen and react to commands it got from the channel.
- keys2commands
function will read button presses and send commands to the channel.
These functions must run in two threads at the same time.
I chose the simplest way of dividing the flow of the program into several threads - a concurrently
function from Control.Concurrent.Async
module (https://hackage.haskell.org/package/async-2.2.2/docs/Control-Concurrent-Async.html).
TetrisCommand
data type represents commands, messages that threads send each other.
data TetrisCommand = CmdRotate | CmdDescend | CmdRight | CmdLeft | CmdTick | CmdPauseOrResume
A timer periodically performs an action defined in timerTick
function, it sends a CmdTick
command to the channel.
timerTick :: Chan TetrisCommand -> IO () timerTick chan = do writeChan chan CmdTick
The code from the main
function that creates a channel and a timer looks like this:
chan <- newChan timer <- repeatedTimer (timerTick chan) (msDelay 300)
TetrisState
data type represents a state of a game, whether it is running or paused.
data TetrisState = TetrisStateRunning Arena Level (Chan TetrisCommand) TimerIO | TetrisStatePaused Arena Level (Chan TetrisCommand)
keys2commands
function reads button presses, translates them to commands, writes commands to the channel, and then recursively calls itself.
keys2commands :: Chan TetrisCommand -> IO () keys2commands chan = do key <- getKey when (key /= "\ESC") $ do case key of "\ESC[A" -> do -- up writeChan chan CmdRotate keys2commands chan "\ESC[B" -> do -- down writeChan chan CmdDescend keys2commands chan "\ESC[C" -> do -- right writeChan chan CmdRight keys2commands chan "\ESC[D" -> do -- left writeChan chan CmdLeft keys2commands chan "\n" -> do -- enter writeChan chan CmdPauseOrResume keys2commands chan _ -> return ()
handleTetrisCommand
function receives a game state and a command as an input, prints an arena and returns a new state of a game (wrapped with IO
monad).
handleTetrisCommand :: TetrisState -> TetrisCommand -> IO TetrisState handleTetrisCommand tetrisState CmdRotate = do let arena = getArenaFromState tetrisState newArena = rotateCurrentFigureClockwise arena printArena 20 20 newArena return $ stateWithNewArena tetrisState newArena handleTetrisCommand tetrisState CmdDescend = do let arena = getArenaFromState tetrisState (newArena, _) = descendCurrentFigure arena printArena 20 20 newArena return $ stateWithNewArena tetrisState newArena handleTetrisCommand tetrisState CmdRight = do let arena = getArenaFromState tetrisState newArena = moveCurrentFigureRight arena printArena 20 20 newArena return $ stateWithNewArena tetrisState newArena handleTetrisCommand tetrisState CmdLeft = do let arena = getArenaFromState tetrisState newArena = moveCurrentFigureLeft arena printArena 20 20 newArena return $ stateWithNewArena tetrisState newArena handleTetrisCommand (TetrisStateRunning arena level chan timer) CmdTick = do let (newArena, descended) = descendCurrentFigure arena if descended then do printArena 20 20 newArena return $ TetrisStateRunning newArena level chan timer else do let fixedFigureArena = fixCurrentFigure newArena (newFigure, newLevel) = nextFigure level (newFigureArena, newFigureWasSet) = setFigureIfPossible newFigure fixedFigureArena if newFigureWasSet then if hasFullRows newFigureArena then do printArena 20 20 fixedFigureArena stopTimer timer let noFullRowsArena = removeFullRows newFigureArena oneShotTimer (writeChan chan CmdPauseOrResume) (msDelay 500) return $ TetrisStatePaused noFullRowsArena newLevel chan else do printArena 20 20 newFigureArena return $ TetrisStateRunning newFigureArena newLevel chan timer else do stopTimer timer return $ TetrisStateRunning newFigureArena newLevel chan timer handleTetrisCommand (TetrisStateRunning arena level chan timer) CmdPauseOrResume = do stopTimer timer return $ TetrisStatePaused arena level chan handleTetrisCommand (TetrisStatePaused arena level chan) CmdPauseOrResume = do timer <- repeatedTimer (timerTick chan) (msDelay 300) printArena 20 20 arena return $ TetrisStateRunning arena level chan timer
handleTetrisCommand
function is not recursive, you can start the cycle using another recursive function or using the foldM
function.
Recursive function version is in recursive-handle-tetris-commands
branch in repo, foldM
version is in chan
branch.
We can create the cycle using recursive function that invokes handleTetrisCommand
function.
keepHandlingTetrisCommands :: TetrisState -> Chan TetrisCommand -> IO () keepHandlingTetrisCommands tetrisState chan = do cmd <- readChan chan newState <- handleTetrisCommand tetrisState cmd keepHandlingTetrisCommands newState chan
The main
function divides the program's flow of execution to two threads using concurrently
:
concurrently (keepHandlingTetrisCommands arenaWithLevel chan) (keys2commands chan)
Alternatively, instead of using recursive function to cycle you can use foldM
that invokes handleTetrisCommand
function.
I wanted to try this method to see the laziness of haskell lists in action.
commands <- getChanContents chan concurrently (foldM handleTetrisCommand arenaWithLevel commands) (keys2commands chan)
commands
is a list of commands extracted from the channel.
concurrentrly
divides the program flow into two threads.
One of them runs foldM handleTetrisCommand arenaWithLevel commands
, the other one runs keys2commands chan
.
How does foldM
work? It is a monadic version of fold (there is a left fold and a right fold - foldl
and foldr
). (http://learnyouahaskell.com/higher-order-functions#folds)
fold resembles the reduce
function in javascript.
We extract the commands list from the channel before invoking the function that sends commands to the channel, so you would think that it must be empty.
If the commands list was empty, then the foldM handleTetrisCommand
thread would do nothing, because there are no commands to handle.
It doesn't work that way because the laziness of haskell lists: you can create lists that are not fully constructed yet and to add items later. You can even create infinite lists.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Conclusions
Thinking about abstract things lead to a dead-end. It was the case of dealing with unknown unknowns, when you don't know what you don't know, so you don't know what to think about and what is the next step.
One of the branches of that way was "learning libraries" with no connection to anything tangible.
Results were achieved by going from concrete to abstract.
One of the reasons was that I had a wrong impression about a language that it was purely functional, that it was impossible to write imperative code in haskell. That's why I wanted to learn a purely functional language: I couldn't imagine how would one design and organize compicated and intertwined code using only functions and immutable objects. Also I heard about reactive streams, that also influenced the impression.
It turns out haskell is not so strict as I thought when it comes to functional style. For example, a writeChan
function from Control.Concurrent.Chan
module: it's result is wrapped with IO
monad, it gives us a clue that we are not dealing with a pure function, but even conceptually this function is imperative by design, it behaves like an imperative void method. I thought that in haskell world a channel would return a new version of itself like all well-behaved immutable structures and then there would be some tricky way of propagating that new version to the recipient, but it turns out that things are much simpler.
Now I know that all that esoteric functional stuff I expected to be forced to use in haskell (like category theory, lenses, reactive streams, iteratees, etc.) is not built into the language, but implemented in libraries, and as a last resort you can just write imperative code or imperatively use entities which are mutable by design (like channels).
Haskell allows to write both functional and imperative code, one ways in which Haskell is special is that it sets the boundaries between them on the level of types.
Maybe purely functional languages don't exist.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
What's next?
what part of code is pure functional and what part is not pure?
In this project we can find pure functions in modules that describe models.
idk what all these functions do: atomically
, forkIO
, readTVar
, writeTVar
, readTMVar
, putTMVar
in examples from "Haskell by Example: Timers" (https://lotz84.github.io/haskellbyexample/ex/timers)
exit game functionality is not implemented yet, one way of exiting game is through interrupting the console
optimize changing values in cells matrix
how to introduce more functional stuff into code, like monads, monad transformers, etc.?
UPDATE 27.08.20: implemented exit game functionality using race
instead of concurrently
.
The code is in "feature-quit-game" branch in the repo.
Found out about functions like atomically
, forkIO
, readTVar
, writeTVar
, etc. from the book "Parallel and Concurrent Programming in Haskell" by Simon Marlow. The book is freely available: https://www.oreilly.com/library/view/parallel-and-concurrent/9781449335939/pr02.html
Comments
No comments found for this article.
Loading comments...
next comments page
Join the discussion for this article on this ticket. Comments appear on this page instantly.