创建n元笛卡尔积的惯用方式(几组参数的组合)

Eri*_*rik 5 idioms idiomatic set cartesian-product kotlin

要创建两组参数的所有可能组合并对其执行操作,您可以执行以下操作:

setOf(foo, bar, baz).forEach { a ->
    setOf(0, 1).forEach { b ->
        /* use a and b */
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,如果您有更多(可能更多)参数,这很快就会变成毁灭金字塔

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 */
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以使用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)
Run Code Online (Sandbox Code Playgroud)

但是我认为这再好不过了,因为这里存在一些可读性问题:d,c,b和a的动作是相反编写的。无法推断出它们的类型规格,因此必须指定它们。与厄运金字塔相比,它被顺序颠倒了。提供可能值的集合的顺序应该无关紧要,但是,它会:您只想从一堆集合中创建任何组合,但是,在此代码中,每一行都取决于前一行。

可以采用一种惯用的方式来完成类似PythonHaskell的理解之类的工作,在这种情况下,您(几乎像数学符号一样)可以执行以下操作:

{ /* 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!")
}
Run Code Online (Sandbox Code Playgroud)

这很容易理解:没有过多的缩进,首先要执行您感兴趣的操作,非常清楚地定义了数据源,等等。

附注:与发生类似的问题flatMap- flatMap-...- flatMap- map

关于如何在Kotlin中巧妙地创建n元笛卡尔积的任何想法?

Eri*_*rik 15

我自己创建了一个解决方案,所以我不必按照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()
Run Code Online (Sandbox Code Playgroud)

例子:

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)
Run Code Online (Sandbox Code Playgroud)

输出:

[[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]]
Run Code Online (Sandbox Code Playgroud)

该函数cartesianProduct返回一组列表。这些列表存在许多问题:

  • 任何类型信息都会丢失,因为返回的集合包含包含输入集合类型联合的列表。这些列表元素的返回类型是Any?。该函数返回 a Set<List<*>>,即Set<List<Any?>>
  • 根据定义,列表的大小是未知的;它们不像 KotlinPairTriple,根据定义,大小是常数。但是,这些列表/元组的大小应该等于输入集的数量,即上例中的 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"))
Run Code Online (Sandbox Code Playgroud)

输出:

number = 1, maybe = true
number = 1, maybe = false
number = 1, maybe = null
number = 2, maybe = true
number = 2, maybe = false
number = 2, maybe = null
Run Code Online (Sandbox Code Playgroud)

转换的签名(::Parameters在示例中)指定了列表内容的契约。

因为map { ::Parameters.call(*it.toTypedArray()) }不是很好,我创建了第二个扩展函数来为我做这件事:

fun <T> Set<List<*>>.map(transform: KFunction<T>) = map { transform.call(*it.toTypedArray()) }
Run Code Online (Sandbox Code Playgroud)

这样,代码就变得非常地道了:

val parametersList: List<Parameters> = cartesianProduct(e, f).map(::Parameters)
Run Code Online (Sandbox Code Playgroud)

该代码可从这个 GitHub Gist 获得,如果我改进它,我会在那里更新它。还有测试:包含任何空集的笛卡尔积返回空集,正如数学上所预期的那样。我既不是说这是一个最佳解决方案,也不是说它在数学上是合理的(并非每个数学属性都被明确实现和测试),但它适用于问题的目的。

  • 你说得对。查看 Arrow 的源代码,他们具有从 2 到 22 个元素的元组的数据类定义。任何大量的参数都是不太可能的,而且计算量可能太大。我将留下我的答案以供参考,作为一个简单的实现,其中类型信息被删除,这对于不希望 Arrow 作为额外依赖项的人仍然有用。 (2认同)

Oma*_*gra 5

我会建议使用Arrow-kt应用型List。参见示例:

val ints = listOf(1, 2, 3, 4)
val strings = listOf("a", "b", "c")
val booleans = listOf(true, false)

val combined = ListK.applicative()
    .tupled(ints.k(), strings.k(), booleans.k())
    .fix()

// or use the shortcut `arrow.instances.list.applicative.tupled`
// val combined = tupled(ints, strings, booleans)

combined.forEach { (a, b, c) -> println("a=$a, b=$b, c=$c") }
Run Code Online (Sandbox Code Playgroud)

产生笛卡尔积

a = 1,b = a,c = true

a = 1,b = b,c = true

a = 1,b = c,c = true

a = 2,b = a,c = true

a = 2,b = b,c = true

a = 2,b = c,c = true

a = 3,b = a,c = true

a = 3,b = b,c = true

a = 3,b = c,c = true

a = 4,b = a,c = true

a = 4,b = b,c = true

a = 4,b = c,c = true

a = 1,b = a,c = false

a = 1,b = b,c = false

a = 1,b = c,c = false

a = 2,b = a,c = false

a = 2,b = b,c = false

a = 2,b = c,c = false

a = 3,b = a,c = false

a = 3,b = b,c = false

a = 3,b = c,c = false

a = 4,b = a,c = false

a = 4,b = b,c = false

a = 4,b = c,c = false