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 emptyOptional<B>ifAis different fromB. 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:
- Initialize the global variable
countto0. - Define
createSkuas a function returning a unique SKU as aString. - Update
count, allowing you to get a different SKU at eachcreateSkuinvocation.
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
createSkupure. - 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
SandTin the definition and the returningPair<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:
- Initialize the beginning state of
state0at0. - Invoke
skuStateTransformer, passing the current value for the state,state0, and getting the SKU and the new state you save insku1andstate1, respectively. - Use
sku1to create theSkuProductforprod1. - Invoke
skuStateTransformer, passing the new state,state1, and getting a new SKU and the new state you save insku2andstate2, respectively. - Use
sku2to create theSkuProductforprod2. - Again, invoke
skuStateTransformerwithstate2. - Use
sku3to create theSkuProductforprod3, 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:
- Create an initial state.
- Invoke the
StateTransformer<S, T>to get a new state. - Pass the new state to another
StateTransformer<S, T>to get the next state. - Keep going until you get all the type
Tvalues 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:
- Define
assignSkuas a function of two input parameters of typesProductandInt, respectively, and aPair<SkuProduct, Int>as output. - Create the SKU using the current
state. - Return the
SkuProductand 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:
curryand 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:
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:
- Using a
companion objectas a common pattern. - With an input parameter of type
T. - Returning a
State<S, T>. - Creating the
State<S, T>using aStateTransformer<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:
- Define
mapas an extension function for the typeState<S, A>. - Have a function of type
Fun<A, B>as an input parameter. - Return a value of type
State<S, B>according to the concept of functors. - Create an instance of
State<S, B>, passing in a lambda with an input parameterstateof typeS. Note how you can omit the()because the only parameter, and also the last, is a lambda expression. - To get the new state and the value of type
A, you need to invoke the state transformer of typeStateTransformer<S, A>encapsulated in the receiver. Remember that you can do this because you defined theinvokeoperator on theState<S, T>type above. Another option would be simply to invokest(state). - Invoke the input parameter
fn, passing the value of typeAyou 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:
-
skuSerial, which has the typeFun<String, String>and returns the last4characters of the inputString. -
skuStateas theState<Int, String>for the generation of the SKUs. -
skuSerialStateas theState<Int, String>you get fromskuStatepassingskuSerialas an argument ofmap. -
mainthat prints the result ofskuStateandskuSerialStatepassing 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:
- Use
Chain2<A, B, C>in place ofFun2<A, B, C>. - Get the
valueto return withfn(pair.first)(pair.second)instead of passing the two parameters together, likefn(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:
- Define
applforState<S, A>, the same way you did in Chapter 14, “Error Handling With Functional Programming”, to make the code more readable. - Use
liftto createState<Int, Chain3<String, Int, String, String>>, passingcReplaceSuffix. - Do the same for all the input parameters. Note how
inputAppandpostfixApphave typeState<Int, String>, andlastToRemoveApphas typeState<Int, Int>. - 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). - 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:
- Define
apas an extension function forState<S, T>. - Accept a parameter of type
State<S, (T) -> R>as input. Note how the value is a function of type(T) -> R. - Use
State<S, R>as the return type. - Invoke the
State<S, R>constructor, passing a lambda with the current states0as the initial state. - Get the value of type
Tand the new states1, invoking the current receiver with the initial state. - Use the new state
s1as input forfnto get the value of type(T) -> Rand the final states2. - Get the final result, invoking
fnon the value of typeTyou got in Step 5 and using the final states2.
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:
- Define
flatMapas an extension function for theState<S, A>data type. - Provide
fnas an input parameter of type(A) -> State<S, B>. - 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:
-
Use the
State<S, B>constructor, passing the state transformation as a body. Of course, the state transformation has the initial states0as an input parameter. -
Invoke the receiver of type
State<S, A>, passing the initial state. In this way, you get the valueaof typeAand the new states1of typeS. Remember that the type for the stateSnever changes, but its value can. -
Pass the value
aas an input parameter of the functionfnof type(A) -> State<S, B>you have as input, getting a value of typeState<S, B>. To get the finalPair<B, S>, you need to invoke theState<S, B>with the states1.
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:
- Implement
assignSkuWithStateas a function of type(Product) -> State<Int, SkuProduct>responsible for assigning a new SKU to aProductinto aSkuProductand updating the current state. - Implement
assignSkuWithState, simply encapsulating theStateTransformer<Int, SkuProduct>from thecurriedAssignSkuyou defined in Inventory.kt, into aState<Int, SkuProduct>. - Define a simple
Productyou use as an input parameter. - Use
liftto define theState<Int, Product>from the valueprod1of typeProduct. This is the initial state,initialState. - Invoke
flatMap, passing the reference toassignSku. This is where the composition magic happens, and you get aState<Int, SkuProduct>that you save infinalState. - Invoke
finalState, passing the initial state value0, 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:
- You check if the current product is empty or an
FCons<Product>. - If it’s empty, you get a
Nil, and you just need to return it along with the current unchanged state. - If you have an
FCons<Product>, you need to handle the head first, getting a SKU and creating the relatedSkuProduct. For the tail, you just need to invokelistInventoryrecursively.
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:
- The SKUs are assigned in reverse order. This might not be a problem as long as they’re unique.
- 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:
- Define
zipas an extension function ofState<S, A>. - Declare a first input parameter of type
State<S, B>,. - Use the second input parameter,
combineof type(A, B) -> C. - Return a
State<S, C>whose value is the value of typeCyou get by invokingcombineon the values of typesAandB. - Create the result, invoking the
State<S, C>constructor. - Invoke the receiver with the initial state, getting the value of type
Aand the new state. - Use the new state to get the value of type
Band an updated version of the state. - Get the value of type
C, invokingcombine, 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:
- First, define
addSku, which returns aState<Int, SkuProduct>given aProductas input. - Define
inventoryas a function accepting anFList<Product>as input and returning aState<Int, FList<SkuProduct>>as output. - Check if the current
FList<Product>is aNilorFCons<Product>. - If it’s a
Nil, you just return the same encapsulated into aState<Int, FList<SkuProduct>>usinglift. - If it’s an
FCons<Product>, you handle the head first. First, you useliftto get aState<Int, Product>, and then useflatMapwithaddSkuto get aState<Int, SkuProduct>. - Invoke
inventoryrecursively on the tail to get the related state of typeState<Int, FList<SkuProduct>>. - Use
zipto combine theState<Int, FList<SkuProduct>>for the head and the tail in a singleFListyou 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 aStateTransformer<S, T>. - You can make
State<S, T>a functor providing the implementation formap. -
mapon aState<S, T>applies a function to the value of typeTbut 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 theflatMap. - 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.