Store

Store package

Learn the basics of modeling with the Nextmv SDK and the store of variables

This reference guide assumes you already completed the steps described in the 5-minute getting started experience. To test that the Nextmv CLI is correctly configured, you can optionally run the following command on your terminal. It will get some files that are necessary to work with the Nextmv Platform. You can see the expected output as well.

nextmv sdk install
Copy

A tour of data store modeling with the Nextmv Software Development Kit (SDK).

Christopher Robin goes
Hoppity, hoppity,
Hoppity, hoppity, hop.
Whenever I tell him
Politely to stop it, he
Says he can't possibly stop.

If he stopped hopping,
He couldn't go anywhere,
Poor little Christopher
Couldn't go anywhere...
That's why he always goes
Hoppity, hoppity,
Hoppity,
Hoppity,
Hop.

-- A. A. Milne

The Nextmv SDK lets you automate any operational decision in a way that looks and feels like writing other code. It provides the guardrails to turn your data into automated decisions, and test and deploy them into production environments.

Let's see how.

Hello Nextmv

Any decision model built on the Nextmv SDK requires a a Go module. A module manages your dependencies, including the Nextmv SDK.

Go to some location in your computer where you can set up the tour. In that location, create a go module called tour. You should obtain an output similar to the one depicted.

go mod init tour
Copy

Now add the Nextmv SDK to your dependencies. You should observe the latest version being pulled as part of the output.

go get github.com/nextmv-io/sdk@latest
Copy

You should now have a go.mod file that looks similar to this one:

module tour

go 1.19

require github.com/nextmv-io/sdk VERSION // indirect
Copy

Create a main.go file that looks just like this and save it in the same location as the go.mod file.

package main

import (
	"fmt"

	"github.com/nextmv-io/sdk/store"
)

func main() {
	s := store.New()
	nextmv := store.NewVar(s, "Nextmv")
	fmt.Printf("Hello %s!\n", nextmv.Get(s))
}
Copy

Run the program using the Nextmv CLI.

nextmv sdk run main.go
Copy

You should see an output that displays the version of the Nextmv SDK used:

--------------------------------------------------------------------------------
This software is provided by Nextmv.
The current Nextmv token expires in 89 days.

© 2019-2023 nextmv.io inc. All rights reserved.

    (\/)     (\/)     (\/)     (\/)     (\/)     (\/)     (\/)     (\/)     (\/)
    (^^)     (^^)     (^^)     (^^)     (^^)     (^^)     (^^)     (^^)     (^^)
   o( O)    o( O)    o( O)    o( O)    o( O)    o( O)    o( O)    o( O)    o( O)
--------------------------------------------------------------------------------
Hello Nextmv!
Copy

If you see output like the above, you're ready to get hopping! Each of the examples in this tour constitute a standalone main.go. You can either place them in unique directories (packages) inside your tour module location (where the go.mod is located) or replace the code you just ran.

You can run each example using the same command shown above. Note that the banner is printed to stderr and the actual output to stdout, meaning the banner will never be part of the output.

Creating stores

A Store is lexical scope, similar to the lexical scope in most programming languages. It can hold variable declarations, variable assignments, and logic.

s := store.New()
Copy

You can add any Var to a store. The store will manage the variables for you.

x := store.NewVar(s, 42)                    // x is a stored int
y := store.NewVar(s, []float64{3.14, 2.72}) // y is a stored []float64
Copy

You can retrieve typed variable values from the store with their Get methods. The store knows which type they are so you don't have to think about it.

fmt.Println(
	x.Get(s)*10,
	y.Get(s)[0],
)
Copy

You can put this together and try it. This is the complete main.go program:

package main

import (
	"fmt"

	"github.com/nextmv-io/sdk/store"
)

func main() {
	s := store.New()

	x := store.NewVar(s, 42)                    // x is a stored int
	y := store.NewVar(s, []float64{3.14, 2.72}) // y is a stored []float64

	fmt.Println(
		x.Get(s)*10,
		y.Get(s)[0],
	)
}
Copy

Run it using the Nextmv CLI. You should obtain an output like the one shown.

nextmv sdk run main.go
Copy

For future examples, you can save the files wherever you like under a subdirectory of the tour module folder, so long as there is only one func main() in any subfolder.

Exercises - creating stores

  • Add more variables of different types to the store. Print their values.
  • Create a second store and add variables to it.
  • What happens when you retrieve a value from the wrong store?

