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
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
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数据结构
A
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 } } |
我正在使用列表的
给定一个
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升的杯子,最初装有零升水,表示为
类似地,将这些状态中的每个状态推进到一组新状态将看起来像这样(以某种简化的形式):
随着每个基于所有可能的移动进入下一组状态,可以将其视为可能路径的爆炸式增长。这就是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 })) } } } |
因此,现在,给定从初始状态到新状态的潜在路径流,对
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一样优雅。
DIV>