Random strings in Elixir
Tags: elixir
Published: 2021-03-13 23:12:01

# Random integers

Elixir doesn't have a module to generate random integers, but it can call underlying Erlang modules such as [rand](https://erlang.org/doc/man/rand.html) (nowadays, preferred over [random](https://erlang.org/doc/man/random.html)) or [crypto](https://erlang.org/doc/man/crypto.html) 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):

```elixir
n = :rand.uniform(256) - 1
n = :crypto.rand_uniform(0, 256)
n = Enum.random(0..255)
```

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](https://hexdocs.pm/elixir/Enum.html#random/1) 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.

```elixir
n = Enum.random(?a..?z)
n = Enum.random('0123456789abcdef')
```

# Random strings

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

```elixir
s = for _ <- 1..10, into: "", do: <<Enum.random('0123456789abcdef')>>
```

# 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](https://erlang.org/doc/man/crypto.html#strong_rand_bytes-1) from Erlang's `crypto` module. It's also much faster than looping in Elixir using comprehensions.

```elixir
s = :crypto.strong_rand_bytes(10)
```

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

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

# Benchmark

I've benchmarked these options using [Benchee](https://github.com/bencheeorg/benchee). 

```elixir
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,
})
```

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
```

# 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`