Updating data

You can think of a store as a lexical scope containing variable declarations, variable assignments, and logic. Thus a store has similar mechanics to a block in a lexically scoped programming language without destructive assignment. For example, say you have outer and inner blocks with the following assignments.

{
    x = 42
    y = "foo"

    {
        y = "bar"
        pi = 3.14
    }
}
Copy

The outer block contains two variables, x = 42 and y = "foo". The inner block inherits x = 42 from the outer block which contains it, overrides y = "bar", and adds a new variable pi = 3.14. Assignments in the inner block do not impact the outer block.

You can code this example up. First define a store s1 and add x and y to it.

s1 := store.New()
x := store.NewVar(s1, 42)
y := store.NewVar(s1, "foo")
Copy

Now apply a changeset to s1. This results in a new store, s2. s2 is functionally a copy of s1 with a new value associated with y and a new variable, pi.

s2 := s1.Apply(y.Set("bar"))
pi := store.NewVar(s2, 3.14)
Copy

If you query:

fmt.Println("s1:", x.Get(s1), y.Get(s1))
fmt.Println("s2:", x.Get(s2), y.Get(s2), pi.Get(s2))
Copy

And put everything together in this main.go source:

package main

import (
	"fmt"

	"github.com/nextmv-io/sdk/store"
)

func main() {
	s1 := store.New()
	x := store.NewVar(s1, 42)
	y := store.NewVar(s1, "foo")

	s2 := s1.Apply(y.Set("bar"))
	pi := store.NewVar(s2, 3.14)

	fmt.Println("s1:", x.Get(s1), y.Get(s1))
	fmt.Println("s2:", x.Get(s2), y.Get(s2), pi.Get(s2))
}
Copy

You can run the program above and see output similar to this:

nextmv sdk run main.go
Copy

Note that calling y.Set("bar") creates a Change. The Apply method accepts any number of changes and applies them in order to create a new store. Thus the code below creates a single store with x = 10 and y = "abc".

s1.Apply(
    x.Set(-3),
    y.Set("bar"),
    y.Set("abc"),
    x.Set(100),
    x.Set(10),
)
Copy

Exercises - updating data

  • Apply multiple changes to s1 to create a new store s3. Does s3 impact s2 in any way?
  • Try to store a slice on s1 then append to it when applying changes to create both s2 and s3. What do you expect to happen? What happens?

Storing custom data

A store can manage any concrete type, including custom structs. You can define a bunny type with a few fields and a String method.

type bunny struct {
	name       string
	fluffiness float64
	activities []string
}

func (b bunny) String() string {
	fluffy := "fluffy"
	if b.fluffiness < 0.5 {
		fluffy = "not fluffy"
	}

	return fmt.Sprintf("%s is %s and likes %v", b.name, fluffy, b.activities)
}
Copy

Now create a bunny and add it to the store.

s := store.New()

peter := store.NewVar(s, bunny{
	name:       "Peter Rabbit",
	fluffiness: 0.52,
	activities: []string{
		"stealing vegetables",
		"losing shoes",
	},
})
Copy

If you retrieve the value of peter from the store and print it, you should get the results of the bunny.String method. Note that peter.Get returns a value of type bunny, so Go knows to call the appropriate String method when you pass it to fmt.Println.

fmt.Println(peter.Get(s))
Copy

Here is the complete main.go program.

package main

import (
	"fmt"

	"github.com/nextmv-io/sdk/store"
)

type bunny struct {
	name       string
	fluffiness float64
	activities []string
}

func (b bunny) String() string {
	fluffy := "fluffy"
	if b.fluffiness < 0.5 {
		fluffy = "not fluffy"
	}

	return fmt.Sprintf("%s is %s and likes %v", b.name, fluffy, b.activities)
}

func main() {
	s := store.New()

	peter := store.NewVar(s, bunny{
		name:       "Peter Rabbit",
		fluffiness: 0.52,
		activities: []string{
			"stealing vegetables",
			"losing shoes",
		},
	})

	fmt.Println(peter.Get(s))
}
Copy

Run this source and you should see an educational message about this Peter Rabbit fellow.

nextmv sdk run main.go
Copy

Exercises - storing custom data

  • Add another bunny to the store and call its String method directly.
  • Add another method to the bunny type. Retrieve peter from the store and call this new method.

Formatting stores

