Chapters

Hide chapters

Functional Programming in Kotlin by Tutorials

First Edition · Android 12 · Kotlin 1.6 · IntelliJ IDEA 2022

Section I: Functional Programming Fundamentals

Section 1: 8 chapters
Show chapters Hide chapters

Appendix

Section 4: 13 chapters
Show chapters Hide chapters

15. Managing State
Written by Massimo Carli

In Section II, you learned about the concept of data type using the analogy of a container providing its content some context. For instance, Optional<A> represents a box that can either be empty or contain a value of type A. The context of a data type is important when you make it a functor or a monad. In the case of a functor, you can apply to Optional<A> a function Fun<A, B> and get an Optional<B>. If the initial Optional<A> is empty, you’ll get an empty Optional<B>. If Optional<A> contains a value of type A, you apply the function Fun<A, B>, to get a value of type B wrapped in an Optional<B>. If the function you apply is of type Fun<A, Optional<B>>, you give Optional<A> the superpower of a monad and use flatMap to get an Optional<B> whose content depends on whether the value A is present. Different data types provide different contexts and behave differently as functors and monads.

Note: An empty Optional<A> is different from an empty Optional<B> if A is different from B. In short, an empty box of pears is different from an empty box of apples. :]

You also learned that a pure function describes an expression that’s referentially transparent and, more importantly, doesn’t have any side effects. These two properties are connected because when an expression isn’t referentially transparent, it uses some data that’s outside the scope of the function, which is a side effect. A side effect changes the state of the universe outside the function body.

The question now is: Can you create a data type representing a box that encapsulates some data and the effect that happens when you apply functions with map or flatMap to the same data? With this data type, you wouldn’t prevent the change of some state, but you’d be able to control it.

The solution is the State<T> data type, which is the topic of this chapter. Here, you’ll learn:

  • What StateTranformation<S, T> is.
  • How to implement a State<S, T> data type.
  • How the State<S, T> data type works as a functor.
  • How to implement State<T> as an applicative functor.
  • What the State<S, T> monad is.
  • How to apply the State<S, T> monad in a practical example.

This is probably one of those chapters you’ll need to read multiple times, but it’ll definitely be worth it!

The problem

To describe how the State<S, T> data type works, it’s helpful to start with a very simple example. You can follow along with it in the Inventory.kt file in the material for this project. Start by adding this code:

data class Product(val id: String, val name: String)

This is a simple Product data class representing — ahem — a product. :] During an inventory, you want to assign a SKU to it.

Note: A stock keeping unit (SKU) is a scannable bar code that you usually find printed on product labels in retail stores. It’s used to track inventory movement automatically.

In this case, suppose the SKU has the format RAY-PROD-####, where #### represents a four-digit number that must be unique for each product.

Note: Please ignore the fact that, with this format, you can only have 10,000 different SKUs. This just allows the code to remain simple so you can focus on the state. Of course, you can implement your SKU generator algorithm however you want.

You represent a product after the inventory as an instance of SkuProduct, which you add to the same file:

data class SkuProduct(val product: Product, val sku: String)

To assign a unique SKU to every product, use the following code:

var count = 0 // 1

fun createSku(): String = // 2
  "RAY-PROD-${String.format("%04d", count++)}" // 3

In this code, you:

  1. Initialize the global variable count to 0.
  2. Define createSku as a function returning a unique SKU as a String.
  3. Update count, allowing you to get a different SKU at each createSku invocation.

To test the previous function, add and run the following code:

fun main() {
  val prod1 = Product("1", "Cheese")
  val prod2 = Product("2", "Bread")
  val prod3 = Product("3", "Cake")

  SkuProduct(prod1, createSku()) pipe ::println
  SkuProduct(prod2, createSku()) pipe ::println
  SkuProduct(prod3, createSku()) pipe ::println
}

Getting in output:

SkuProduct(product=Product(id=1, name=Cheese), sku=RAY-PROD-0000)
SkuProduct(product=Product(id=2, name=Bread), sku=RAY-PROD-0001)
SkuProduct(product=Product(id=3, name=Cake), sku=RAY-PROD-0002)

Everything looks fine, but it’s actually not! Now that you know all about pure functions and side effects, you surely noted that createSku isn’t pure — every time you invoke it, you change the state of the universe that, in this case, you represent with count.

