Kotlin和Vavr的注水问题

Water Pouring Problem With Kotlin and Vavr

我第一次看到以编程方式解决了"浇水问题",这是由Martin Odersky在Coursera上进行的功能编程的讲座。该解决方案展示了使用Scala在Streams中进行惰性评估的强大功能。

用Kotlin解决倒水问题

我想探索如何使用Kotlin重写Martin Odersky描述的解决方案。我意识到了两件事-一是Kotlin提供的不可变数据结构只是Java Collections库的包装,而并非真正的不可变。其次,使用Java中的Streams功能的解决方案将很困难。但是,Vavr提供了一个很好的替代方案,即一流的Immutable集合库和Streams库。考虑到这一点,我努力地与Kotlin和Vavr复制该解决方案。

A Cup看起来像这样,以Kotlin数据类表示:

1
2
3
4
5
6
7
import io.vavr.collection.List

data class Cup(val level: Int, val capacity: Int) {
    override fun toString(): String {
        return"Cup($level/$capacity)"
    }
}

由于倒水问题代表了一组杯子的"状态",因此可以通过以下方式简单地表示为a typealias

1
typealias state = List<Cup>

杯子中的水可以执行三种不同类型的移动-从一个杯子到另一个杯子,再由Kotlin数据类表示:

1
2
3
4
5
6
7
8
9
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)"
    }
}

该实现利用Vavr的List数据结构update方法来创建一个新列表,只更新相关的元素。

A Path表示导致当前状态的移动历史:

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
    }
}

我正在使用列表的prepend方法将元素添加到历史记录的开头。到列表的前面是O(1)操作,而后面的是O(n),因此是选择。

给定一个state,以下是一组更改state的可能动作:

1.清空眼镜:

1
(0 until count).map { Empty(it) }

2.加满眼镜:

1
(0 until count).map { Fill(it) }

3.从一杯倒入另一杯:

1
2
3
4
5
(0 until count).flatMap { from ->
    (0 until initialstate.length()).filter { to -> from != to }.map { to ->
        Pour(from, to)
    }
}

现在,所有这些举动都用于从一种状态前进到另一种状态。假设有两个容量为4升和9升的杯子,最初装有零升水,表示为List(Cup(0/4), Cup(0/9))。通过所有可能的移动,杯子的下一组状态如下:

类似地,将这些状态中的每个状态推进到一组新状态将看起来像这样(以某种简化的形式):

随着每个基于所有可能的移动进入下一组状态,可以将其视为可能路径的爆炸式增长。这就是Vavr的Stream数据结构提供的惰性的地方。流中的值仅应要求计算。

给定一组路径,使用Stream通过以下方式创建新路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun from(Paths: Set<p>
, explored: Set<state>): Stream<Set<p>
> {
    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 })) }
    }
}

因此,现在,给定从初始状态到新状态的潜在路径流,对target状态的解决方案变为:

1
2
3
4
5
6
val PathSets = from(hashSet(initialPath), hashSet())

fun solution(target: state): Stream<p>
 {
    return PathSets.flatMap { it }.filter { Path -> Path.endstate == target }
}

涵盖了解决方案。用此代码进行的测试如下所示:有两个杯子,容量分别为4升和9升,最初装有0升水。最终的目标状态是使第二个杯子装满6升水:

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)
    }

运行时,这会吐出以下解决方案:

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))

仅遵循示例的工作版本可能会更容易,该示例在我的GitHub存储库中可用。

结论

尽管Kotlin缺乏对本机不可变数据结构的一流支持,但我感到Vavr与Kotlin的结合使该解决方案像Scala一样优雅。