The store was designed to work well with JSON data. This makes it easy to deploy models into microservices, run them as serverless functions, and many other things. A store can be directly encoded into JSON as a representation of its variable assignments.

You can create a new store and encode it into JSON.

enc := json.NewEncoder(os.Stdout)

s := store.New()
if err := enc.Encode(s); err != nil {
	panic(err)
}
Copy

This should write an empty list to stdout because the store is empty.

--------------------------------------------------------------------------------
Copy

You can add some variables to the store.

x := store.NewVar(s, 42)
y := store.NewVar(s, "foo")
pi := store.NewVar(s, 3.14)
if err := enc.Encode(s); err != nil {
	panic(err)
}
Copy

By default, the JSON representation of a store contains its variable assignments in order of declaration.

This may be fine, or you may rather reshape the format into something more convenient. You can turn that JSON list into a map with the variable names. To do this, use the Format method. Format is similar to Apply, in that it doesn't change the existing store, but applies a change to create a new one. The difference is that now you are adding logic to the store instead of a change to a variable assignment. Stores give you very specific ways to introduce logic.

s = s.Format(func(s store.Store) any {
	return map[string]any{
		"x":  x.Get(s),
		"y":  y.Get(s),
		"pi": pi.Get(s),
	}
})
if err := enc.Encode(s); err != nil {
	panic(err)
}
Copy

Now you should see your store encoded as a map. This isn't that far from where you started, but is more useful! Since Format can reshape a store into anything that can be encoded in JSON, it is easy to make the output match anything you might expect in a production environment.

[42,"foo",3.14]
Copy

Finally, apply a change to a variable assignment and encode the resulting store.

if err := enc.Encode(s.Apply(y.Set("bar"))); err != nil {
	panic(err)
}
Copy

Note how the new store inherits the formatting logic.

{"pi":3.14,"x":42,"y":"foo"}
Copy

Here is the complete main.go program you may run to obtain the results discussed in this section.

package main

import (
	"encoding/json"
	"os"

	"github.com/nextmv-io/sdk/store"
)

func main() {
	enc := json.NewEncoder(os.Stdout)

	s := store.New()
	if err := enc.Encode(s); err != nil {
		panic(err)
	}

	x := store.NewVar(s, 42)
	y := store.NewVar(s, "foo")
	pi := store.NewVar(s, 3.14)
	if err := enc.Encode(s); err != nil {
		panic(err)
	}

	s = s.Format(func(s store.Store) any {
		return map[string]any{
			"x":  x.Get(s),
			"y":  y.Get(s),
			"pi": pi.Get(s),
		}
	})
	if err := enc.Encode(s); err != nil {
		panic(err)
	}

	if err := enc.Encode(s.Apply(y.Set("bar"))); err != nil {
		panic(err)
	}
}
Copy

Exercises - formatting stores

  • Start with the source above and reshape the output into something more complex than a map. Can you format it as a map of maps or as a user-defined structure? How do the rules of the encoding/json library apply to the output?
  • What happens if you override the formatting logic of a child store? Does that impact the parent or any sibling stores?

Slices

Go passes arguments to functions and makes variable assignments by value. This applies to stores as well: a store will assign to a declared variable by value. If that value is of a type like an int or a struct, then it is copied before assigning it to a variable in a store. This is unsurprising.

It gets a little hairier when assigning a pointer type like a slice or a map to a variable. The pointer value is copied, but not the data it refers to. While the store doesn't keep you from storing pointers, it's usually not what you want. Instead, it provides immutable containers so you can update rich data structures in a safe and efficient manner in your model.

The simplest of these container types is an immutable Slice. You can store a slice using the special NewSlice function.

s1 := store.New()
x := store.NewSlice(s1, "c", "d", "e") // []string{"c", "d", "e"}
Copy

You can assign any type to the slice. In this case, the store infers that you are storing a slice of strings with initial value ["c", "d", "e"] from the parameters. If you want to create an empty slice, the store needs to know the type.

empty := store.NewSlice[string](s1)    // []string{}
Copy

The slice x has several methods that query properties of the slice for a given store, such as Get, which retrieves the value of an index, and Len, which returns the length of the slice. The Slice method returns the underlying slice data as a standard Go slice, like []string.

It also provides methods for creating changes. You may apply these changes to create new stores.


s2 := s1.Apply(
	x.Append("h", "i", "j"),
	x.Prepend("a", "y", "z"),
)

