DEV Community

Riccardo Odone
Riccardo Odone

Posted on • Originally published at odone.io on

Bank Kata in PureScript

You can keep reading here or jump to my blog to get the full experience, including the wonderful pink, blue and white palette.


Intro

I suck at FP and I desperately need some feedback from you. So please, do not get mad at the code. And double please share feedback if you got any!!

The Kata

Let’s first introduce the kata by copy / pasting from the awesome Kata-Log:

Write a class Account that offers the following methods void deposit(int) void withdraw(int) String printStatement()

An example statement would be:

Date Amount Balance
24.12.2015 +500 500
23.8.2016 -100 400
Enter fullscreen mode Exit fullscreen mode

Types

void deposit(int) and void withdraw(int) are impure functions. In fact, they accept an int and return void. The only way they can do anything useful is to mutate some state.

String printStatement() is impure too. As a matter of fact, it returns a string out of nothing. The only way for it to do anything useful is to access some state. In this post, I’ll implement printStatement as if it was void printStatement(). That is, the function will print the statement in the console. The reason is that I don’t know how to code it otherwise.

One way to read / write state in PureScript is using the state monad transformer (StateT).

Therefore, we will use the following types:

deposit :: Int -> StateT (Array Transaction) Effect Unit

withdraw :: Int -> StateT (Array Transaction) Effect Unit

printStatement :: StateT (Array Transaction) Effect Unit
Enter fullscreen mode Exit fullscreen mode

In other words, our three functions will do their thing in the StateT (Array Transaction) Effect Unit environment. In simpler words, each function will be able to manipulate an array of transactions (state), write to console or get datetimes (monadic operations in Effect) and return nothing (Unit) at the end.

And here we have the type for Transaction:

data Transaction
 = Deposit Info
 | Withdraw Info

type Info =
 { timestamp :: DateTime
 , amount :: Int
 }
Enter fullscreen mode Exit fullscreen mode

Implementation

Let’s start with deposit:

deposit :: Int -> StateT (Array Transaction) Effect Unit
deposit amount = do
 ts <- lift nowDateTime
 let t = Deposit { timestamp: ts, amount: amount }
 modify_ \ts -> ts <> [t]
Enter fullscreen mode Exit fullscreen mode

Since we are in a monadic environment (StateT (Array Transaction) Effect Unit), we open the function with a do.

Then we use nowDateTime :: Effect DateTime to get the current datetime. The only catch here is that we need to first lift nowDateTime in StateT (Array Transaction) Effect Unit. That is because in a do block each monadic operation (i.e. non lets) must all use the same monad. In this case, that means that both lift nowDateTime and modify_ \ts -> ts <> [t] have type StateT (Array Transaction) Effect a.

After that, a deposit with correct timestamp and amount is assigned to t.

Lastly, modify_ is used to access the current state ts(array of transactions) by appending the new transaction t.

withdraw is almost the same:

withdraw :: Int -> StateT (Array Transaction) Effect Unit
withdraw amount = do
 ts <- lift nowDateTime
 let t = Withdraw { timestamp: ts, amount: amount }
 modify_ \ts -> ts <> [t]
Enter fullscreen mode Exit fullscreen mode

Finally, we have printStatement:

printStatement :: StateT (Array Transaction) Effect Unit
printStatement = do
 s <- gets toStatement
 lift $ log s
Enter fullscreen mode Exit fullscreen mode

The first line uses gets to take the state (array of transactions) and run it through toStatement :: Array Transaction -> String. That means gets toStatement has type Effect String and s has type String.

The last line lifts log s :: Effect Unit in StateT (Array Transaction) Effect Unit. In other words, it prints s to the console.

The implementation of toStatement is not that important. Here is an example of that:

toStatement :: Array Transaction -> String
toStatement =
 fst <<< foldl fnc (Tuple "" 0)
 where
 fnc (Tuple s i) (Deposit d) =
 Tuple (s <> "\n" <> joinWith " " [show d.timestamp, show d.amount, show $ i + d.amount]) (i + d.amount)
 fnc (Tuple s i) (Withdraw w) =
 Tuple (s <> "\n" <> joinWith " " [show w.timestamp, "-" <> show w.amount, show $ i - w.amount]) (i - w.amount)
Enter fullscreen mode Exit fullscreen mode

Fire it up

Now we can write something like

do
 deposit 500
 withdraw 100
 printStatement
Enter fullscreen mode Exit fullscreen mode

which has type StateT (Array Transaction) Effect Unit. And we can run that computation with evalStateT. Notice that the following code returns Effect Unit.

flip evalStateT [] do
 deposit 500
 withdraw 100
 printStatement
Enter fullscreen mode Exit fullscreen mode

Show me the code

And here we have all the code

data Transaction
  = Deposit Info
  | Withdraw Info

type Info =
  { timestamp :: DateTime
  , amount    :: Int
  }

deposit :: Int -> StateT (Array Transaction) Effect Unit
deposit amount = do
  ts <- lift nowDateTime
  let t = Deposit { timestamp: ts, amount: amount }
  modify_ \ts -> ts <> [t]

withdraw :: Int -> StateT (Array Transaction) Effect Unit
withdraw amount = do
  ts <- lift nowDateTime
  let t = Withdraw { timestamp: ts, amount: amount }
  modify_ \ts -> ts <> [t]

printStatement :: StateT (Array Transaction) Effect Unit
printStatement = do
  s <- gets toStatement
  lift $ log s

toStatement :: Array Transaction -> String
toStatement =
  fst <<< foldl fnc (Tuple "" 0)
  where
  fnc (Tuple s i) (Deposit d) =
    Tuple (s <> "\n" <> joinWith " " [ show d.timestamp, show d.amount, show $ i + d.amount]) (i + d.amount)
  fnc (Tuple s i) (Withdraw w) =
    Tuple (s <> "\n" <> joinWith " " [ show w.timestamp, "-" <> show w.amount, show $ i - w.amount]) (i - w.amount)

main :: Effect Unit
main = do
  flip evalStateT [] do
    deposit 500
    withdraw 100
    printStatement
Enter fullscreen mode Exit fullscreen mode

Outro

If you liked the post and want to help spread the word, please make some noise 🤘 But only if you really liked it. Otherwise, please feel free to comment or tweet me with any suggestions or feedback.

Thanks to Liam Griffin who inspired me to try this exercise in PureScript with his post in Haskell.

Finally, I want to give a shoutout to BusConf and to all the people I’ve met there that showed so much support for my PureScript journey. You are awesome!

If you are hungry for more, see how we can test the code in the followup: Testing Bank Kata in PureScript.


Get the latest content via email from me personally. Reply with your thoughts. Let's learn from each other. Subscribe to my PinkLetter!

Top comments (0)