At this point, you have two main goals:

  • Make createSku pure.
  • Simplify its usage in your inventory.

The first step is introducing the concept of state transformation.

State transformation

createSku is impure because of the side effect related to the count update, which happens every time you invoke createSku. This creates the SKU based on the current value of count, which is the current state. createSku also updates the state, preparing for the next invocation. You can generalize this behavior with the following definition, which you can add to State.kt:

typealias StateTransformer<S> = (S) -> S

StateTransformer<S> is a function that, given the current state of type S, returns the new value for the state which has, of course, the same type. In the case of the product inventory example, this function would be:

val skuStateTransformer: StateTransformer<Int> =
  { state -> state + 1 }

This is because you replaced S with Int, and the transformation consists of adding 1 to the current state.

In your example, you need to use the state to create a new SKU and use skuStateTransformer to update the state. A better abstraction to handle this behavior is the following:

typealias StateTransformer<S, T> = (S) -> Pair<T, S>

Note: The different order for the type parameters S and T in the definition and the returning Pair<T, S> might introduce some confusion. The important thing is to be consistent in the code that follows.

This is the abstraction for all the functions receiving a state of type S as input, and returning both a value of type T and the new state of type S. The returned T might depend on the current state.

In this case, you rewrite skuStateTransformation like this:

val skuStateTransformer: StateTransformer<Int, String> = { state ->
  "RAY-PROD-${String.format("%04d", state)}" to state + 1
}

Now, skuStateTransformer has type StateTransformer<Int, String> and receives an Int as input, which is the current state, and returns the SKU based on that state and the new state.

To see how this works, replace main with the following:

fun main() {
  val prod1 = Product("1", "Cheese")
  val prod2 = Product("2", "Bread")
  val prod3 = Product("3", "Cake")

  val state0 = 0 // 1
  val (sku1, state1) = skuStateTransformer(state0) // 2
  SkuProduct(prod1, sku1) pipe ::println // 3
  val (sku2, state2) = skuStateTransformer(state1) // 4
  SkuProduct(prod2, sku2) pipe ::println // 5
  val (sku3, state3) = skuStateTransformer(state2) // 6
  SkuProduct(prod3, sku3) pipe ::println // 7
}

In this code, you:

  1. Initialize the beginning state of state0 at 0.
  2. Invoke skuStateTransformer, passing the current value for the state, state0, and getting the SKU and the new state you save in sku1 and state1, respectively.
  3. Use sku1 to create the SkuProduct for prod1.
  4. Invoke skuStateTransformer, passing the new state, state1, and getting a new SKU and the new state you save in sku2 and state2, respectively.
  5. Use sku2 to create the SkuProduct for prod2.
  6. Again, invoke skuStateTransformer with state2.
  7. Use sku3 to create the SkuProduct for prod3, and so on…

Run this code, and you’ll get:

SkuProduct(product=Product(id=1, name=Cheese), sku=RAY-PROD-0000)
SkuProduct(product=Product(id=2, name=Bread), sku=RAY-PROD-0001)
SkuProduct(product=Product(id=3, name=Cake), sku=RAY-PROD-0002)

Looking at this code, you can’t say you made everything easier to use. This is because you have to:

  1. Create an initial state.
  2. Invoke the StateTransformer<S, T> to get a new state.
  3. Pass the new state to another StateTransformer<S, T> to get the next state.
  4. Keep going until you get all the type T values you need.

You should find a way to make all these “passing values” for the state automatic and somehow hidden under the hood. This is what you’ll achieve in the following paragraphs, and this is what the state monad is for.

A state transformer visual intuition

Before proceeding, there’s something you should fix. skuStateTransformer receives an Int as input and returns a String for the SKU and another Int for the new state. In the inventory problem, you need something else because you need to start from a Product and get a SkuProduct.

To do this, add the following definition to Inventory.kt:

val assignSku: (Product, Int) -> Pair<SkuProduct, Int> = // 1
  { product: Product, state ->
    val newSku = "RAY-PROD-${String.format("%04d", state)}" // 2
    SkuProduct(product, newSku) to state + 1 // 3
  }

In this code, you:

  1. Define assignSku as a function of two input parameters of types Product and Int, respectively, and a Pair<SkuProduct, Int> as output.
  2. Create the SKU using the current state.
  3. Return the SkuProduct and the new state.