s3 := s2.Apply(
	x.Insert(6, "f", "g"),
	x.Remove(2, 2),
	x.Set(1, "b"),
Copy

Here is the complete main.go program.

package main

import (
	"fmt"

	"github.com/nextmv-io/sdk/store"
)

func main() {
	s1 := store.New()
	x := store.NewSlice(s1, "c", "d", "e") // []string{"c", "d", "e"}
	empty := store.NewSlice[string](s1)    // []string{}

	s2 := s1.Apply(
		x.Append("h", "i", "j"),
		x.Prepend("a", "y", "z"),
	)

	s3 := s2.Apply(
		x.Insert(6, "f", "g"),
		x.Remove(2, 2),
		x.Set(1, "b"),
	)

	fmt.Println(x.Slice(s1))
	fmt.Println(empty.Slice(s1))
	fmt.Println(x.Slice(s2))
	fmt.Println(x.Slice(s3))
}
Copy

Run this source and you should see the slice as it is stored across different stores.

nextmv sdk run main.go
Copy

Exercises - slices

  • Try to guess what s1, s2, and s3 contain. Run the source and see if you are right.
  • Create a slice of a user-defined type. Insert values into it and retrieve the underlying slice contents.
  • Use the Len and Get methods to iterate over a slice and print its values one at a time.

Maps

Just like stores provide an immutable slice type and various methods for creating new slices from existing ones, it also provides a Map collection. Like slices, maps can store any type of value. However, they only allow either int or string keys.

A map is initialized empty and therefore requires its key and value types.

s1 := store.New()
x := store.NewMap[string, float64](s1) // map[string]float64{}
Copy

You can assign values to keys in a map using its Set method. Like other types associated with a store, instead of mutating the underlying data in a maps, this returns a change to apply to a new store.

s2 := s1.Apply( // map[string]float64{"pi": 3.14, "e": 2.72}
	x.Set("pi", 3.14),
	x.Set("e", 2.72),
)
Copy

Maps can assign new values to existing keys through subsequent calls to Set. They can also remove keys entirely using their Delete method.

s3 := s2.Apply(x.Delete("e")) // map[string]float64{"pi": 3.14}
Copy

Among the other methods available on the map collection, the Map method returns its underlying representation.

fmt.Println(x.Map(s3))
Copy

Here is the complete main.go program.

package main

import (
	"fmt"

	"github.com/nextmv-io/sdk/store"
)

func main() {
	s1 := store.New()
	x := store.NewMap[string, float64](s1) // map[string]float64{}

	s2 := s1.Apply( // map[string]float64{"pi": 3.14, "e": 2.72}
		x.Set("pi", 3.14),
		x.Set("e", 2.72),
	)

	s3 := s2.Apply(x.Delete("e")) // map[string]float64{"pi": 3.14}

	fmt.Println(x.Map(s1))
	fmt.Println(x.Map(s2))
	fmt.Println(x.Map(s3))
}
Copy

Run this source and you should see the map as it is stored across different stores.

nextmv sdk run main.go
Copy

Exercises - maps

  • Try to guess what s1, s2, and s3 contain. Run the source and see if you are right.
  • Create a map with int keys and values of a custom type. Set values on the map and retrieve its underlying representation.

Domains

Domains are a special type. A domain stores integers which typically represent potential choices. For example, a domain may represent the hours a shift might start or the destinations a traveler could arrive at.

Structurally, a domain is an ordered, compact, set of integers. Domains maintain a minimal representation of ranges as we apply operators to them to create new ones. They are also, conveniently, immutable.

The Nextmv SDK provides two domain types: model.Domain and store.Domain. model.Domain is just an integer domain, unattached to a store. store.Domain has many of the same methods but with similar mechanics to store.Slice and store.Map. Thus, model.Domain is the underlying type for store.Domain, which must be associated with a store.

You can create a few domains to see how they work.

s1 := store.New()
d1 := store.NewDomain(s1)
d2 := store.NewDomain(s1, model.NewRange(1, 10))
d3 := store.NewDomain(s1, model.NewRange(-5, 5), model.NewRange(15, 25))
Copy

You can pass as many integer ranges as you want to store.NewDomain. If you pass none, the domain is empty, like d1. d2 contains the integers 1 through 10, while d3 contains two disjoint ranges of integers, -5 though 5 and 15 through 25.

You can apply a changeset to these domains and encode the store into JSON.

s2 := s1.Apply(
	d1.Add(42, 43, 44),
	d2.Remove([]int{2, 4, 6, 8, 10}),
	d3.AtLeast(10),
)

enc := json.NewEncoder(os.Stdout)
if err := enc.Encode(s2); err != nil {
	panic(err)
}
Copy

In s2, d1 contains 42 through 44, d2 contains only odd numbers, and d3 contains the range 15 through 25. Note that each of these domains is encoded in a compact JSON representation. This is similar to its internal data.

--------------------------------------------------------------------------------
Copy

Frequently, you might want to create and operate on multiple domains at once. You can use functions like store.NewDomains or store.Repeat to create a slice of related domains. In the code below, five domains are created, each containing the values 1 through 10.

s3 := store.New()
d := store.Repeat(s3, 5, model.NewDomain(model.NewRange(1, 10)))
Copy

The store.Domains type has many of the same methods as the store.Domain type. Change methods require a domain index to modify as their first argument.

s4 := s3.Apply(
	d.Add(0, 42),
	d.Assign(2, 5),
	d.Remove(4, []int{2, 3, 4}),
)
if err := enc.Encode(s4); err != nil {
	panic(err)
}
Copy

Here is the complete main.go program.

package main

import (
	"encoding/json"
	"os"

	"github.com/nextmv-io/sdk/model"
	"github.com/nextmv-io/sdk/store"
)

func main() {
	s1 := store.New()
	d1 := store.NewDomain(s1)
	d2 := store.NewDomain(s1, model.NewRange(1, 10))
	d3 := store.NewDomain(s1, model.NewRange(-5, 5), model.NewRange(15, 25))

	s2 := s1.Apply(
		d1.Add(42, 43, 44),
		d2.Remove([]int{2, 4, 6, 8, 10}),
		d3.AtLeast(10),
	)

	enc := json.NewEncoder(os.Stdout)
	if err := enc.Encode(s2); err != nil {
		panic(err)
	}

	s3 := store.New()
	d := store.Repeat(s3, 5, model.NewDomain(model.NewRange(1, 10)))

	s4 := s3.Apply(
		d.Add(0, 42),
		d.Assign(2, 5),
		d.Remove(4, []int{2, 3, 4}),
	)
	if err := enc.Encode(s4); err != nil {
		panic(err)
	}
}
Copy

Run this source and you should see the domains described above.

nextmv sdk run main.go
Copy

There are many methods available on domains. Some allow you to modify them, while others help you select an individual domain from a collection of them. Take a look at the store and model Go package documentation to see what domains have to offer.

Exercises - domains

  • Create a domain on a store and a domain unattached to a store. Modify both of these domains in various ways.
  • Create multiple distinct domains on a store. Use a selector method, like Smallest or Largest to select an individual domain by index. Assign that domain a value.
  • Create a collection of domains each containing more than one value. Remove values from the domains until calling Singleton returns true.

Input and output

You are ready to start building models that actually do something. But first, you need to take a quick detour to understand how runners let you read input data into a model and output formatted JSON data to make decisions.

A runner is responsible for reading input data, setting up the solver execution environment, and writing output to a desired location. Runners make it easy to switch between different environments which may need to read data from different places or handle timeouts differently. They are the key to writing model code locally with confidence that the model with behave the same in production.

  • run.CLI: Command Line Interface runner. Useful for running from a terminal. Can read from a file or stdin and write to a file or stdout.
  • run.HTTP: HTTP runner. Useful for sending requests and receiving responses via http.
  • run.Run: Wrapper function to our Command Line Interface runner. This wrapper fixes the signature of the handler to accept store.Options and return a store.Solver. This function is useful for when you want to use any Nextmv functionality that adheres to this signature, such as Router or Nextmv's own search technology (e.g. you call store.Minimize).

We have a page going into all the details on runners, but for now, this is all you need to know.

The code below creates an empty store and passes it to a runner. Technically this is a model, though it doesn't do anything. What it does do is let you see the whole process of reading data, building a model, and solving that model.

package main

import (
	"github.com/nextmv-io/sdk/run"
	"github.com/nextmv-io/sdk/store"
)

func main() {
	err := run.Run(
		func(input any, opt store.Options) (store.Solver, error) {
			return store.New().Satisfier(opt), nil
		},
	)
	if err != nil {
		panic(err)
	}
}
Copy

Most decision models begin with a call to run.Run in a main function. run.Run requires a handler. A handler reads data of any JSON-unmarshallable type, pulls solver options out of the environment or command line, and constructs a solver. The runner unmarshals the input for you and knows what to do with the solver.

In the handler here, it is not important what type the input data is, so you can label it as any. Usually you would use something like n int or x foo, where foo can be any structure that follows Go's JSON decoding rules.

To run this empty source you have several options, as you can read from a file or stdin. Similarly, you can write to a file or to stdout.

For example, you can read the number 42 from stdin, use the Nextmv CLI to run, write to stdout, and use jq to make the output look nice.

echo 42 | nextmv sdk run main.go -- \
    -limits.duration 5s \
    -diagram.expansion.limit 1 | jq .
Copy

You can also store the number 42 in an input.json file to read from, and specify to the runner that you would like to write to a file named output.json.

nextmv sdk run main.go -- \
    -runner.input.path input.json \
    -runner.output.path output.json \
    -limits.duration 5s \
    -diagram.expansion.limit 1
Copy

The runner also accepts a wealth of command line flags and environment variables. You can see these by passing the -h flag. For example, run the binary and display all the available options.

nextmv sdk run main.go -- -h
Copy

Some flags and variables are specific to the runner. By default, the CLI runner is provided, though runners for methods like HTTP are also available. You can use command-line flags or environment variables. When using environment variables, use all caps and snake case. For example, using the command-line flag -limits.duration is equivalent to setting the environment variable LIMITS_DURATION.

You can read more about the available flags in the Nextmv CLI reference.

Note that transient fields like timestamps, duration, versions, etc. are represented with dummy values due to their dynamic nature. I.e., every time the input is run or the version is bumped, these fields will have a different value.

Exercises - input and output

  • Run the model with -h. What are the most useful flags to you?
  • Try setting the solutions output flag to all and last. How does this change the resulting JSON?

Running solvers directly

There may be situations in which a Nextmv runner is not needed, and you want to call the solver directly to process the solutions. The Solver has two methods for getting a Solution:

  • All: gets all the solutions found by the solver. Useful for understanding the progression of the search.
  • Last: gets the last (best) solution found by the solver.

Consider the same code we showed for processing input and output. We taught you how to use the CLI runner to process some input and output the -empty- solution. In the following snippets you can see how to get the solution from the solver directly (and printing it, for example), as opposed to using a runner.

package main

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/nextmv-io/sdk/store"
)

