Converting a Functional Hangman game from Scala (ZIO) to Kotlin (with Arrow) was a nice exercise. I enjoyed working on it and I learned a lot. When I asked for feedback on the #arrow channel, one of the maintainers, Leandro had an interesting suggestion. Instead of hard-coding the data type
IO I should try and make the program polymorphic and use
Kind instead. That means writing the code focusing on the domain logic, using abstractions, and deferring the decision for the concrete data type like
Single (from RxJava) until the main function.
I was not familiar with that style of programming so I used this example from the excellent Arrow documentation as a guide.
Writing to the the console
In the previous article I used
IO<A> to interact with the console.
IO<A> represents an operation that can be executed lazily, fail with an exception (the exception is captured inside
IO), run forever or return a single
A. Let’s take a look at the original implementation:
putStrLn is a function that take a
String and return a
IO takes a lambda that is lazily evaluated at the end of the world, when we call
unsafeRunSync(). If we want to achieve the same thing with
Single we could use
Single.fromCallable wrap our lambda and evaluate it in the main function when we call
Single have something in common. A set of capabilities like: lazy evaluation, exception handling, and running forever or completing with a result of type
Single do a lot more, but for this use case, we want something as simple as possible that has the same capabilities. There is a type-class in Arrow that can do just that and it’s called
MonadDefer(more info). After a few iterations, and feedback from the Arrow team, this is the code I came up with for printing to the console.
putStrLn function is generic with type
F, but not any
F, whatever it is, need to do certain things like lazy evaluation and error handling. This
F thing needs the capabilities of
MonadDefer. The return value also needs a type parameter, and in Arrow we can do that by returning
Kind<F, SOMETHING>. In the case of printing to command line, that
Unit (no return value).
MonadDefer comes with an
delay function that we can use to construct the value of
Kind<F, Unit>. We pass a lambda inside which will be lazily evaluated.
Compared to the original implementation we have a few key differences:
- a type parameter
- one more parameter of type
- the return type is
Kind<F, Unit>instead of
Now we can use this function to print something to the console. In order to do that, we need a data type that has the capabilities of
IO can do that so we can use it. Arrow also ships with
SingleK, a wrapper for
Single that has the
Arrow also ships with
DeferredK for coroutines etc.
Reading from the console
Reading from the console is similar to writing to it. We still need a type parameter
F, we still need to return
Kind<F, SOMETHING> and we need to perform the operation lazily with success or an error.
readLine() returns a nullable
String? and if that happens we need to signal an error.
Reading from the console #2
I am throwing an
IOException to indicate failure.
IO would wrap that exception so it isn’t that bad. But there is a better way (thanks Leandro).
I convert the nullable
String? to an
Option<String>. Then I use
fold to check if the
Option has a value. If it’s empty I use
raiseError to create
MonadDefer which when evaluated returns an error. If it’s not empty I create a
MonadDefer that returns a
The key difference here is I am NOT throwing the
IOException. Using exceptions can be expensive.
M.defer here means the
readLine() happens lazily.
The Hangman class
The next step is to make the
Hangman class polymorphic. To do that I added a type parameter
F and property of the type
Choosing a letter
To make the
getChoice() function work in a polymorphic way we need a few changes. The return type changes from
M is the property of type
putStrLn() also need
M as the parameter.
Updating all other functions follows the same pattern. Replace
Kind<F, SOMETHING>, replace
M.binding and pass
M as parameter for reading/writing to the console.
You can find the full code here.
The main program
The main program, run with
Single, looks like this:
The decision in which type constructor to run is made at the point of execution. Switching from
Observable requires only updating the main function.
I am still learning FP and I don’t know most of the type-classes in Arrow and what they can do. In my first attempt I used
Async instead of
MonadDefer and adds additional capabilities to it. I asked for feedback on the #arrow channel and they pointed me towards
MonadDefer. Together with the Arrow maintainers we made a few more improvements improvements.
To avoid passing
printStrLn every time I can convert it to an extension function on
and I can use Implementation by delegation and have the
Hangman class implement
This means everywhere inside the
Hangman class I can use the methods of
delay and extension methods like
You can find the full implementation here.
Note: the code samples here use the function
delay which doesn’t exist in the latest published version (0.8.1). In arrow 0.8.2
invoke (used in the code on Github) will be deprecated and replaced by
Working with abstractions like
MonadDefer frees the business logic from implementation details like
Single. It can also enable easier composition of different modules because the decision for the concrete data type is delayed until the main program.