Finch vs Http4s, which is FPer
This is purely personal overall comparation from Functional Programming point of view, which only consider composible, extensible, and joy of FP. Performance and eco system are out of the scope.
The explicit version that I'm comparing here is finch 0.26.1 and Http4s 0.20-M4
Definition:
- composible: how easy to build blocks from existing components
- extensible: how easy to add support for new type
- joy of FP: how many FP knowledges are actually practiced
Scale from 1 to 5:
Finch
composible: ð ð ð ð
finch takes full advantage of shapeless' product and coproduct to routes Matcher
val getApples = get("apple") { Ok("🍎🍎")}
val getApple = get("apple" :: path[Int]) { (id: Int) => Ok("🍎")}
val routes = getApples :+: getApple
::
here is a product, means the path will match if there is an "apple"
and an Int in the path path[Int]
:+:
constructs a coproduct, which means either getApples or getApple matched route will match
About Middleware, finch need to mix finagle filter to do the job. so it will end up very ugly and inconsistent from the routes above
class AuthFilter(implicit ttf: ToTwitterFuture[ProgramF]) extends SimpleFilter[Request, Response] {
def apply(req: Request, service: Service[Request, Response]): Future[Response] = {
(req.path, req.headerMap.get("TOKEN")) match {
case ("/login", _) => service(req)
case (url, Some(token)) => ??? // fetch user info and pass to service
and then compose filter and service
new AuthFilter andThen routes.toServiceAs[Application.Json]
you may have notice that the finangle filter is more lower level verbose code and inconsistent with the previous shapeless DSL.
extensible: ð ð
finch has a lot of internal custom types, e.g. the magic happen behind the DSL above:
val getApples = get("apple") { Ok("🍎🍎")}
is:
- string "apple" will implicitly convert into
Endpoint[Unit]
get("apple")
will covertEndpoint[Unit]
into functionMapper[F, In] => Endpoint[F, Out]
basically just Cont
, taking Mapper as parameter and wrapping itself.
- Another implicit conversion will take place in
Ok("")
, fromOutput[String] => Mapper[F, String]
If you didn't follow, that's fine, because there is so many implicit conversion and internal custom types took place here.
But think about it, because all implicit conversion was well-defined in finch internally we've restricted to the types they provided.
For example my controller will actually return me a Free Monad i.e. Free[Program, Output[String]]
How can I get it to work with the router?
val getApples = get("apple") { Free[Program, Output[String]].pure(Ok("🍎🍎"))}
it won't compile because the implicit conversion expecting an Output[_]
type
actually it only supports IO[_]
, Output[_]
, Response
UPDATE: from 0.27.0 it supports custom converter to convert any user custom type to IO
implicit val conv = new ToEffect[Free[Program, ?], IO] {
def apply[A](a: Free[Program, A]): IO[A] = ???
}
val getApples = get("apple") { Free[Program, Output[String]].pure(Ok("🍎🍎"))}
But still, it's so many implicits magic happen behind and the journey wasn't so fun.
joy of FP: ð ð ð
except some funs from shapeless there aren't actually any cats interop during composition of routes
While that could be much more attractive to beginner friendly though, since user don't have to have any knowledge about cats before using finch.
Http4s
composible: ð ð ð ð ð
meanwhile, http4s takes advantage of cats to achieve path Matcher
val getApples = HttpRoutes.of[IO] {
case GET -> Root / "apple" => Ok("🍎🍎")
}
val getApple = HttpRoutes.of[IO] {
case GET -> Root / "apple" / id => Ok("🍎")
}
val routes = getApples <+> getApple
what happen here is HttpRoutes.of[IO] {...}
return a data type Kleisli{OptionT{F, ?}, Request{F}, F{Response{F}}}
<+>
is combindK of SemigroupK[Kleisli[OptionT[F, ?], Request[F], ?]]
You may already realize that nothing of above make any sense to you if you aren't familiar with data types and typeclasses defined in cats or scalaz.
It requires some knowledge background from cats, just like you should know some sort of shapeless to fully understand what the hell is ::
and :+:
about.
Short story, Kleisli is a generic data type representing a function A => F[B]
so you could imagine that Kleisli[OptionT[F, ?], Request[F], F[Response[F]]]
is just something like
Request[F] => OptionT[F, F[Response[F]]]
while <+>
is very like :+:
, it combines these Kleisli, but any of these match, it will
return that matched Kleisli
About Middleware, it's nothing more just compose a Kleisli of type Kleisli[Option[F,?], Request[F], F[Request[F]]]
before
or compose a Kleisli of type Kleisli[Option[F,?], Response[F], F[Response[F]]]
after
e.g. auth user before getApple
def auth = Kleisli { req: Request[IO] =>
findUserInDatabase(req) match {
case true => OptionT(IO(Some(req)))
case false => OptionT.fromOption[IO](None)
}
}
auth andThen getApple
extensible: ð ð ð ð ð
Since route matcher is simply just Kleisli, extending http4s to support types other than F[Response[F]]
will
be much simpler.
For the same example as above in finch, that my controller will return a free program Free[Program, IO[Response[IO]]]
instead of IO[Response[IO]]
// type alias for route that return free monad
type FreeRoute[F[_]] =
Kleisli[OptionT[F, ?], Request[F], ProgramF[F[Response[F]]]]
// custom dsl
def route[F[_]: Monad](
pf: PartialFunction[Request[F], ProgramF[F[Response[F]]]]): FreeRoute[F] =
Kleisli(
(req: Request[F]) => OptionT(implicitly[Monad[F]].pure(pf.lift(req))))
val getApples = route {
case GET -> Root / "apple" => Ok("🍎🍎").pure[Free[Program,?]]
}
since getApples is still Kleisli, all the methods such as <+>
are still available
to hook it back to http4s route, simply map interpreter to getApples
val interp: Program ~> IO = ???
val router = getApples flatMapF interp
here I used flatMapF because interp will give it another IO
, which need to be flatten
joy of FP: ð ð ð ð ð
Since Http4s takes full power of data type Kleisli, since Kleisli at the end is just a function, it's much more composable and extensible in every way.
Once you have some knowledge around cats data types and typeclasses, you'll be able to enable all cats power in http4s for free. Since Kleisli has instances for:
- Functor
- Applicative
- Monad
- Alternative
- Choice
- Arrow
- Parallel
- Monoid
- MonoidK
…
As you can see it already cover most of the cats typeclasses, plus, those typeclasses are almost the most popular typeclasses in FP and cats.
And you will get all the chance to practiceall thee fun in your http4s server by just using Kleisli data type.