要创建两组参数的所有可能组合并对它们执行操作,您可以执行以下操作:
setOf(foo, bar, baz).forEach { a ->
setOf(0, 1).forEach { b ->
/* use a and b */
}
}
然而,如果你有(可能很多)更多的参数,这很快就会变成 pyramid of doom :
setOf(foo, bar, baz).forEach { a ->
setOf(0, 1).forEach { b ->
setOf(true, false, null).forEach { c ->
setOf("Hello,", "World!").forEach { d ->
/* use a, b, c and d */
}
}
}
}
你可以用
for
类似地写这个循环,或者像这样不同:val dAction = { d: String -> /* use a, b, c and d */ }
val cAction = { c: Boolean? -> setOf("Hello,", "World!").forEach(dAction) }
val bAction = { b: Int -> setOf(true, false, null).forEach(cAction) }
val aAction = { a: Any? -> setOf(0, 1).forEach(bAction) }
setOf(foo, bar, baz).forEach(aAction)
但我不认为这样更好,因为这里存在一些可读性问题:d、c、b 和 a 的 Action 是反过来写的。它们的类型规范无法推断,因此必须指定它们。与厄运金字塔相比,它顺序颠倒了。提供可能值的集合的顺序应该无关紧要,但确实如此:您只想从一堆集合中创建任何组合,但是,在此代码中,每一行都取决于前一行。
有一种惯用的方式来做类似 Python's 的事情会非常好。或 Haskell's理解,其中您( almost like the mathematical notation )可以执行以下操作:
{ /* use a, b, c and d */
for a in setOf(foo, bar, baz),
for b in setOf(0, 1),
for c in setOf(true, false, null),
for d in setOf("Hello,", "World!")
}
这很容易阅读:没有过多的缩进,您感兴趣的操作在前,数据源定义非常明确,等等。
旁注:类似的问题发生在
flatMap
- flatMap
-...- flatMap
- map
.关于如何在 Kotlin 中巧妙地创建 n-ary 笛卡尔积的任何想法?
最佳答案
我自己创建了一个解决方案,所以我不必按照 Omar's answer 的建议添加依赖项.
我创建了一个需要两组或更多组任意大小的函数:
fun cartesianProduct(a: Set<*>, b: Set<*>, vararg sets: Set<*>): Set<List<*>> =
(setOf(a, b).plus(sets))
.fold(listOf(listOf<Any?>())) { acc, set ->
acc.flatMap { list -> set.map { element -> list + element } }
}
.toSet()
例子:
val a = setOf(1, 2)
val b = setOf(3, 4)
val c = setOf(5)
val d = setOf(6, 7, 8)
val abcd: Set<List<*>> = cartesianProduct(a, b, c, d)
println(abcd)
输出:
[[1, 3, 5, 6], [1, 3, 5, 7], [1, 3, 5, 8], [1, 4, 5, 6], [1, 4, 5, 7], [1, 4, 5, 8], [2, 3, 5, 6], [2, 3, 5, 7], [2, 3, 5, 8], [2, 4, 5, 6], [2, 4, 5, 7], [2, 4, 5, 8]]
函数
cartesianProduct
返回一组列表。这些列表存在许多问题:Any?
.该函数返回 Set<List<*>>
,即 Set<List<Any?>>
. Pair
或 Triple
,其中大小根据定义是常数。但是,这些列表/元组的大小应等于输入集的数量,即上例中的 4。 但是,使用反射,我们可以解决这些问题。我们想要对每个列表执行的操作可以写成一个函数(例如某个类的构造函数,它也只是一个函数):
data class Parameters(val number: Int, val maybe: Boolean?) {
override fun toString() = "number = $number, maybe = $maybe"
}
val e: Set<Int> = setOf(1, 2)
val f: Set<Boolean?> = setOf(true, false, null)
val parametersList: List<Parameters> = cartesianProduct(e, f).map { ::Parameters.call(*it.toTypedArray()) }
println(parametersList.joinToString("\n"))
输出:
number = 1, maybe = true
number = 1, maybe = false
number = 1, maybe = null
number = 2, maybe = true
number = 2, maybe = false
number = 2, maybe = null
转换的签名(示例中的
::Parameters
)指定了列表内容的契约。因为
map { ::Parameters.call(*it.toTypedArray()) }
不是很好,我创建了第二个扩展函数来为我做这件事:fun <T> Set<List<*>>.map(transform: KFunction<T>) = map { transform.call(*it.toTypedArray()) }
这样,代码就变得非常地道了:
val parametersList: List<Parameters> = cartesianProduct(e, f).map(::Parameters)
该代码可从 this GitHub Gist 获得,如果我改进它,我会更新它。还有测试:包含任何空集的笛卡尔积返回空集,as is mathematically expected .我既不是说这是一个最佳解决方案,也不是说它在数学上是合理的(并非每个数学属性都被明确实现和测试),但它适用于问题的目的。
关于kotlin - 创建 n 元笛卡尔积的惯用方法(多组参数的组合),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53749357/