Given two sets of "things", A
and B
, the Cartesian product is the set of all possible pairs (a, b)
where a
is a thing from set A
and b
is a thing from set B
. A real life example: let's say you have 3 shirts and 2 pairs of pants. What possible outfits can you create?
Call the set of shirts S
. Then, S = {shirt-1, shirt-2, shirt-3}
.
Call the set of pants P
. Then P = {pants-1, pants-2}
.
How many ways can we combine these items into an outfit (i.e. a shirt and pants)? Let's represent the possibilities as pairs of (s, p)
, where s
is in S
and p
is in P
. Then we have:
(shirt-1, pants-1)
(shirt-2, pants-1)
(shirt-3, pants-1)
(shirt-1, pants-2)
(shirt-2, pants-2)
(shirt-3, pants-2)
This collection of ordered pairs is the Cartesian product of S
and P
.
cartesian
is a Go package that makes it easy to compute and return the Cartesian product of an arbitrary number of slices of varying types.
- Go 1.18 or higher
Consider the following code:
package main
import (
"fmt"
"github.com/zaataylor/cartesian/cartesian"
)
type Job struct {
name string
id int
}
func main() {
sliceA := []int{1, 8}
sliceB := []bool{true, false}
sliceJ := []Job{
{
name: "test job",
id: 1,
},
{
name: "another test job",
id: 2,
},
}
slices := []any{sliceA, sliceB, sliceJ}
// Construct Cartesian product of these slices
cp := cartesian.NewCartesianProduct(slices)
fmt.Printf("Cartesian product of slices:\n%s", cp)
}
Running this code returns:
Cartesian product of slices:
[
[1, true, {name:test job id:1}],
[1, true, {name:another test job id:2}],
[1, false, {name:test job id:1}],
[1, false, {name:another test job id:2}],
[8, true, {name:test job id:1}],
[8, true, {name:another test job id:2}],
[8, false, {name:test job id:1}],
[8, false, {name:another test job id:2}],
]
The sections below illustrate how to use cartesian
effectively.
Put the slices you want to compute the cartesian of inside of an []any
-type slice. Then, provide this slice as input to NewCartesianProduct()
. For example:
sliceA := []int{4, 5, 8}
sliceB := []bool{true, false}
input := []any{sliceA, sliceB}
cp := cartesian.NewCartesianProduct(input)
First, use Iterator()
to create a CartesianProductIterator
. Then, use the iterator's HasNext()
method as an indicator of when to continue iterating, and Next()
to return the iterands themselves. If you want to iterate over indices instead, use NextIndices()
. For example:
// Construct the Cartesian product
sliceA := []int{4, 5, 8}
sliceB := []bool{true, false}
input := []any{sliceA, sliceB}
cp := cartesian.NewCartesianProduct(input)
// Construct the Cartesian Product iterator
cpi := cp.Iterator()
// Iterate over its values and print them out
for cpi.HasNext() {
value := cpi.Next()
fmt.Printf("Value is: %v\n", element)
}
// Reset the iterator, if you'd like...
cpi.ResetIterator()
// Iterate over its indices and print them out
for cpi.HasNext() {
indices := cpi.NextIndices()
fmt.Printf("Indices are: %v\n", indices)
}
// ...or, create a new iterator
newCpi := cp.Iterator()
When you first call NewCartesianProduct()
, it computes the full Cartesian Product as part of its initialization process. You can then use Values()
to print or iterate over the values of the product. Example:
// Construct the Cartesian product
sliceA := []int{4, 5, 8}
sliceB := []bool{true, false}
input := []any{sliceA, sliceB}
cp := cartesian.NewCartesianProduct(input)
// Iterate over its values and print them out
for _, v := range cp.Values() {
fmt.Printf("Value is: %#v\n", v)
// Assign values to individual variables
firstValue, secondValue := v[0], v[1]
fmt.Printf("First item: %v; Second item: %v", firstValue, secondValue)
}
There are times when you might want/need to obtain indices into each of the passed-in slices, then directly index into each slice yourself to obtain the values corresponding to an element of the Cartesian product. In this case, it's best to use Indices()
, as it will return a slice of int
s where each element is an index into a specific slice. One use case for this is computing Cartesian products with a slice of functions and slices of args, then applying the args from the product to the functions:
// Is it stonks???
func isStonksFunc(isStonks bool, company string) string {
if isStonks {
return fmt.Sprintf("%s is stonks! 👍", company)
}
return fmt.Sprintf("%s is NOT stonks! 👎", company)
}
func isOneFunc(isOne bool, _ string) string {
if isOne {
return "isOne"
}
return "isZero"
}
func main() {
sliceB := []bool{true, false}
sliceS := []string{"MSFT", "GOOG", "META"}
// Function slice
sliceF := make([]func(bool, string) string, 0)
sliceF = append(sliceF, isStonksFunc)
sliceF = append(sliceF, isOneFunc)
// Construct Cartesian product
anotherInput := []any{sliceF, sliceB, sliceS}
cp := cartesian.NewCartesianProduct(anotherInput)
// Explanation: Iterate over indices, and use them to obtain a specific
// Cartesian product by indexing into each slice one by one.
// Then, split the product into a function and two args, and apply those
// args to the function, printing out the result.
for _, indexes := range cp.Indices() {
fn, boolArg, stringArg := sliceF[indexes[0]], sliceB[indexes[1]], sliceS[indexes[2]]
fmt.Printf("Function Name: %v, boolArg: %v, stringArg: %v --> Result: %v\n", cartesian.GetFunctionName(fn), boolArg, stringArg, fn(boolArg, stringArg))
}
}