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

时间:2018-12-12 18:38:20

标签: kotlin set idioms cartesian-product idiomatic

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

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的动作是相反编写的。无法推断出它们的类型规格,因此必须指定它们。与厄运金字塔相比,它被顺序颠倒了。提供可能值的集合的顺序无关紧要,但是,它确实如此:您只想从一堆集合中创建任何组合,但是,在此代码中,每一行都取决于前一行。

有一种惯用的方式来进行类似Python'sHaskell'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元笛卡尔积的任何想法?

5 个答案:

答案 0 :(得分:1)

我建议在List上使用Arrow-ktApplicative。参见示例:

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(), doubles.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") }

哪个生产笛卡尔积

  

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

答案 1 :(得分:1)

我自己创建了一个解决方案,因此不必像Omar's answer那样添加依赖项。

我创建了一个函数,该函数接受任意数量的任意大小的集合:

fun cartesianProduct(vararg sets: Set<*>): Set<List<*>> =
    when (sets.size) {
        0, 1 -> emptySet()
        else -> 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?>>
  • 列表可以是任意大小,但您希望它们具有指定的大小(输入集的数量,在上面的示例中为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。我并不是说这是一个最佳解决方案,也不是说它在数学上是合理的(并不是每个数学属性都得到明确实现和测试),但是它可以解决问题。

答案 2 :(得分:1)

这是@Erik的答案的改编版本,它维护类型安全性并可以在功能链中使用:

fun <T> Collection<Iterable<T>>.getCartesianProduct(): Set<List<T>> =
    if (isEmpty()) emptySet()
    else drop(1).fold(first().map(::listOf)) { acc, iterable ->
        acc.flatMap { list -> iterable.map(list::plus) }
    }.toSet()

如果我们要保持输入大小至少为两个的要求,这就是我重写该解决方案的类型安全的方法:

fun <T> cartesianProduct(a: Set<T>, b: Set<T>, vararg sets: Set<T>): Set<List<T>> =
    (setOf(a, b).plus(sets))
        .fold(listOf(listOf<T>())) { acc, set ->
            acc.flatMap { list -> set.map { element -> list + element } }
        }
        .toSet()

答案 3 :(得分:0)

这是使用任意组合器功能的另一种方法:

fun <T, U, V> product(xs: Collection<T>, ys: Collection<U>, combiner: (T, U) -> V): Collection<V> =
    xs.flatMap { x ->
        ys.map { y ->
            combiner(x, y)
        }
    }

operator fun <T, U> Set<T>.times(ys: Set<U>): Set<Set<*>> =
    product(this, ys) { x, y ->
        if (x is Set<*>) x + y // keeps the result flat
        else setOf(x, y)
    }.toSet()

operator fun <T, U> List<T>.times(ys: List<U>): List<List<*>> =
    product(this, ys) { x, y ->
        if (x is List<*>) x + y // keeps the result flat
        else listOf(x, y)
    }.toList()

// then you can do

listOf(1, 2, 3) * listOf(true, false)

// or

setOf(1, 2, 3) * setOf(true, false)

// you can also use product's combiner to create arbitrary result objects, e.g. data classes

答案 4 :(得分:0)

懒惰地产生结果序列的解决方案。它需要列表,但没有设置。

fun <T> product(vararg iterables: List<T>): Sequence<List<T>> = sequence {

    require(iterables.map { it.size.toLong() }.reduce(Long::times) <= Int.MAX_VALUE) {
        "Cartesian product function can produce result whose size does not exceed Int.MAX_VALUE"
    }

    val numberOfIterables = iterables.size
    val lstLengths = ArrayList<Int>()
    val lstRemaining = ArrayList(listOf(1))

    iterables.reversed().forEach {
        lstLengths.add(0, it.size)
        lstRemaining.add(0, it.size * lstRemaining[0])
    }

    val nProducts = lstRemaining.removeAt(0)

    (0 until nProducts).forEach { product ->
        val result = ArrayList<T>()
        (0 until numberOfIterables).forEach { iterableIndex ->
            val elementIndex = product / lstRemaining[iterableIndex] % lstLengths[iterableIndex]
            result.add(iterables[iterableIndex][elementIndex])
        }
        yield(result.toList())
    }
}

该算法的所有功劳归于Per L,答案是here. 在我的2核计算机上进行了用char生成5x5 2-d数组的测试,大约需要4.4秒才能生成33554432产品。