Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No mechanism to use seeded random generation #3289

Closed
hugoferreira opened this issue Jul 31, 2017 · 23 comments
Closed

No mechanism to use seeded random generation #3289

hugoferreira opened this issue Jul 31, 2017 · 23 comments

Comments

@hugoferreira
Copy link

Currently, methods that expose random behaviour, like sample, use the internal Math.random() function, which is unseeded. Not being able to optionally enforce deterministic behaviour in a functional setting is... odd. I would like to suggest the discussion over possible enhancements and alternatives to implement such feature, amongst which:

  • Change all internal function call of Math.random() to a lodash random number generator;
  • Make this random number generator a... generator function;
  • Support passing an extra parameter in functions like sample to set the seed;
  • Support an optional callback in functions like sample that provides a generator;
  • Get an objective measurement on the performance impact of such changes, and aim for a zero-cost abstraction so we don't end up with angry people.
@jdalton
Copy link
Member

jdalton commented Jul 31, 2017

Hi @hugoferreira!

If you're wanting that level of control I think you'd be better off with a mixin or specialized library for randomizing things.

@jdalton jdalton closed this as completed Jul 31, 2017
@carlsmith
Copy link

carlsmith commented Oct 22, 2017

Is this open to change? It's not the end of the world if not, but I think seeded random number generators would be a nice addition to Lodash too, especially as it already has an unseeded RNG.

There's a library called seedrandom by David Bau, which is mature and includes a collection of JS RNG implementations.

The basic idea with seedrandom is you have a function you can invoke with no args to get a random float between zero and one (quickly), or you can pass the function a seed and get back a function that generates random numbers based on that seed (quickly).

The Lodash random API is totally different from seedrandom, so you would still have to figure out how to make it all fit together. I'm not putting myself forward to do that, and think the issue here on GitHub should stay closed for now, but could we please get an indication of whether you're open to exploring adding the feature if someone did volunteer?

@jdalton
Copy link
Member

jdalton commented Oct 23, 2017

Hi @carlsmith!

For folks who want this I'm happy to punt to another specialized package.

@carlsmith
Copy link

Cool. Thanks for considering it.

@yoiang
Copy link

yoiang commented Feb 3, 2018

This would be super useful for me and I'm happy to make the PR.

There are two architectures that come to mind:

  1. Add a seedRandom function to the library that accepts a seed and returns a random number generator function stand-in for Math.random, which is then passed in as an optional parameter to any other function in the library that supports random generation.

  2. Have all other functions in the library that support random generation point to a library global variable, first initialized with Math.random. Add a seedRandom that overwrites this new global variable with the resulting generated function, causing all functions in the library that support random generation to call this new, seeded function.

Let me know if any other design makes sense to you all!

@yoiang
Copy link

yoiang commented Feb 15, 2018

The tests folder appears to be missing from master, should I wait until the next version of lodash?

@falsyvalues
Copy link
Contributor

@yoiang The master branch is in flux while we work on Lodash v5 and thats why test are missing right now, take a look at CONTRIBUTING.

@yoiang
Copy link

yoiang commented Feb 16, 2018

@falsyvalues yah, I'd read! :) So should I wait for v5?

@joe-hilling
Copy link

@yoiang I could really use this feature for testing application methods that rely heavily on lodash. The second implementation would make much more sense in this scenario as I don't want to have to pass an additional parameter throughout my application source just for the purposes of testing.

@stefanuddenberg
Copy link

The easiest way for me to get my desired behavior (in my main JS file of interest) was as follows:

/* Top of script */
Math.seedrandom("my-random-seed");
_ = _.runInContext();
/* Rest of script */

@markstos
Copy link
Contributor

markstos commented Mar 29, 2019

For those that don't know what what this, seedrandom is one library provides seedable random number generators: https://www.npmjs.com/package/seedrandom

As that project documents, Math.sendrandom() may not be what you want, as it changes the seed globally, possibly causing random sequences to become predictable in places you didn't intend.

Having a seed option for Lodash random would save adding a new dependency, but I realize this is a less common use of _.random and understand if there's not interest in adding in here.

In my case, I want a bunch of different customer jobs to run at random-but-stable times to spread the load out. By generating a random number with the customer name as a seed, all the customer names are converted into random-but-stable numbers based on the customer name. The stability is nice, so if see there's a slow job that happening every day at 5:23 AM, I can track down that the job belongs to "customer X".

@rriemann
Copy link

@markstos , why do not you use a hash function in your case to map customers to evenly distributed numbers?

@markstos
Copy link
Contributor

@rriemann That would work, too. The Jenkins project uses the hashing approach to allow you to run jobs "every 30 minutes based on a hash of the project name".

@breck7
Copy link
Contributor

breck7 commented May 30, 2019

I would also very much want a seed for lodash random, shuffle, etc. I want it for testing and reproducibility. Especially now that data science is becoming a more popular use case in Javascript (my use case). I was very surprised seed is not a parameter to the random number methods. I think I would rather have no random methods at all in lodash, than to have them w/o seeds.

@hugoferreira
Copy link
Author

