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 ints 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))
}
}