In the first section of the book, you learned a magic trick called currying. It allows you to convert any function with multiple input parameters into a chain of functions with a single input parameter. If you apply curry to assignSku, you get a function of type (Product) -> (Int) -> Pair<SkuProduct, Int>. Look at the second part of this function type, and you’ll recognize the (Int) -> Pair<SkuProduct, Int> type, which is the same as StateTransformer<Int, SkuProduct>. To make everything explicit, you can say that assignSku has type (Product) -> StateTransformer<Int, SkuProduct>.

To prove this, add the following code in Inventory.kt, and see that it compiles:

val curriedAssignSku:
      (Product) -> StateTransformer<Int, SkuProduct> =
  assignSku.curry()

Note: curry and other higher-order functions you implemented in the previous chapters are already available in the lib sub-package of the project in the material.

At this point, a visual representation of assignSku can be helpful. Look at Figure 15.1:

(state) (new state) Product Int Int SkuProduct assignSku
Figure 15.1: State transformer

You can see that assignSku has a visible part that maps a Product into a SkuProduct and an invisible one that’s responsible for updating the state.

What you want to achieve is a way to implement (Product) -> SkuProduct while keeping the state transformation hidden.

Now, replace the main with the following:

fun main() {
  val prod1 = Product("1", "Cheese")
  val prod2 = Product("2", "Bread")
  val prod3 = Product("3", "Cake")

  val state0 = 0
  val (skuProd1, state1) = curriedAssignSku(prod1)(state0)
  skuProd1 pipe ::println
  val (skuProd2, state2) = curriedAssignSku(prod2)(state1)
  skuProd2 pipe ::println
  val (skuProd3, state3) = curriedAssignSku(prod3)(state2)
  skuProd3 pipe ::println
}

Run the code, and you’ll get:

SkuProduct(product=Product(id=1, name=Cheese), sku=RAY-PROD-0000)
SkuProduct(product=Product(id=2, name=Bread), sku=RAY-PROD-0001)
SkuProduct(product=Product(id=3, name=Cake), sku=RAY-PROD-0002)

You can already see some minor improvements, but you still want to remove the need to pass the current state.

Introducing the State<S, T> data type

The StateTransformer<T, S> type you defined earlier is a function type. What you need now is a data type so you can apply all the typeclasses; like functor, applicative and monad; you applied for Optional<T>, Either<A, B> or Result<T>. To do this, you just need to add the following code to the State.kt file:

data class State<S, T>(val st: StateTransformer<S, T>)

If you want to think of this in terms of containers again, you can look at the State<S, T> as a box whose context is about the encapsulation of a StateTransformer<S, T> that describes how the state changes at every action you do through the box itself.

To make things a little bit easier and remove the effort due to the definition of the State<S, T> data type, add the following code:

operator fun <S, T> State<S, T>.invoke(state: S) = st(state)

In this case, you can start from a State<S, T> and use the invoke function or the (), pass the current state, and apply to it the StateTransformer<S, T> it encapsulates. Just remember that the type for invoke is (State<S, T>) -> (S) -> Pair<T, S>.

Now that you have the State<S, T> data type, you need to add some magic starting from lift up to the monadic superpower. Now, the real fun starts. :]

Implementing lift

The first operation you need to implement is lift, also called return in other languages. This is the function you use to get a value of type T and put it into the box related to the specific data type, in this case, State<S, T>. In State.kt, change the State<S, T> definition like this:

data class State<S, T>(
  val st: StateTransformer<S, T>
) {

  companion object { // 1
    @JvmStatic
    fun <S, T> lift(
      value: T // 2
    ): State<S, T> = // 3
      State({ state -> value to state }) // 4
  }
}

In this code, you implement lift:

  1. Using a companion object as a common pattern.
  2. With an input parameter of type T.
  3. Returning a State<S, T>.
  4. Creating the State<S, T> using a StateTransformer<S, T> that simply keeps the state the same and returns the input value as a result.

With lift, you can start from a simple value and get a State<S, T> for it, like this:

fun main() {
  val initialState = State.lift<Int, Product>(Product("1", "Cheese"))
}