func main() {
	solver := store.New().Satisfier(store.DefaultOptions())
	all := solver.All(context.Background())

	// Loop over the channel values to get the solutions.
	solutions := make([]store.Store, len(all))
	for solution := range all {
		solutions = append(solutions, solution.Store)
	}

	// Print the solutions.
	b, err := json.Marshal(solutions)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(b))
}
Copy

Running the above code snippets, you can see an empty output being printed.

nextmv sdk run main.go
Copy

Note that the code snippets use the store.DefaultOptions which provide sensible defaults for solving.

Exercises - running solvers directly

  • Complete this tutorial and use a solver that actually does something. What happens when using All and modifying the Limits on Options to return fewer solutions?
  • Compare the last solution when using All, is it the same as the only solution returned when using Last?

Modeling

The Generate method is possibly the most powerful one, as it allows you to define the guardrails to generate new stores from existing ones. This creates a search tree that the solver uses to find the best operationally valid store.

A store is operationally valid if all decisions have been made and those decisions fulfill certain requirements; e.g.: all stops have been assigned to vehicles, all shifts are covered with the necessary personnel, all assignment have been made, quantity respects an alloted capacity, etc.

For example, you can search for all permutations of the positive integer numbers that go up to a specific number:

1 -> [
 [1]
]
2 -> [
 [1,2],
 [2,1]
]
3 -> [
 [1,2,3]
 [1,3,2]
 [2,1,3]
 [2,3,1]
 [3,1,2]
 [3,2,1]  
]
Copy

