Gleam ULID

Universally Unique Lexicographically Sortable Identifier implementation in Gleam.

What’s a ULID? Some say it’s a better UUID. In a string form it is shorter (26 characters vs. 32) and sortable.

Caveats

  1. Only Erlang build target is supported (at the moment)

Package Version Hex Docs

gleam add gulid

Basic Use

import gulid.{ new_as_string, new, new_monotonic, from_string_function,
               to_string_function }

pub fn main() {
  // Quick and dirty ULID string - performance implications
  let ulid_str = new_as_string()
  io.println("ULID is " <> ulid_str)

  // Convert many ULIDs to String with a generator function
  let to_string = to_string_function()
  // `to_string_function` returns a function that then can be used to
  // convert `Ulid` values to `String`. The reason this is done this way is
  // that Gleam doesn't have a way to define module scoped global constants
  // that could use function calls to initialize. Nor does it have module scoped
  // `let`. Therefore, the only way to have a `private` reused value is to
  // have it as a capture. So, there is a `let` in `to_string_function`, which
  // binds an Erlang array with ULID character encodings, captured in returned
  // function.
  let bunch_of_ulids = list.map([new(), new(), new(), new(), new()], to_string)
  io.println("A bunch of ULIDs:")
  echo bunch_of_ulids

  // Parse ULIDs from string
  let from_string = from_string_function()
  // Quick and dirty
  let assert Ok(ulid) = from_string("01J9HSAQG7YR6Z16SS7ZTH26WQ")
  io.println("Quick and dirty parsed ULID is")
  echo ulid

  // More proper
  io.println("A more properly parsed ULID is")
  case from_string("01J9HS6WA9ZBA045WTNYWAGPM5") {
    Ok(ulid) -> {
      echo ulid
      Nil
    }
    Error(InvalidLength(error)) | Error(DecodeError(error)) ->
      io.println("Oh, noes: " <> error)
  }

  // Monotonic ULIDs:
  // 1. Generate initial `Ulid` with `new()`
  // 2. Then use `new_monotonic(Ulid)` with initial and subsequently generated
  list.range(0, 9) // We want 10 monotonic ULIDs
  |> list.scan(new(), fn(ulid, _) { new_monotonic(ulid) })
  // Convert'em to strings
  |> list.map(to_string_function())
  |> echo
}

Advanced Use

In case one needs to create or outpur ULIDs out of

import gleam/erlang
import gleam/int
import gleam/io
import gleam/list
import gleam/result
import gleam/string
import gleam/bool
import gulid.{from_parts, from_tuple, to_parts, from_bitarray, to_bitarray}

pub fn main() {
  let ulid =
    from_parts(
      erlang.system_time(erlang.Millisecond),
      int.random(99_999_999_999),
    )
  io.println("My ulid made from spare parts:")
  echo ulid

  let #(timestamp, random) = to_parts(ulid)
  io.println("Now, extracted its constituent timestamp and random:")
  io.println(
    "\tTime: "
    <> { system_time_to_rfc3339(timestamp / 1000) |> from_codepoints },
  )
  // echo system_time_to_rfc3339(timestamp / 1000)
  io.println("\tRandom: " <> int.to_string(random))

  let same_ulid = from_tuple(#(timestamp, random))

  io.println("Same ulids? " <> { bool.to_string(same_ulid == ulid) })

  // Ulid from a binary
  let assert Ok(bin_ulid) =
    <<
      erlang.system_time(erlang.Millisecond):big-48,
      int.random(9_999_999_999):big-80,
    >>
    |> from_bitarray
  io.println("Ulid from a bitarray: ")
  echo bin_ulid

  // Ulid to a bitarray
  io.println("Ulid to binary: ")
  bin_ulid
  |> to_bitarray
  |> echo
}

@external(erlang, "calendar", "system_time_to_rfc3339")
fn system_time_to_rfc3339(seconds_since_epoch: Int) -> List(Int)

fn from_codepoints(code_points: List(Int)) -> String {
  code_points
  |> list.map(string.utf_codepoint)
  |> list.map(fn(res) {
    let assert Ok(default_codepoint) = string.utf_codepoint(97)
    result.unwrap(res, default_codepoint)
  })
  |> string.from_utf_codepoints
}

Further documentation can be found at https://hexdocs.pm/gulid.

Examples

Change directory to examples then:

gleam run -m example1   # Run the example one
gleam run -m example2   # Run the example two

Development

gleam test  # Run the tests
Search Document