DEV Community

Diogo Kollross
Diogo Kollross

Posted on

Random strings in Elixir

Random integers

Elixir doesn't have a module to generate random integers, but it can call underlying Erlang modules such as rand (nowadays, preferred over random) or crypto and can select random items from enumerables. This gives us some options to generate integers inside a range.

To generate integers between 0 and 255 (inclusive):

n = :rand.uniform(256) - 1
n = :crypto.rand_uniform(0, 256)
n = Enum.random(0..255)
Enter fullscreen mode Exit fullscreen mode

Note that :rand.uniform(n) returns integers so that 1 <= x <= n - that's why we decrement the result. Also, the documentation for Enum.random states that it will efficiently "pick a random value between the range limits, without traversing the whole range".

You can also use Enum.random with ranges or charlists to pick random code points from specific alphabets.

n = Enum.random(?a..?z)
n = Enum.random('0123456789abcdef')
Enter fullscreen mode Exit fullscreen mode

Random strings

The next step is to use these building blocks to create a binary from a number of random code points using comprehensions:

s = for _ <- 1..10, into: "", do: <<Enum.random('0123456789abcdef')>>
Enter fullscreen mode Exit fullscreen mode

Cryptographically secure strings

Sometimes there's a need for a cryptographically secure generator to create, for example, random passwords or authentication tokens.

If you simply need a sequence of random bytes, you can use strong_random_bytes from Erlang's crypto module. It's also much faster than looping in Elixir using comprehensions.

s = :crypto.strong_rand_bytes(10)
Enter fullscreen mode Exit fullscreen mode

If you need to use a specific alphabet, you can combine :crypto.rand_uniform with Enum.at:

symbols = '0123456789abcdef'
symbol_count = Enum.count(symbols)
s = for _ <- 1..10, into: "", do: <<Enum.at(symbols, :crypto.rand_uniform(0, symbol_count))>>
Enter fullscreen mode Exit fullscreen mode

Benchmark

I've benchmarked these options using Benchee.

random_length = 10_000
Benchee.run(%{
  "Enum.random comprehension" => fn ->
    for _ <- 1..random_length, into: "", do: <<Enum.random(0..255)>>
  end,
  ":rand.uniform comprehension" => fn ->
    for _ <- 1..random_length, into: "", do: <<:rand.uniform(256) - 1>>
  end,
  ":crypto.rand_uniform comprehension" => fn ->
    for _ <- 1..random_length, into: "", do: <<:crypto.rand_uniform(0, 256)>>
  end,
  ":crypto.strong_rand_bytes" => fn ->
    :crypto.strong_rand_bytes(random_length)
  end,
})
Enter fullscreen mode Exit fullscreen mode

Which yielded these results:

Name                                         ips        average  deviation         median         99th %
:crypto.strong_rand_bytes               73695.16      0.0136 ms    ±34.33%      0.0130 ms      0.0239 ms
:rand.uniform comprehension               488.88        2.05 ms    ±10.39%        1.99 ms        2.62 ms
Enum.random comprehension                 284.56        3.51 ms     ±8.22%        3.53 ms        4.33 ms
:crypto.rand_uniform comprehension         44.04       22.71 ms     ±2.16%       22.64 ms       24.54 ms

Comparison: 
:crypto.strong_rand_bytes               73695.16
:rand.uniform comprehension               488.88 - 150.74x slower +2.03 ms
Enum.random comprehension                 284.56 - 258.98x slower +3.50 ms
:crypto.rand_uniform comprehension         44.04 - 1673.55x slower +22.70 ms
Enter fullscreen mode Exit fullscreen mode

Summary

Based on the benchmarks and thinking about readability, this is what I would use depending on what I need to generate:

  • Binary strings: :crypto.strong_rand_bytes
  • Any other symbol alphabet: Enum.random
  • Passwords/tokens: :crypto.rand_uniform
  • Fast: :rand.uniform

Discussion (0)