Just note how you need to help the Kotlin type inference by providing the type for the input type parameters S and T. This is because Kotlin infers T‘s type from the value you pass, but it can’t understand S’s type on its own.

State<S, T> as a functor

After lift, it’s time to make State<S, T> a functor. This means providing an implementation of the map function of type (State<S, A>) -> (Fun<A, B>) -> (State<S, B>).

Before writing the code, take some time to think about what it means for a State<S, A> to be a functor. You start with a value of type A and, using map, you apply a function of type Fun<A, B>, getting a value of type B. The State<S, A> needs to handle the state update. To understand how it works, add the following code to the StateFunctor.kt file:

fun <S, A, B> State<S, A>.map( // 1
  fn: Fun<A, B> // 2
): State<S, B> = // 3
  State { state ->  // 4
    val (a, newState) = this(state) // 5
    fn(a) to newState // 6
  }

In this code, you:

  1. Define map as an extension function for the type State<S, A>.
  2. Have a function of type Fun<A, B> as an input parameter.
  3. Return a value of type State<S, B> according to the concept of functors.
  4. Create an instance of State<S, B>, passing in a lambda with an input parameter state of type S. Note how you can omit the () because the only parameter, and also the last, is a lambda expression.
  5. To get the new state and the value of type A, you need to invoke the state transformer of type StateTransformer<S, A> encapsulated in the receiver. Remember that you can do this because you defined the invoke operator on the State<S, T> type above. Another option would be simply to invoke st(state).
  6. Invoke the input parameter fn, passing the value of type A you got in the previous step, and get the value you return along with the new state.

In the previous code, it’s essential to understand that the new state you get with State<S, B> is the same as what you get with the initial State<S, A>. The difference is that the value of type B in State<S, B> is the one you get applying Fun<A, B> to the value of type A in State<S, A>.

As an example, add the following code to StateFunctor.kt:

val skuSerial = { sku: String -> sku.takeLast(4) } // 1

val skuState: State<Int, String> = State { state: Int -> // 2
  "RAY-PROD-${String.format("%04d", state)}" to state + 1
}

val skuSerialState = skuState.map(skuSerial) // 3

fun main() { // 4
  skuState(0) pipe ::println
  skuSerialState(0) pipe ::println
}

Here, you define:

  1. skuSerial, which has the type Fun<String, String> and returns the last 4 characters of the input String.
  2. skuState as the State<Int, String> for the generation of the SKUs.
  3. skuSerialState as the State<Int, String> you get from skuState passing skuSerial as an argument of map.
  4. main that prints the result of skuState and skuSerialState passing the same value as the initial state.

When you run the code, you’ll get:

(RAY-PROD-0000, 1)
(0000, 1)

As you see, the new state is the same, but skuSerial has been applied to the value of type String.

State<S, T> as an applicative functor

Looking at the signature of map for the State<S, A>, notice that it accepts a function of type Fun<A, B> as input. As you know, Fun<A, B> is the type of function with a single input parameter of type A returning a value of type B.

It’s interesting, now, to generalize map as accepting functions with multiple input parameters. For instance, in the Curry.kt file in lib, you find the following code:

typealias Fun2<T1, T2, R> = (T1, T2) -> R
typealias Fun3<T1, T2, T3, R> = (T1, T2, T3) -> R
typealias Fun4<T1, T2, T3, T4, R> = (T1, T2, T3, T4) -> R

Using curry, you know they’re equivalent to the following types:

typealias Chain2<T1, T2, R> = (T1) -> (T2) -> R
typealias Chain3<T1, T2, T3, R> = (T1) -> (T2) -> (T3) -> R
typealias Chain4<T1, T2, T3, T4, R> =
  (T1) -> (T2) -> (T3) -> (T4) -> R

Note: In the previous code, you considered up to four input parameters, but, of course, you could consider all the cases up to N.

If you want to generalize map for functions of different input parameters, you could just provide an implementation of map for each of those. For instance, in the case of Fun2<A, B, C>, you could define the following:

fun <S, A, B, C> State<S, Pair<A, B>>.map2(
  fn: Fun2<A, B, C>
): State<S, C> =
  State { state ->
    val (pair, newState) = this(state) // Or st(state)
    val value = fn(pair.first, pair.second)
    value to newState
  }

