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.