hugoferreira commented May 31, 2019

I think I would rather have no random methods at all in lodash, than to have them w/o seeds.

I sympathize with your opinion, in the sense that people look for libraries such as lodash because they prefer coding in a functional setting (i.e. no side-effects). The predictability of a random number generator is simply part of this mindset :)

However, I think the main issue why @jdalton regards such functionality as out-of-scope is because JS/ES does not have a built-in seedable PRNG. Either lodash accepts having a new dependency (meh), or it accepts the responsibility of dealing with a custom PRNG (also meh). A third option (similar to what @yoiang is proposing, and which I believe solves the current conundrum), would be for lodash to simply accept custom PRNGs as an optional argument (or in an overridable global reference) in random-dependent functions.

The best possible scenario would be for JS/ES to provide a standard way of seeding its PRNG programmatically, but I don't believe this is even on their horizon. Node supports the --random-seed=xxx flag; it's highly sub-optimal, but it may be an option for some of you to consider in the meantime.

@breck7
Copy link
Contributor

breck7 commented May 31, 2019

simply accept custom PRNGs as an optional argument

I agree. This seems like the best design. And then from there it would be easy to add a very rudimentary PRNG with seeding

@geluso
Copy link

geluso commented Mar 30, 2020

I am using seedrandom and lodash together. I made sure to include seedrandom before lodash when I manually include my script tags and I initialize the seed value too. It works like a charm!

  <!-- Specifically include seedrandom before lodash so lodash uses the seededd ranom -->
  <script src="thirdjs/seedrandom.js"></script>

  <script>
    Math.seedrandom("dev")
  </script>

  <script src="thirdjs/lodash.js"></script>

  <script>
    console.log('run this again to see _.sample chooses the same number every time')   
    console.log('seed', _.sample([1, 2, 3, 4, 5, 6, 7, 8, 9]);
  </script>

@yoiang
Copy link

yoiang commented Mar 30, 2020

@geluso @stefanuddenberg suggested this functionality above.

While it works just great it does require intimate knowledge of what lodash uses internally, should lodash ever change the internal behavior of this technique would fail without notice.

On the other hand APIs and interfaces are characterized as defining interactions between systems. Capturing this requirement as a method or in a signature sets up expectations, is commonly tracked by semantic versioning, can have tools report changes, etc.

@geluso
Copy link

geluso commented Mar 30, 2020

Ah, I see. I must have scrolled through @stefanuddenberg's response too. Thanks for pointing that out.

@alexamy
Copy link

alexamy commented Oct 12, 2021

For testing purposes you can use importing module with seedrandom call as first import in your test file, as described in this article by Pete Corey.

seed.js:

import seedrandom from "seedrandom";

seedrandom("seed", { global: true });

test.js:

import "./seed.js";
import _ from "lodash";

_.shuffle([1, 2, 3]); // Produces deterministic shufflings!

@bfintal
Copy link

bfintal commented Jul 3, 2022

The code provided by @alexamy didn't work for me.

To use seeded random generation for Lodash, I needed to do this:

import seedrandom from "seedrandom";
import _lodash from "lodash"; // Temporary name for Lodash

seedrandom("seed", { global: true });
const _ = _lodash.runInContext();

_.shuffle([1, 2, 3]); // Always the same

I read about this here: davidbau/seedrandom#32 (comment)

@benwinding
Copy link

Here's a handy typescript function, based on the smart people commenting above 👌

// shuffleSeeded.ts
import seedrandom from 'seedrandom';
import _lodash from 'lodash';

export function shuffleSeeded<T>(array: T[], seed: string): T[] {
  seedrandom(seed, { global: true });
  const _ = _lodash.runInContext();
  return _.shuffle(array);
}

@goastler
Copy link

@benwinding 's answer above has a flaw in setting the seed every call, e.g.

shuffleSeeded(myarray, 0)
// (A) do some other work using Math.random
shuffleSeeded(myarray, 1)
// (B) do more work using Math.random

At A the global seed has begun at 0 then set to 1 for B onwards. This is an undesirable side effect as any work in A or B that uses Math.random() will inadvertently get affected by the shuffleSeeded call changing the seed.

Instead, I propose the following:


// create a new lodash instance with the given seed
export const seedLodash = (seed: number | string) => {
    // take a snapshot of the current Math.random() fn
    const orig = Math.random
    // replace Math.random with the seeded random
    seedrandom(seed, { global: true })
    // runInContext() creates a new lodash instance using the seeded Math.random()
    // the context is a snapshot of the state of the global javascript environment, i.e. Math.random() updated to the seedrandom instance
    const lodash = _lodash.runInContext()
    // restore the original Math.random() fn
    Math.random = orig
    // return the lodash instance with the seeded Math.random()
    return lodash
}

This creates a lodash instance which uses a seedrandom rng under the hood, so only lodash has been seeded and not Math.random. In the case where you want both Math.random and lodash to be seeded, use the following once upon program initalisation:

seedrandom(seed, { global: true });
const _ = _lodash.runInContext();
// use lodash and Math.random as normal...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests