kotlin - 创建 n 元笛卡尔积的惯用方法(多组参数的组合)

标签 kotlin set cartesian-product idioms

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

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?>> .
  • 根据定义,列表的大小是未知的;他们不像 Kotlin PairTriple ,其中大小根据定义是常数。但是,这些列表/元组的大小应等于输入集的数量,即上例中的 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/

    相关文章:

    Kotlin 对 "supposedly"正确类型的类型推断

    java - HashSet "cannot convert from element type Object to Integer"期望整数的 HashSet

    python - 在Python中获取一系列表格的笛卡尔积

    matlab - Matlab 中行索引的笛卡尔积

    postgresql - 确定 PostgreSQL 中关系之间路径的存在

    sql - JOIN on DATEPART 月份和年份会导致额外的行

    java - Kotlin 类类型参数 : Mismatch undetected

    java - Maven 与 Kotlin 1.2 : A required class was missing kotlin/reflect/KDeclarationContainer

    android - 如何使用 navGraph 范围初始化 viewModel

    c++ - C++中这些之间有什么区别?