Skip to content

Instantly share code, notes, and snippets.

@tvoklov
Last active September 16, 2022 11:26
Show Gist options
  • Save tvoklov/c773c6ce93425cc61ef3759cebbfb9c2 to your computer and use it in GitHub Desktop.
Save tvoklov/c773c6ce93425cc61ef3759cebbfb9c2 to your computer and use it in GitHub Desktop.
nanoid generator in scala using cats effect
import cats.Monad
import cats.effect.kernel.Async
import cats.effect.std.Random
import cats.syntax.all._
def generate[F[_]: Monad](random: Int => F[List[Byte]], alphabet: Array[Char], size: Int) = {
val mask = (2 << (Math.log(alphabet.length - 1) / Math.log(2)).toInt) - 1
val step = Math.ceil(1.6 * mask * size / alphabet.length)
def go(filled: List[Char], rest: List[Byte]): F[List[Char]] =
if (filled.size == size) Monad[F].pure(filled.reverse)
else
rest match {
case Nil => random(step.toInt).flatMap(go(filled, _))
case x :: xs =>
alphabet.lift(x & mask) match {
case None => go(filled, xs)
case Some(v) => go(v :: filled, xs)
}
}
go(Nil, Nil)
}
val defaultNanoIdAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict".toCharArray
val defaultNanoIdSize = 21
def nanoid[F[_]: Async]: F[String] =
for {
r <- Random.scalaUtilRandom
id <- generate(
i => r.nextBytes(i).map(_.toList),
defaultNanoIdAlphabet,
defaultNanoIdSize
)
} yield id
import cats.effect.std.Random
import cats.effect.unsafe.implicits.global
import cats.effect.IO
import cats.effect.implicits._
import cats.syntax.all._
val justAnId = nanoid[IO].unsafeRunSync()
// with every run, this will be different
// val justAnId: List[Char] = List(d, b, a, b, c)
val `sameSeed=sameId` = {
val seed = 2
val size = 5
val alphabet = "abcdef"
val idGen =
for {
random <- Random.scalaUtilRandomSeedInt[IO](seed)
id <-
generate(
i => random.nextBytes(i).map(_.toList),
alphabet.toCharArray,
size
)
} yield id
LazyList
.continually(idGen)
.take(5)
.toList
.parUnorderedTraverse(identity)
}.unsafeRunSync()
// val `sameSeed=sameId`: List[List[Char]] =
// List(
// List(b, c, e, d, a),
// List(b, c, e, d, a),
// List(b, c, e, d, a),
// List(b, c, e, d, a),
// List(b, c, e, d, a)
// )
val `reusing-random` = {
val seed = 2
val size = 5
val alphabet = "abcdef"
// unfortunately, there seems to be no standard data type in cats effect that
// allows for truly functional randomness, i.e. where .next() functions return
// both the value AND the new random with the updated seed.
//
// if there was one, then you could have removed the F[_] constraint on the nanoid's `random` argument
for {
random <- Random.scalaUtilRandomSeedInt[IO](seed)
ids <-
LazyList
.continually(
(f: Int) =>
random
.nextBytes(f)
.map(_.toList)
)
.take(5)
.toList
.traverse(
generate(_, alphabet.toCharArray, size)
)
} yield ids
}.unsafeRunSync()
// val reusing-random: List[List[Char]] =
// List(
// List(b, c, e, d, a),
// List(a, b, c, d, f),
// List(e, c, e, d, f),
// List(b, f, b, e, e),
// List(e, a, c, e, e)
// )
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment