The Bank Kata in Ocaml - Part 2: implementing the kata

In part 1 we’ve set everything up, let’s implement the kata now. If we check the readme we can see a simple acceptance test, let’s start with that.

We create a file test/account_test.ml:

open! Base
open! Stdio

let%expect_test "Printing the statements should contain all transactions" =
  Lib.Account.create ()
  |> Lib.Account.deposit ~amount:1000.0 ~on:"10/01/2012"
  |> Lib.Account.deposit ~amount:2000.0 ~on:"13/01/2012"
  |> Lib.Account.withdrawal ~amount:500.0 ~on:"14/01/2012"
  |> Lib.Account.print;
  [%expect{|
    date || amount || balance
    14/01/2012 || -500.00 || 2500.00
    13/01/2012 || 2000.00 || 3000.00
    10/01/2012 || 1000.00 || 1000.00
  |}]

Let’s first see how we can get this test to green as fast as possible.

The simplest way to make this compile is with lib/account.ml:

let create () = ()

let deposit _ _ = ()

let withdrawal _ _ = ()

let print _ =
  print_endline "TODO"

If you now run your tests, you should get a compiling code, but mismatching output, so let’s think about the actual implementation.

Now it’s time to start thinking about some types. Let’s see, what should create return?

type account = transaction list
and type transaction = {
    date: string;
    amount: float;
}

Something like this seems ok for the first iteration of this kata (ofcourse having floats for an amount is serious no-go, but for this kata, we’ll ignore it!) We create an account type, that’s just a list of transactions, and thus, create can be defined like:

let create () = []

So this makes deposit and withdrawal trivial to implement:

let deposit ~amount ~on account = {date = on; amount} :: account
let withdrawal ~amount ~on account = {date = on; amount = amount *. -1.} :: account

And now we’re ready to implement the print function to get the tests green.

let print account =
  let calculate account =
    List.fold_right (fun transaction (total, x) -> (total +. transaction.amount, (total +. transaction.amount, transaction) :: x)) account (0., [])
  in
  let buffer = Buffer.create (List.length account * 50) in
  let rec fill_buffer = function
    | [] -> ()
    | (total, transaction) :: tl ->
      Buffer.add_string buffer (Printf.sprintf "%s || %.2f || %.2f\n" transaction.date transaction.amount total);
      fill_buffer tl
  in
  let (_, calculated) = calculate account in
  Buffer.add_string buffer "date || amount || balance\n";
  fill_buffer calculated;
  print_string (Buffer.contents buffer)

This is not the most beautiful code, but it makes the tests green. The question is are we happy with it or is it time for refactoring?

We could argue that doing amount *. -1 in withdrawal is not very clean, but that’s something I’m going to leave for now.

The print function is something else though. Let’s see if we can clean this up a bit:

let print account =
  let add_balance account =
    let (_, with_calculated_balance) = List.fold_right (
        fun transaction (total, calculated_transactions) ->
          (total +. transaction.amount, (total +. transaction.amount, transaction) :: calculated_transactions)
      ) account (0., []) in
    with_calculated_balance
  in
  let to_print_statements account_with_balance =
    List.map (fun (total, transaction) ->
        Printf.sprintf "%s || %.2f || %.2f\n" transaction.date transaction.amount total
      ) account_with_balance
  in
  let print_statements_to_string_with_header print_statements =
    let buffer = Buffer.create (List.length account * 50) in
    Buffer.add_string buffer "date || amount || balance\n";
    List.iter (Buffer.add_string buffer) print_statements;
    Buffer.contents buffer
  in

  account
  |> add_balance
  |> to_print_statements
  |> print_statements_to_string_with_header
  |> print_string

This feels a lot better, when you start reading the last lines it tells clearly what it is doing. It’s a bit more code, but all functions have better names and have one purpose.

Before we’re saying we’re done, something still feels off. We pass dates into our public API. That’s not how a bank works? After looking a bit better at the bank kata, I found this where the dates are not part of the public API. How can we fix this with OCaml. Let’s find out in part 3.

Thomas Coopman

Software Consultant