Why is my random shuffle always producing the same result when using Suiji?

I’m really stuck with this. I’d like to have a function that will randomize some options, but no matter how I go about it the randomization from suiji doesn’t seem to work quite randomly. To be more specific, all of the calls to the function result in the same randomization. What is expected is that each call to the function will have a different result.

How can I have a different random list each call to the function?

#import "@preview/suiji:0.4.0": *

#let rng = gen-rng(1)

#let randomize(options) = {
  (_ , options) = shuffle(rng, options)
  options.join(", ")
}

#let options = ("A", "B", "C", "D")
  
#randomize(options)

#randomize(options)

#randomize(options)

#randomize(options)

#randomize(options)

Results in

D, B, C, A (repeated five times)

Because each function in Typst is purely functional, there can’t be any state internal to the function (it can’t remember anything). This means we need to give different arguments if we want different outputs (in this case different orderings).
Each time Suiji generates a random number it takes the rng value and also returns a new value that needs to be stored. Then that new value is passed into the next function to use randomness.

You code becomes:

#import "@preview/suiji:0.4.0": *

#let rng = gen-rng(1)

#let randomize(options-arg, rng-arg) = {
  let (rng-local , options-local) = shuffle(rng-arg, options-arg)
  (rng-local, options-local.join(", "))
}

#let options = ("A", "B", "C", "D")

#let (rng, output) = randomize(options, rng)
#output

#let (rng, output) = randomize(options, rng)
#output

#let (rng, output) = randomize(options, rng)
#output

#let (rng, output) = randomize(options, rng)
#output

#let (rng, output) = randomize(options, rng)
#output

Results in:

D, B, C, A
C, B, D, A
C, D, B, A
B, C, A, D
D, A, C, B
3 Likes

This is a good explanation, thank you!

My use case for this is for a package, and a user wouldn’t be able to manage handling rng constantly. I’ve tried to deal with this using state but if there are a number of calls to the function then it doesn’t resolve after a while (resulting in repeats).

Do you happen to know if there is some way to update state?

Here is what I tried (I realize the nested expression isn’t necessary, but you get the idea):

#import "@preview/suiji:0.4.0": *

#let rng = state("rng", gen-rng(1))

#let randomize(options) = {
  context {
    let _rng = none
    let _options = none
    (_rng, _options) = shuffle(rng.get(), options)
    rng.update(_rng)
    _options.join(",")
  }
}

#let options = ("A", "B", "C", "D")
  
#randomize(options)

#randomize(options)

#randomize(options)

#randomize(options)

#randomize(options)

#randomize(options)

#randomize(options)

I remember reading recently in an answer on the forum that using both get() and update() on a state in one context is not ideal. I tried searching for the post that I am vaguely remembering but didn’t find it.

I see in the state page of the documents that update() can be given a function that:

receives the previous state and has to return the new state.

After some trial-and-error I ended up with the following. It stores the most recent ordering of options in the state with rng (as an array).

#import "@preview/suiji:0.4.0": *

#let options = ("A", "B", "C", "D")
#let rng-and-options = state("rng", (gen-rng(1), options))

#let randomize(rng-and-options-local) = context {
  rng-and-options-local.update((current) => {
    let (rng-cur, options-cur) = current
    let (rng-new, options-new) = shuffle(rng-cur, options-cur)
  
    let new-state = (rng-new, options-new)
    return new-state
  })
  rng-and-options-local.get().at(1).join(", ")
}

#for _ in range(10) [
  #randomize(rng-and-options)
  #linebreak()
]

Because the state is a parameter of the function, you can create a second set of options (and state):

#let options2 = ("A", "B", "C", "D", "E")
#let rng-and-options2 = state("rng", (gen-rng(1), options2))

And call the randomize() function with either of the states.

1 Like

I think what @gezepi recommended is fundamentally the only way to do it usefully.

The problem you have with state is layout convergence, see this post for details: Why is State Final not "final"? In that thread I suggest a solution that, applied to your situation, would look roughly like this:

#let rng = state("rng", (gen-rng(1), none))

#let randomize(options) = {
  rng.update(((rng, _)) => shuffle(rng, options))
  context rng.get().last()
}

The rng state now contains the actual rng and the most recent result. The update is not done through a get…update cascade, so that’s good!

However, this function doesn’t give you an array; it gives you content: Why is the value I receive from context always content? - #2 by laurmaedje The typical way around this is to pull the context keyword out:

#let rng = state("rng", (gen-rng(1), none))

#let randomize(options) = {
  rng.update(((rng, _)) => shuffle(rng, options))
  rng.get().last()
}

...

#context randomize(options)

But in this case that also doesn’t work: randomize() uses update(), and an update itself is content. So unless you really only need to output the randomized array (in which case content is fine), that’s also not a full solution. If that’s all you need, @gezepi’s second post (or my first snippet) gives you an answer.

Assuming you want to use the randomized array further down, you’ll need to separate the update from the result, e.g. like this:

#let randomize(options) = {
  rng.update(((rng, _)) => shuffle(rng, options))
}
#let get-randomized() = {
  rng.get().last()
}

...

#randomize(options)
#context get-randomized()

… but imo then you’re basically in the same situation as at the start, where you had to handle the rng variable (instead of the state update) separately on each call.

1 Like

Actually I just had one more idea that I’ll put separately; using callbacks:

#let rng = state("rng", (gen-rng(1), none))

#let randomize(options, callback) = {
  rng.update(((rng, _)) => shuffle(rng, options))
  context callback(rng.get().last())
}

#let options = ("A", "B", "C", "D")

#randomize(options, shuffled => [#shuffled])

#randomize(options, shuffled => [#shuffled])

This lets you make a single randomize call and hides the random handling, but it requires you to put everything that depends on the random result in a callback. This probably becomes annoying when you need multiple random things at the same time (meaning you have nested callbacks).

2 Likes

Wow. There’s a lot here, and I know its a complicated problem because of the limitations of state.
I’ll take a look and try to flesh out some of your suggestions to see if they’ll work with what I have in mind.

I can’t say thank you enough!

1 Like

Note that there’s a slight difference between our suggestions, @gezepi. You have a context around the whole function, while I only wrap get(). That makes a difference! Context is frozen when a context expression starts, so your get() will actually retrieve the value before randomizing. Note how the first line you print contains “A, B, C, D” and not something random.

1 Like

Here you go: How to keep exercise and solution together in source, but render them separately? - #3 by Andrew.

1 Like

After hashing this out a bit with my use case, I’m going to mark this as a solution.

There are a few other caveats:

  1. Nesting these will result in non-convergence.
  2. If there are multiple steps or multiple arrays that need to be shuffled/etc, it is possible. It seems the best way is to have a nested function that does all of the randomization necessary step by step. I have one use case that takes two arrays of options, both arrays have to go through suiji #choice, then combined together for a shuffle. Doing those steps all together and keeping the intermittent arrays stored in state works. An alternative is to store a dictionary as the state and reference the keys later post callback.

So, it seems to work. I have yet to modify my project but it looks like it will work out. This does make for a different structure/approach for code (i.e. you basically have to make the randomization the last thing and so everything else should be done first).