Reputation: 5500
I'm playing around with Cats and Free Monads and I've written a toy REST Service algebra and a "program" called ensureOneProduct
. Unfortunately ensureOneProduct
has more boiler plate code than I'd like to see. Is there a better way to write the ensureOneProduct
method below? Or have I just been spoiled by Haskell's do notation? Thanks!
import cats.free.Free
import cats.free.Free.liftF
import cats.{Id, ~>}
object Algebra3 {
type Url = String
/**
* The REST Service Algebra
*/
sealed trait Service[+A]
case class Get[T](url: Url) extends Service[Option[T]]
case class Put[T](url: Url, rep: T) extends Service[T]
case class Post[T](url: Url, rep: T) extends Service[Option[Url]]
case class Delete(url: Url) extends Service[Unit]
// A Free REST Service
type ServiceF[A] = Free[Service, A]
// The Product resource
case class Product(name: String, quantity: Int)
/**
* Bad example of REST but I'm focusing on learning about Free Monads.
*/
def ensureOneProduct[T](url: Url, rep: T): ServiceF[Url] = {
for {
// Attempt to retrieve the product...
res <- get[Product](url)
_ <- if (res.isDefined)
for {
// The product existed so delete it.
_ <- delete(url)
// Now create the product
_ <- put(url, rep)
} yield ()
else {
// The product did not exist so create it.
put(url, rep)
}
} yield url
}
def get[T](url: Url): ServiceF[Option[T]] = liftF[Service, Option[T]](Get[T](url))
def put[T](url: Url, rep: T): ServiceF[T] = liftF[Service, T](Put[T](url, rep))
def post[T](url: Url, value: T): ServiceF[Option[Url]] = liftF[Service, Option[Url]](Post[T](url, value))
def delete(key: String): ServiceF[Unit] = liftF(Delete(key))
def defaultCompiler: Service ~> Id =
new (Service ~> Id) {
def apply[A](fa: Service[A]): Id[A] =
fa match {
case Get(key) =>
println(s"GET($key)")
Some(new Product("Hat", 3))
case Put(key, rep) =>
println(s"PUT($key, $rep)")
rep
case Post(url, rep) =>
println(s"POST($url)")
Some(url)
case Delete(key) =>
println(s"DELETE($key)")
()
}
}
def main(args: Array[String]) = {
val url = "https://www.example.com/api/v1/hats/1024"
val product = new Product("Hat", 1)
println(ensureOneProduct(url, product).foldMap(defaultCompiler))
}
}
This code prints:
GET(https://www.example.com/api/v1/hats/1024)
DELETE(https://www.example.com/api/v1/hats/1024)
PUT(https://www.example.com/api/v1/hats/1024, Product(Hat,1))
https://www.example.com/api/v1/hats/1024
It was interesting and a little concerning that when I forgot to enclose the nested delete
and put
calls in a for expression, it compiled but did not run the delete
operation. It makes sense why the delete
call was omitted but I would prefer to get some sort of compile time feedback.
Upvotes: 1
Views: 186
Reputation: 7768
You might be able to scrap the inner for comprehension using the followed by (>>
) operator:
for {
res <- get[Product](url) // Attempt to retrieve the product...
_ <- if (res.isDefined)
delete(url) >> put(url, rep) // It exists: delete & recreate it
else
put(url, rep) // It does not exist: create it
} yield url
Upvotes: 3