Replace Fun2<A, B, C> with Chain2<A, B, C>, and you get:

fun <S, A, B, C> State<S, Pair<A, B>>.map2(
  fn: Chain2<A, B, C> // 1
): State<S, C> =
  State { state ->
    val (pair, newState) = this(state) // Or st(state)
    val value = fn(pair.first)(pair.second)  // 2
    value to newState
  }

Here, you:

  1. Use Chain2<A, B, C> in place of Fun2<A, B, C>.
  2. Get the value to return with fn(pair.first)(pair.second) instead of passing the two parameters together, like fn(pair.first, pair.second).

With Fun2<A, B, C>, this isn’t a problem, but imagine if you had to implement all these map versions for all the possible options. This would be very tedious. Fortunately, this is the case where the applicative functor typeclass comes into play. Similar to what you saw in Chapter 14, “Error Handling With Functional Programming”, to define all possible maps for all the possible functions with multiple parameters, you just need two basic functions you’ve already met. The first is your lift that, in the context of an applicative functor, is called pure. The second is the ap function that, in the context of the State<S, T> data type, has the following signature:

fun <S, T, R> State<S, T>.ap(
  fn: State<S, (T) -> R>
): State<S, R> {
  // TODO
}

As you can see, ap is an extension function for the type State<S, T> and accepts in input a State<S, (T) -> R> where the value is actually a function from T to R. The result, then, is a State<S, R>.

Before implementing its body, it’s interesting to see how you can use ap to handle functions of multiple parameters. You do this using the applicative style.

Suppose you want to apply a function with three parameters of type Fun3<A, B, C, R>, which is equivalent to a Chain<A, B, C, R> typealias of (A) -> (B) -> (C) -> R. As a simple example, add the following code to StateApplicative.kt:

fun replaceSuffix(
  input: String,
  lastToRemove: Int,
  postfix: String
) = input.dropLast(lastToRemove) + postfix

This is a basic function of three parameters that removes the lastToRemove characters from an input String, replacing them with postfix. It has the type Fun3<String, Int, String, String>.

You can make it of type Chain<String, Int, String, String> using curry, like this:

val cReplaceSuffix = ::replaceSuffix.curry()

To use the applicative style, add the following code:

infix fun <S, A, B> State<S, (A) -> B>.appl(a: State<S, A>) =
  a.ap(this) // 1

fun main() {
  val initialStateApp = State
    .lift<Int, Chain3<String, Int, String, String>>(
      cReplaceSuffix
    ) // 2
  val inputApp = State.lift<Int, String>("1234567890") // 3
  val lastToRemoveApp = State.lift<Int, Int>(4) // 3
  val postfixApp = State.lift<Int, String>("New") // 3
  val finalStateApp = initialStateApp appl
    inputApp appl lastToRemoveApp appl postfixApp // 4

  inputApp(0) pipe ::println // 5
  finalStateApp(0) pipe ::println // 5
}

In this code, you:

  1. Define appl for State<S, A>, the same way you did in Chapter 14, “Error Handling With Functional Programming”, to make the code more readable.
  2. Use lift to create State<Int, Chain3<String, Int, String, String>>, passing cReplaceSuffix.
  3. Do the same for all the input parameters. Note how inputApp and postfixApp have type State<Int, String>, and lastToRemoveApp has type State<Int, Int>.
  4. Using the infix operator appl, you apply the applicative style, which remains associative. This means it works as if it were (((initialStateApp appl inputApp) appl lastToRemoveApp) appl postfixApp).
  5. Print the initial and final values as output.

Of course, at the moment, you can’t run this code because you still need to implement app.

Replace the previous skeleton with the following code:

fun <S, T, R> State<S, T>.ap( // 1
  fn: State<S, (T) -> R> // 2
): State<S, R> = // 3
  State { s0: S -> // 4
    val (t, s1) = this(s0) // 5
    val (fnValue, s2) = fn(s1) // 6
    fnValue(t) to s2 // 7
  }

In this code, you:

  1. Define ap as an extension function for State<S, T>.
  2. Accept a parameter of type State<S, (T) -> R> as input. Note how the value is a function of type (T) -> R.
  3. Use State<S, R> as the return type.
  4. Invoke the State<S, R> constructor, passing a lambda with the current state s0 as the initial state.
  5. Get the value of type T and the new state s1, invoking the current receiver with the initial state.
  6. Use the new state s1 as input for fn to get the value of type (T) -> R and the final state s2.
  7. Get the final result, invoking fn on the value of type T you got in Step 5 and using the final state s2.

