How To Become a Millionaire Scala Developer
Stefan Compton
Posted on April 29, 2022
Or, Simple Web API with Http4s and Cats Effect
"The EuroMillions lottery gives you a 1 in 140 million chance of not going to work tomorrow. With alcohol it's 1 in 5." - Anonymous
OK, I'm unlikely to get any "thank you" messages from Millionaires, but if you follow this post then there is a sense in which you will become a millionaire. If our physicists are right. And for a given meaning of become.
Getting All Quantum
So, the set up is this. We use a quantum random number generator, which then splits the universe in 140 million or so worlds, and...
Wait, Splits the Universe?
According to the most austere and plain reading of quantum mechanics, any quantum event which we perceive as probabilistic, is not really - all eventualities actually happen, none of them is a special real eventuality, and it's just our parochial view on the world that only permits us to see one outcome.
So given all that, we use a quantum random number generator, create all those universes and guarantee that a version of me (or you) will win the lottery.
Enough Theory, Let's Get Coding
The first thing I did was create a service for getting random numbers. The class RandomService takes a function that for a given number will generate that many random numbers. I'm using the Cats Effect IO monad to wrap the calculation, as I don't want to immediately evaluate it. Since the call will be to a front end API, and it in turn wants to call the back-end API, I don't want to run any of it explicitly synchronously.
class RandomService(randomFunction: Int => IO[Seq[Int]])
I'm generating randoms in the service, then using them to perform a Fisher-Yates shuffle. If you ask for 5 numbers from 1-50 then it will generate all numbers from 1-50 and take 5 of them.
def generateNumberSequence(numberGroup: NumberGroup): IO[Seq[Int]] = {
randomFunction(numberGroup.size).map(rands => {
val result = shuffle(1 to numberGroup.size, rands)
.take(numberGroup.count)
if (numberGroup.isFullList) result else result.sorted
})
}
@tailrec
final def shuffle(values: Seq[Int], randoms: Seq[Int], index: Int = 0): Seq[Int] = {
val n = values.size
if (index == n - 1) values
else shuffle(swap(values, index, index + (randoms(index) % (n - index))), randoms, index + 1)
}
@tailrec
final def swap(seq: Seq[Int], i: Int, j: Int): Seq[Int] = {
if (i == j) seq
else if (j < i) swap(seq, j, i)
else seq.take(i) ++ Seq(seq(j)) ++ seq.slice(i + 1, j) ++ Seq(seq(i)) ++ seq.drop(j + 1)
}
the NumberGroup is just a data type to carry the magnitude of numbers requested and how many of them. Note that we sort the numbers you request except when the group is full - if you request all numbers from 1-50 then the order is important, you don't just want 1,2,3 ... 50!
The other interesting part is the web service Lotto
trait Lotto {
def generate(vals: Seq[NumberGroup]): IO[Lotto.Response]
}
object Lotto {
final case class Response(nums: Seq[(Int, Seq[Int])])
val randomService = new RandomService(QRNG.f)
object Response {
implicit val lottoEncoder: Encoder[Response] = (resp: Response) =>
Json.obj(
("Your Numbers:",
Json.fromFields(resp.nums.map(t => (t._1.toString, t._2.asJson)))
)
)
}
def impl: Lotto = (vals: Seq[NumberGroup]) => {
val resp = vals.map(randomService.generateNumberSequence).sequence
resp.map(r => Response(r.zipWithIndex.map(t => (t._2, t._1))))
}
I love the straightforward declarative nature of the code. Also how concise it is. Most of the complexity is in getting the response to look OK. It still needs some work though!
Notice also the .sequence
method that takes a sequence of our IO monads and sequences them into one IO. Conceptually it's reorganizing the result without having to perform the computation. Nice!
All that's left is to run our Http4s server, which couldn't be simpler.
def run: IO[Nothing] = {
val lottoAlg = Lotto.impl
val httpApp = QuantumMillionaireRoutes.lottoRoutes(lottoAlg).orNotFound
val finalHttpApp = Logger.httpApp(logHeaders = true, logBody = true)(httpApp)
EmberServerBuilder.default[IO]
.withHost(ipv4"0.0.0.0")
.withPort(port"8080")
.withHttpApp(finalHttpApp)
.build
}.useForever
And there it is. All the code is on my GitHub here
And when you do become a Millionaire, please let that version of me know - if I know him, he'll put it on his CV ...
Posted on April 29, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024