Water Pouring Problem with Kotlin and Vavr
The first time I saw the Water Pouring Problem being programmatically solved was the excellentlectures on functional Programming by Martin Odersky on Coursera. The solution demonstrates the power of
lazy evaluation in Streams with Scala.
Solving Water Pouring Problem using Kotlin
I wanted to explore how I can rewrite the solution described by Martin Odersky using Kotlin and I realized two things – one is that the immutable data structures that Kotlin offers are simply wrappers over Java Collections library and are not truly immutable, secondly the solution using Streams feature in Java will be difficult. However, theVavr offers a good alternative to both – a first-class Immutable collections library and a Streams library and I took a crack at replicating the solution with Kotlin and Vavr.
A Cup looks like this, represented as aKotlin data class:
1 2 3 4 5 6 7 8 | import io.vavr.collection.List data class Cup(val level: Int, val capacity: Int) { override fun toString(): String { return "Cup($level/$capacity)" } } |
Since the water pouring problem repesents the “state” of a set of cups, this can be simply represented as a “typealias” the following way:
1 | typealias State = List<Cup> |
There are 3 different types of moves that can be performed with the water in the cup – Empty it, Fill it, or Pour from one cup to another, represented again as Kotlin Data classes:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | interface Move { fun change(state: State): State } data class Empty(val glass: Int) : Move { override fun change(state: State): State { val cup = state[glass] return state.update(glass, cup.copy(level = 0 )) } override fun toString(): String { return "Empty($glass)" } } data class Fill(val glass: Int) : Move { override fun change(state: State): State { val cup = state[glass] return state.update(glass, cup.copy(level = cup.capacity)) } override fun toString(): String { return "Fill($glass)" } } data class Pour(val from: Int, val to: Int) : Move { override fun change(state: State): State { val cupFrom = state[from] val cupTo = state[to] val amount = min(cupFrom.level, cupTo.capacity - cupTo.level) return state .update(from, cupFrom.copy(cupFrom.level - amount)) .update(to, cupTo.copy(level = cupTo.level + amount)) } override fun toString(): String { return "Pour($from,$to)" } } |
The implementation is making use of Vavr’s List data structures “update” method to create a new list with just the relevant elements updated.
A “Path” represents a history of moves leading to the current state:
1 2 3 4 5 6 7 | data class Path(val initialState: pour.State, val endState: State, val history: List<Move>) { fun extend(move: Move) = Path(initialState, move.change(endState), history.prepend(move)) override fun toString(): String { return history.reverse().mkString( " " ) + " ---> " + endState } } |
I am using the “prepend” method of the list to add elements to the beginning of history. Prepending to a list is an O(1) operation whereas appending is O(n), hence the choice.
Given a “state”, a set of possible moves to change the “state” are the following –
1. Empty the glasses –
1 | ( 0 until count).map { Empty(it) } |
2. Fill the glasses –
1 | ( 0 until count).map { Fill(it) } |
3. Pour from one glass to another –
1 2 3 4 5 | ( 0 until count).flatMap { from -> ( 0 until initialState.length()).filter { to -> from != to }.map { to -> Pour(from, to) } } |
Now, all these moves are used for advancing from one state to another. Consider say 2 cups with capacity of 4 and 9 litres, initially filled with 0 litres of water, represented as “List(Cup(0/4), Cup(0/9))”, with all possible moves the next set of states of the cups are the following:
![Water Pouring Problem](http://www.javacodegeeks.com/wp-content/uploads/2019/03/FirstMove-1024x256.png)
Similarly, advancing each of these states to a new set of states would like this(in a somewhat simplified form):
![Water Pouring Problem](http://www.javacodegeeks.com/wp-content/uploads/2019/03/SecondMove-1024x344.png)
As each State advances to a next set of states based on all possible moves, it can be seen that there will be an explosion of possible paths, this is where laziness offered by theStream data structure of Vavr comes in. The values in a stream are only computed on request.
Given a set of paths, new paths are created using Stream the following way:
01 02 03 04 05 06 07 08 09 10 11 12 13 | fun from(paths: Set<Path>, explored: Set<State>): Stream<Set<Path>> { if (paths.isEmpty) { return Stream.empty() } else { val more = paths.flatMap { path -> moves.map { move -> val next: Path = path.extend(move) next }.filter { !explored.contains(it.endState) } } return Stream.cons(paths) { from(more, explored.addAll(more.map { it.endState })) } } } |
So, now given a stream of potential paths from the initial state to a new state, a solution to a “target” state becomes:
1 2 3 4 5 | val pathSets = from(hashSet(initialPath), hashSet()) fun solution(target: State): Stream<Path> { return pathSets.flatMap { it }.filter { path -> path.endState == target } } |
That covers the solution, a test with this code looks like this – there are two cups of 4 litre and 9 litre capacity, initially filled with 0 litres of water. The final target state is to get the second cup filled with 6 litres of water:
1 2 3 4 5 6 7 | val initialState = list(Cup( 0 , 4 ), Cup( 0 , 9 )) val pouring = Pouring(initialState) pouring.solution(list(Cup( 0 , 4 ), Cup( 6 , 9 ))) .take( 1 ).forEach { path -> println(path) } |
when run, this spits out the following solution:
1 | Fill( 1 ) Pour( 1 , 0 ) Empty( 0 ) Pour( 1 , 0 ) Empty( 0 ) Pour( 1 , 0 ) Fill( 1 ) Pour( 1 , 0 ) Empty( 0 ) ---> List(Cup( 0 / 4 ), Cup( 6 / 9 )) |
Graphically represented, the solution looks like this:
![Water Pouring Problem](http://www.javacodegeeks.com/wp-content/uploads/2019/03/sample-255x1024.png)
It may be easier to simply follow a working version of the sample which is available in my github repohttps://github.com/bijukunjummen/algos/blob/master/src/test/kotlin/pour/Pouring.kt
Conclusion
Although Kotlin lacks first class support for native immutable datastructures, I feel that a combination ofVavr with Kotlin makes for a solution that is as elegant as the Scala one.
Published on Java Code Geeks with permission by Biju Kunjummen, partner at our JCG program. See the original article here: Water Pouring Problem with Kotlin and Vavr Opinions expressed by Java Code Geeks contributors are their own. |
You’ve some whitespace issues with the link formatting here.