Note how you get the values from the current receiver first and then from the input State<S, (T) -> R>. Also, note how the state updates twice. The first update is based on the current receiver of type State<S, T>, and the second is because of the one of input State<S, (T) -> R>.

Now, you can finally run main, getting:

(1234567890, 0)
(123456New, 0)

As you can see, you applied replaceSuffix to the input String, but the value for the state of type Int remained the same. This is actually expected because this works exactly like the functor. It just gives you the chance to apply functions with multiple parameters to the value of type T in State<S, T>, leaving the state unchanged.

State<S, T> as a monad

Now, it’s finally time to give State<S, T> the power of a monad by implementing flatMap. To do that, you could follow the same process you learned in Chapter 13, “Understanding Monads”, providing implementation to fish, bind, flatten and finally flatMap. That was a general process valid for all monads, but now you can go straight to the solution, starting with the following code you write in StateMonad.kt:

fun <S, A, B> State<S, A>.flatMap( // 1
  fn: (A) -> State<S, B> // 2
): State<S, B> = TODO() // 3

Here, you:

  1. Define flatMap as an extension function for the State<S, A> data type.
  2. Provide fn as an input parameter of type (A) -> State<S, B>.
  3. Return a State<S, B>.

So, if the State<S, A> data type is a way to encapsulate some state and the logic to update it, flatMap is the tool you use to compose a function of type (A) -> State<S, B> coming from the Kleisli category.

You’ll probably be surprised at how simple the flatMap implementation is. Just replace the previous definition with the following code:

fun <S, A, B> State<S, A>.flatMap(
  fn: (A) -> State<S, B>
): State<S, B> =
  State { s0: S -> // 1
    val (a, s1) = this(s0) // 2
    fn(a)(s1) // 3
  }

Here, you:

  1. Use the State<S, B> constructor, passing the state transformation as a body. Of course, the state transformation has the initial state s0 as an input parameter.

  2. Invoke the receiver of type State<S, A>, passing the initial state. In this way, you get the value a of type A and the new state s1 of type S. Remember that the type for the state S never changes, but its value can.

  3. Pass the value a as an input parameter of the function fn of type (A) -> State<S, B> you have as input, getting a value of type State<S, B>. To get the final Pair<B, S>, you need to invoke the State<S, B> with the state s1.

As a simple example of how to use State<S, T> as a monad, add the following code to the same StateMonad.kt file:

val assignSkuWithState: // 1
      (Product) -> State<Int, SkuProduct> =
  { prod: Product ->
    State(curriedAssignSku(prod)) // 2
  }

fun main() {
  val prod1 = Product("1", "First Product") // 3
  val initialState = State.lift<Int, Product>(prod1) // 4
  val finalState = initialState.flatMap(assignSkuWithState) // 5
  finalState(0) pipe ::println // 6
}

In this code, you define a main, where you:

  1. Implement assignSkuWithState as a function of type (Product) -> State<Int, SkuProduct> responsible for assigning a new SKU to a Product into a SkuProduct and updating the current state.
  2. Implement assignSkuWithState, simply encapsulating the StateTransformer<Int, SkuProduct> from the curriedAssignSku you defined in Inventory.kt, into a State<Int, SkuProduct>.
  3. Define a simple Product you use as an input parameter.
  4. Use lift to define the State<Int, Product> from the value prod1 of type Product. This is the initial state, initialState.
  5. Invoke flatMap, passing the reference to assignSku. This is where the composition magic happens, and you get a State<Int, SkuProduct> that you save in finalState.
  6. Invoke finalState, passing the initial state value 0, and send the result to the standard output.

Run the code, and you’ll get:

(SkuProduct(product=Product(id=1, name=First Product), sku=RAY-PROD-0000), 1)

The initial value for the state was 0, and this is the one assignSku used to generate the SKU RAY-PROD-0000. The new value for the state is 1. This is because lift doesn’t apply the state transformation but returns a value for the state that’s the same value you have in input.

A practical example