You can see that as the number increases, this search becomes non-trivial. Starting with an integer input n, define the root store and a domain of the unused integers:

	return nil, errors.New("input must be > 1")
}
Copy

You can use a slice to store the permutations as you search for them.

The store is operationally valid if all permutations have been found, i.e.: the unused domain is empty.

unused := store.NewDomain(root, model.NewRange(1, n))
Copy

For a simple output format, you can visualize the permutation slice.

permutation := store.NewSlice[int](root)

root = root.Validate(unused.Empty)
root = root.Format(
	func(s store.Store) any {
Copy

From an existing parent store, you must define the rules to generate child stores. You can do so through an Eager or Lazy generator. In the following code snippet, we show you how to lazily generate new child stores by getting the values that haven't been used for a parent store. For each unused value, we append it to the permutations and remove it from the unused domain.

		return permutation.Slice(s)
	},
)
root = root.Generate(func(s store.Store) store.Generator {
	values := unused.Slice(s)
	return store.Lazy(
		func() bool { return len(values) > 0 },
		func() store.Store {
			next := values[0]
			values = values[1:]

			return s.Apply(
				unused.Remove([]int{next}),
				permutation.Append(next),
			)
Copy

The Lazy generator will generate new stores on demand when the solver needs them. On the other hand, an Eager generator will generate all children stores upfront, consuming more memory. In the following snippet you can find the same implementation already described but eagerly generating new child stores.

		return permutation.Slice(s)
	},
)
root = root.Generate(func(s store.Store) store.Generator {
	values := unused.Slice(s)
	stores := make([]store.Store, unused.Len(s))
	for v, val := range values {
		stores[v] = s.Apply(
			unused.Remove([]int{val}),
			permutation.Append(val),
		)
	}
	return store.Eager(stores...)
})
Copy

Given that you only care about finding all permutations, it is sufficient to satisfy operational validity by summoning a Satisfier, which is a type of Solver.

When seeking to maximize or minimize a value, you can use the Value method on the store to define its value and the Maximizer or Minimizer solver, respectively.

Here are the complete sources for the Eager and Lazy variations, respectively.

package main

import (
	"errors"

	"github.com/nextmv-io/sdk/model"
	"github.com/nextmv-io/sdk/run"
	"github.com/nextmv-io/sdk/store"
)

func main() {
	err := run.Run(handler)
	if err != nil {
		panic(err)
	}
}

func handler(n int, opt store.Options) (store.Solver, error) {
	if n < 1 {
		return nil, errors.New("input must be > 1")
	}

	root := store.New()
	unused := store.NewDomain(root, model.NewRange(1, n))
	permutation := store.NewSlice[int](root)

	root = root.Validate(unused.Empty)
	root = root.Format(
		func(s store.Store) any {
			return permutation.Slice(s)
		},
	)
	root = root.Generate(func(s store.Store) store.Generator {
		values := unused.Slice(s)
		stores := make([]store.Store, unused.Len(s))
		for v, val := range values {
			stores[v] = s.Apply(
				unused.Remove([]int{val}),
				permutation.Append(val),
			)
		}
		return store.Eager(stores...)
	})

	return root.Satisfier(opt), nil
}
Copy

For n = 3, you can run the following command and one of the sources to observe the corresponding result. Please note that the input is piped via stdin and the result is saved to an output.json file. You can use jq to go into the .solutions key and look for all the .store keys.

echo 3 | \
nextmv sdk run main.go -- -runner.output.solutions all \
                            -runner.output.path output.json \
                            -limits.duration 5s \
                            -diagram.expansion.limit 1 \
                            -sense 2
cat output.json | jq ".solutions | .[].store" -c
Copy

Note that the -runner.output.solutions flag is set to all, to gather all operationally valid solutions. By default, only the last solution is shown.

In some models, it is useful to propagate constraints to child stores. For example, in the case of sudoku, if you find one cell with one value, you can remove that value as an option from the other cells in the same row, column, or square. For models requiring constraint propagation you can make use of the Propagate method, as seen on our sudoku and shift scheduling templates.

Exercises - modeling

  • Run the tutorial for different values of n, such as 4.
  • Run the tutorial without specifying the -runner.output.solutions flag. What is the last solution that the satisfier found?
  • Use Value and Minimizer to look for the permutation that has the smallest absolute distance between its numbers, i.e.: the permutation [1,3,4,2] has a distance of |3-1|+|4-3]+|2-4| = 2+1+2 = 5. On the other hand, the permutation [1,2,3,4] has a distance of 3.
  • Use Value and Maximizer to look for the permutation that has the largest absolute distance between its numbers.

🎉 🎉 🎉 🎉

You have successfully completed the tour of SDK and store! Click here for more information on the store package and visit our how-to guides for complete walkthroughs and examples of more advanced search problems.

Page last updated

Go to on-page nav menu