In the previous example, you didn’t have the chance to appreciate the hidden state transformation that the State<S, T> monad does for you behind the scenes. As a more complicated example, suppose you have an FList<Product>, and you want to assign each one a unique value for the SKU, getting an FList<SkuProduct> as output.

Open StateMonadInventory.kt, and add the following code:

val products = FList.of(
  Product("1", "Eggs"),
  Product("2", "Flour"),
  Product("3", "Cake"),
  Product("4", "Pizza"),
  Product("5", "Water")
)

This is a simple FList<Product> with some dummy data, and you want to get an FList<SkuProduct>, assigning each one a unique SKU value.

A possible solution implies the use of the map function, like this:

var currentCount = 0
fun inventoryMap(products: FList<Product>): FList<SkuProduct> {
  return products.map {
    SkuProduct(it,
      "RAY-PROD-${String.format("%04d", currentCount++)}")
  }
}

To see how this works, run the following code:

fun main() {
  inventoryMap(products).forEach(::println)
}

Getting:

SkuProduct(product=Product(id=1, name=Eggs), sku=RAY-PROD-0000)
SkuProduct(product=Product(id=2, name=Flour), sku=RAY-PROD-0001)
SkuProduct(product=Product(id=3, name=Cake), sku=RAY-PROD-0002)
SkuProduct(product=Product(id=4, name=Pizza), sku=RAY-PROD-0003)
SkuProduct(product=Product(id=5, name=Water), sku=RAY-PROD-0004)

But, there’s a but! inventoryMap isn’t pure because it uses and changes currentCount, which is part of the external world. A possible solution could be to move currentCount inside inventoryMap, like this:

fun inventoryMapWithCount(
  products: FList<Product>
): FList<SkuProduct> {
  var internalCount = 0
  return products.map {
    SkuProduct(it,
      "RAY-PROD-${String.format("%04d", internalCount++)}")
  }
}

The output would be the same. internalCount now has the scope inventoryMapWithCount, and it’s not considered as a side effect. The problem now is that internalCount starts from 0 every time you invoke inventoryMapWithCount, so it’ll produce duplicated SKUs.

In this chapter, you learned that a possible solution is to add the current state as part of the input parameter and define a listInventoryHelper function, like the following:

fun listInventory(
  products: FList<Product>
): (Int) -> Pair<Int, FList<SkuProduct>> =
  when (products) { // 1
    is Nil -> { count: Int -> count to Nil } // 2
    is FCons<Product> -> { count: Int -> // 3
      val (newState, tailInventory) =
        listInventory(products.tail)(count)
      val sku = "RAY-PROD-${String.format("%04d", newState)}"
      newState + 1 to FCons(
        SkuProduct(products.head, sku), tailInventory)
    }
  }

In this code:

  1. You check if the current product is empty or an FCons<Product>.
  2. If it’s empty, you get a Nil, and you just need to return it along with the current unchanged state.
  3. If you have an FCons<Product>, you need to handle the head first, getting a SKU and creating the related SkuProduct. For the tail, you just need to invoke listInventory recursively.

To test the code, run:

fun main() {
  listInventory(products)(0).second.forEach(::println)
}

Getting:

SkuProduct(product=Product(id=1, name=Eggs), sku=RAY-PROD-0004)
SkuProduct(product=Product(id=2, name=Flour), sku=RAY-PROD-0003)
SkuProduct(product=Product(id=3, name=Cake), sku=RAY-PROD-0002)
SkuProduct(product=Product(id=4, name=Pizza), sku=RAY-PROD-0001)
SkuProduct(product=Product(id=5, name=Water), sku=RAY-PROD-0000)

This approach works, but you can improve it. The main issues are:

  1. The SKUs are assigned in reverse order. This might not be a problem as long as they’re unique.
  2. You need to handle the internal state explicitly. This is error-prone and might lead to errors that are difficult to debug.

Fortunately, the State<S, T> monad helps. To understand how, you need to create a utility function first. Add the following code to State.kt:

fun <S, A, B, C> State<S, A>.zip( // 1
  s2: State<S, B>, // 2
  combine: (A, B) -> C // 3
): State<S, C> = // 4
  State { s0 -> // 5
    val (v1, s1) = this(s0) // 6
    val (v2, s2) = s2(s1) // 7
    combine(v1, v2) to s2 // 8
  }

In this code, you:

  1. Define zip as an extension function of State<S, A>.
  2. Declare a first input parameter of type State<S, B>,.
  3. Use the second input parameter, combine of type (A, B) -> C.
  4. Return a State<S, C> whose value is the value of type C you get by invoking combine on the values of types A and B.
  5. Create the result, invoking the State<S, C> constructor.
  6. Invoke the receiver with the initial state, getting the value of type A and the new state.
  7. Use the new state to get the value of type B and an updated version of the state.
  8. Get the value of type C, invoking combine, and return the result along with the last version of the state.

Now, you can finally add the following code to StateMonadInventory.kt:

val addSku: (Product) -> State<Int, SkuProduct> = // 1
  { prod: Product ->
    State<Int, SkuProduct> { state: Int ->
      val newSku = "RAY-PROD-${String.format("%04d", state)}"
      SkuProduct(prod, newSku) to state + 1
    }
  }

fun inventory(
  list: FList<Product>
): State<Int, FList<SkuProduct>> = // 2
  when (list) { // 3
    is Nil -> State.lift(Nil) // 4
    is FCons<Product> -> {
      val head = State.lift<Int, Product>(list.head) // 5
        .flatMap(addSku)
      val tail = inventory(list.tail) // 6
      head.zip(tail) { a: SkuProduct, b: FList<SkuProduct> -> // 7
        FCons(a, b)
      }
    }
  }

Here, you:

  1. First, define addSku, which returns a State<Int, SkuProduct> given a Product as input.
  2. Define inventory as a function accepting an FList<Product> as input and returning a State<Int, FList<SkuProduct>> as output.
  3. Check if the current FList<Product> is a Nil or FCons<Product>.
  4. If it’s a Nil, you just return the same encapsulated into a State<Int, FList<SkuProduct>> using lift.
  5. If it’s an FCons<Product>, you handle the head first. First, you use lift to get a State<Int, Product>, and then use flatMap with addSku to get a State<Int, SkuProduct>.
  6. Invoke inventory recursively on the tail to get the related state of type State<Int, FList<SkuProduct>>.
  7. Use zip to combine the State<Int, FList<SkuProduct>> for the head and the tail in a single FList you return as a result.

To test how this works, add and run the following code:

fun main() {
  inventory(products)(0).first.forEach(::println)
}

Getting as output:

SkuProduct(product=Product(id=1, name=Eggs), sku=RAY-PROD-0000)
SkuProduct(product=Product(id=2, name=Flour), sku=RAY-PROD-0001)
SkuProduct(product=Product(id=3, name=Cake), sku=RAY-PROD-0002)
SkuProduct(product=Product(id=4, name=Pizza), sku=RAY-PROD-0003)
SkuProduct(product=Product(id=5, name=Water), sku=RAY-PROD-0004)

As you can see, now the SKUs have the right order, but, more importantly, all the state management is handled entirely under the hood. In the last version of inventory, you don’t pass over any state, and the code is completely functional.

Key points

  • A data type is like a container that provides some context to its content.
  • A state represents any value that can change.
  • You can use the concept of state to model the side effect of an impure function.
  • The context of a data type impacts how you interact with its content when applying some functions.
  • The State<S, T> data type encapsulates the concept of state transition.
  • StateTransformer<S, T> abstracts a value and a state update.
  • State<S, T> is a data type that encapsulates a StateTransformer<S, T>.
  • You can make State<S, T> a functor providing the implementation for map.
  • map on a State<S, T> applies a function to the value of type T but leaves the state unchanged.
  • Making State<S, T> an applicative functor allows you to apply functions with multiple parameters.
  • You can make State<S, T> a monad, providing implementation for the flatMap.
  • The State<S, T> allows you to define two different types of transactions. The first, on the value, is visible. The second, on the state transition, is hidden.

Where to go from here?

Congratulations! This is definitely one of the most challenging chapters of the book. Using most of the concepts from the first two sections of the book, you learned how to use the State<S, T> data type and how to implement lift, map, app, appl and flatMap. Finally, you applied the State<S, T> monad to a real example, showing how it’s possible to keep the state transaction hidden. The concept of side effects is one of the most important in functional programming, and in the next chapter, you’ll learn even more about it.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.