CoRecursive: Coding Stories - Tech Talk: Http4s and Functional Web Development With Ross Baker
Episode Date: July 27, 2018Tech Talks are in-depth technical discussions. The promise of functional programming is code that is easier to reason about, test and maintain. Referential transparency means there is no extra context... to worry about, we can just focus on inputs and outputs. Examples of functional programming in the small are plentiful. Fibonacci is easy to write as a function but what about fp in the large? Http4s is a web framework written in scala that takes a pure functional approach to building http services. Ross Baker is a contributor to http4s and he explains the benefits of this approach. We also touch on the benefits of working remotely, since he and I have both been doing it for some time. Links: Http4s Presentation on Http4s Today I talk with @rossabaker about http4s and the benefits of a pure functional approach to building http services
Transcript
Discussion (0)
Welcome to Code Recursive, where we bring you discussions with thought leaders in the world of software development.
I am Adam, your host.
To me, the promise of functional programming is code that is easier to reason about. Referentially transparent code,
wherein if I give it some inputs,
I get a defined output back, always.
And there's no magical context outside of this.
It makes code easier to understand,
easier to test, easier to debug.
From a unit testing perspective,
you don't have to mock a bunch of things.
You can just provide the inputs and then measure the outputs at whatever level of the system,
if it's just a simple block of code or if it's a giant web request. I think, though,
it's easier to think of these examples like a function that returns a Fibonacci value.
Much harder to imagine a functional, referentially transparent system that is doing something complex like a web framework.
HTTP 4S does the latter.
It attempts to, and succeeds, I think, at taking these functional concepts
and applying them to web frameworks.
Today, I talk to Ross Baker about how this works and the benefits thereof.
Ross Baker is the creator of the HTTP4S project.
Ross, welcome to the podcast.
Thank you. I'm glad to be here.
Is creator a good term? The major contributor? What term do you like?
I am one of the originators and I've been around the longest,
but through the life of the entire project, we've had several people contributing to it.
So I push back a little bit on it being my project,
but I have been around the longest.
So what is HTTP4S?
So HTTP4S was created to fill a hole
that we saw in the Scala community,
where you look at other languages
and they all had this abstraction
that would let you write multiple web frontends on top of it
and bind it to multiple servers.
So you think about Ruby where they've got Rack, or you've got Python where you've got WSGI,
or in a little bit more of the typed FP realm, you've got Haskell where you've got WAI.
There wasn't anything quite like that in Scala.
And we wanted to create something like that where you could say,
all right, we want to take a DSL that looks a little bit like unfiltered or looks like Scalatra
or looks like something else entirely and be able to run it on your choice of backend,
whether it's a servlet container or on a Netty backend or on other backends yet to be imagined.
We wanted to have that Cartesian product, so to speak, between ways of writing services and what you run the services on.
So HTTP4S was born as that abstraction layer to sit between the two to kind of fill that gap in the Scala community.
And when you say to bind to multiple servers, but one at a time?
Yes, one at a time.
So you've got your choice.
You can write your HTTP for SAP. Most people use what's
called the HTTP for SDSL, which is based on Scala extractors, just pattern matching of requests.
That's how most people are writing their services at this point. And you can write that and you can
take it and you can run it on Jetty or you can run it on Tomcat or what most people do is they run it
on our native Blaze backend. You mentioned one of the goals was, well, I don't know if you mentioned it as a goal,
but you mentioned Netty.
So does it run on Netty?
There's an open pull request to get it working on Netty.
So originally on the project,
the reason the project started in the first place
was a bunch of us had been working on Scalatra.
It was a port of Sinatra that says Ruby framework to Scala.
And that was very tightly coupled to the servlet container. And what we wanted to do was be able to run it on Netty. We had a lot of users
who wanted to do that. We thought we could get a little bit more speed out of it and we were
missing abstraction. So it was a bunch of Scala or Scalatra developers early on the project who
got that started. And we tried writing a Netty backend for it. And the person who was in charge
of the Netty backend, he ended up peeling off the project. And somebody else came
along. His name is Bryce Anderson, a really bright guy. He works at Twitter now. He wanted to learn
how to do NIO. So he came along and wrote his own backend just to learn NIO. That's the Blaze
backend that grew into the primary one that most HTTP for us users use today. So Netty was an original goal of the project.
And then about five years later, we finally got a pull request where it's coming to fruition.
Nice. Maybe we should define Netty.
So Netty is, to my understanding, it basically is for serving network requests.
How would you describe it?
Yeah, it's just a good general networking toolkit. It's built on NIO.
You can do non-blocking with it as opposed to some older JVM techniques.
It's just kind of the foundation of a lot of networking
libraries in the JVM. It surprises people when they come
to HTTP for us for the first time. They expect there to be a nutty backend.
And we say, well, actually, we've got this other thing that somebody wrote on his own and completely reinvented things,
but it works out really well for us. So that thing is Blaze, is that correct?
That's Blaze, yes. Describe how Blaze differs from Netty, if you could.
Okay, so Blaze is written in Scala, whereas Netty is a Java project. So
Blaze is pure Scala,
but it still differs a little bit from the rest of the
HTTP4S code base because
Blaze emphasizes futures
and byte buffers and mutability,
whereas HTTP4S is
much more on the functional side of things.
Blaze is going for raw performance.
It gets down really in the muck.
So it uses byte buffers directly.
It uses futures.
There's mutability happening all over the place.
It's a very different code base from HTTP for S.
Now, an HTTP for S user is not going to have to be exposed to any of that
because HTTP for S wraps around that.
It provides a pure functional interface on top of it,
so it encapsulates all that.
But for speed underneath the covers, Blaze resorts to those tricks.
And it ends up being really nice,
because that way it provides a reasonable level of abstraction,
lets people write code in the functional style with all the benefits thereof,
but they get the performance benefits of getting down there
and doing the really dirty tricks that the JVM is optimized for.
So the two work together very well, it turns out.
So an interesting thing about HTTP4S and Blaze, I mean, an interesting thing, I guess, about HTTP4S to me, I guess, was that it doesn't use Netty. And then a secondary interesting thing is that
when I was looking at benchmarks,
like there's like the Tech Empower benchmark.
I don't know if you're familiar with it.
It's like a million web server, web frameworks all tested.
Like it does very well.
Like it's performant.
Yes.
So Raw Blaze is going to do better than http for us on those benchmarks
just because when you think about it http for us is a layer that sits on top of blaze so http for
us is doing all the work that blaze is doing and more all these functional wrappers in terms of
wrapping bodies in an fs2 stream and adding an immutable object model that's nice for programming
but that turns out to not be free. So if you compare
those benchmarks, you'll see that Blaze does better than HTTP for S. And we get a lot of
questions about that because, oh, I see that HTTP for S has so much overhead. And my answer to that
is that's true when you look at the benchmarks. But when you look at real world applications,
if you look at the tech and power benchmarks, what they're doing is they're doing a ping response or they're doing the most trivial JSON parsing.
And that's not what most people are doing.
Most people, when they're handling a response, they're doing a network call.
They're talking to a database.
They're making another web service call.
They're maybe aggregating a web service call with a database. database and when you add all that up it turns out that the overhead that http for us adds to
provide that functional api on top of something lower level like blaze turns out to be insignificant
in the grand scheme of things yeah agreed and and if you look um like if you compare apples to
apples if you compare http http for us i forget what stats i was looking at but i was comparing
it to uh the play framework and Drop Wizard, and it seemed to be
doing better than either of them. Except on
the serialization of Jason, but I think that's unrelated.
So, I mean, I think that it's quite fast is the thing
that I was noting. Yeah, my answer to people who are concerned about it
is it's always been fast enough
for me. I've used it at three companies now, and it's never been a problem for me. So definitely
a goal of the project is to be as fast as possible, but we're not going to sacrifice correctness or
being able to reason about functional programming toward that goal. So it's a goal, but it's a
secondary goal. So let's talk about that. So what is your
goal around functional programming and your framework? That's an excellent question. So
if you look at the landscape of web libraries, that tends to be not an area where functional
programming has excelled in the past. It's an area where you see a lot of
mutability all over the place. I've seen a lot of apps where they have a functional core and then
on the outside they will use something like Play or Akka HTTP or something like that that isn't
pure. And what we wanted to do is come up with something that pushed functional programming out
further to the edges of the application.
Now, at some point, you're talking to a socket, so I.O. is still happening.
Every time you're doing functional programming, there's still an end of the world,
and beyond that end of the world, you're not functional anymore.
But what we wanted to do was see, could we get any benefits out of expanding that functional universe?
Is there value in having referential transparency and composability up at the web
tier layer rather than just at the business logic layer that was one of the real motivating factors
for the project as well and how have you found trying to work towards that goal i i've been very
happy with it because it's there's always this tension in Scala where you've got
when you start doing functional programming, you start using things like an IOMonad
or a task, or there's various implementations out there,
Cats Effect IO or Monix Task. You use those and then
you get into some other libraries and they want to use Scala Concurrent Future
and trying to adapt between those two worlds. You can adapt between those two worlds. It's just a couple lines of code, but you've got
that fault line in your code when you don't push the functional boundaries all the way
out to the end. So it's really nice to have a web library where everything is based on those
same functional monads that the business logic here is based on. So we're
polymorphic over our effect. That means that anything that implements the
cats effect type classes
is something that you can run your server in,
and that lets you bring your Monix task or your Cats Effect IO
or there's a ZIO project out there that also implements the type classes.
There are several options out there, and whatever you've built your backend on,
you're going to be able to build your web tier on,
and you're not going to have that mismatch between the two base effects anymore.
So being able to have all that line up
has been very pleasant to deal with.
Yeah, to unpack that,
so you're saying you want the framework to be,
you want it to follow functional programming principles,
so you want your side effects kind of wrapped in something, right?
And in Scala, there's like several competing somethings, which is sometimes
attention, right? So the trick that you've used is to abstract over that using Kat's effects.
Is that right? Correct. Yeah. So that's a relatively recent innovation in the project.
When we first started it, we were built on top of Scala Z concurrent task.
And then we moved over to FS2.
As Scala Z stream gave way to FS2, FS2 had its own task monad.
So we started to use that.
And around that same time, Monarch's task was becoming popular.
And there were some people that were still off on Scala Z and didn't want to move.
So we had all these competing things that became clear that there wasn't going to be one effect that was going to dominate all the others in the community.
And the functional programming community in Scala is small enough as it is to divide things even further.
That's kind of unfortunate.
And the CATS effect project came along providing type classes over these various things. So it's got this notion of an effect type class,
and it's got some weaker type classes underneath that. But basically what an effect type class does
is it defines things that are able to describe an effect that gets run when you choose to run it.
And it's able to describe asynchronous effects. It's able to describe things that can be flat mapped. It's a monad. It's able to do
air handling. It's a monad air. It has all these capabilities built into it. It's got brackets,
so it's got resource safety built into it. So there are a bunch of types that satisfy those
constraints for things that we're looking for up in the web tier. So by expressing everything in
terms of this effect type class class we're able to welcome everybody
back under the tent as long as they've got an instance of that type class they're able to
use hdb for us without any friction between their back end and their front end it's kind of a it's
kind of funny in retrospect i guess right like okay we want to we want to abstract and encapsulate
these side effects and then but everybody wants to abstract and encapsulate these side effects. But everybody wants to abstract and encapsulate them
in their own little way.
And then so as a framework builder,
I can see how that poses a problem for you.
Yeah, that was something that was very frustrating to me early on.
I just thought, okay, an IOMonad is a pretty basic thing.
We should all just come to agree on one.
Everybody should use that, and we don't need this.
But there are a little bit different trade-offs on it,
where if you look at a Cats Effect I.O. versus the Monix task,
Monix task has a scheduler that you pass along
and it's able to do things like fairness
because it passes along this context.
But in I.O. Monad, the Cats Effect I.O.,
it doesn't need that context passed along.
But at the same time, it's not able to do those automatic thread shifts for fairness. So you get a little bit of a trade off there on complexity of
the implementation versus some things that it can do for you out of the box. So there are some pros
and cons to those various things as well. So I wanted to return to your statements about
how a functional paradigm makes a lot of sense for a web framework.
And I think you're right in that most web frameworks,
they seem to have a bunch of extra stuff.
They're not pure functions that take a response and produce a result.
But why not, right?
If you think of, I make a request against a web server,
I'm giving it this request,
and then all it needs to do is provide a response.
Obviously, there is I.O. happening there,
but it seems like a very contained function.
Exactly.
And one area that we find that really shines is for testing.
So people come along and say,
yeah, we're really enjoying this library, we got a service up and say, yeah, we're really enjoying this library.
We got a service up and running, and now we're talking about testing it. And we're wondering
what sort of test kit do we need? Because people are used to having like the ACA test kits,
or I'm showing my age way back here. I've been doing servlet programming for a long time. What
we used to do to test things is we used to use this library called Cactus, which would let you run JUnit tests remotely into a servlet container using a bunch of RMI calls.
So we've come a long way over the years.
And what we get down to in HTTP for us when we recognize that a service is just a function
is you don't need any kind of mock container at all.
You don't need any web server at all.
You've just got your function that represents the service.
You construct a request in your unit test, and then you assert over the response in
your unit test. And you don't need to mock anything anywhere along the way.
Yeah, it sounds like a great promise. So if we, I mean, you can think of it as this input and
output request and response. But I think that in practicality,
it needs to get much more split up than that, right?
Like we may have, like I pass some sort of JSON object
through like a form post,
and then it gets like converted to something else.
And then that gets processed.
And then maybe we need to reach out to the database
and provide an update, some row,
and then return a result.
So when we have all these different steps,
how do we compose that in HTTP 4S?
One thing that I like to encourage people to do
is just remember that everything is a function.
So the definition of a service, it's a Kleisler arrow.
You've got some effect, type of F
that can be your IO or your task or whatever.
And then you're going from a request to a response.
And if you think about that, we can unwrap the Kleisli. We can set that
aside for a second. That's just a glorified wrapper around
a request to an F of response.
And those functions compose. So instead
of having a request to an F of a response, what you can do is instead of having a request to an f of a response,
what you can do is you can have a request to an f of an a, and what that represents is parsing
some type a out of your request. And then you've got your business logic, which is a to f of b,
which represents an asynchronous function from an a to your response type d. And then you've got a
function of b to F of response,
which is how you take a B and serialize it back into a response.
And you take all those three functions I just described
for request to F of A, and then A to F of B,
and then B to F of response.
And you can compose all those back together,
and you get back to that original shape of request to f of response so you
can decompose your logic however you like and when you look at that with your a to f of b there isn't
anything related to http inside of that so that should reflect the shape of your business logic
so it lets you not have to worry about these uh um you know uh wire level concerns in your business logic or in your serialization logic.
Right. Just like we want to push the networking level out to the edge of the world,
what HTTP for us does with its functional interface is it lets you push the HTTP concerns
out to not the fullest edge of the world, but the next layer beyond that, the next layer of the
world. HTTP can live inside of there and you can just
keep getting closer and closer until you get down to your core of your business logic and that's
just a simple function from a to f of b so if this http application is uh is a function from
from a request um to a to a response in some effect um what what happens if it's like a 404?
So it's like the function's not total.
There's no route that matches this request.
How do we model that?
So one thing that you can do is,
this is hard-coded in the current production version of HTTP4S.
We're loosening this constraint to enable some more things.
But right now, if you look at the definition, it's actually a clisely of option T over F, question mark, to request, to response.
And what that option T does is that's a monad transformer that lets you embed an option
inside of an effect. And what that lets you do is it lets you respond asynchronously and decide,
can I handle this request or do I need to pass this request on to someone else? So you either
return an F of some response or you return an F of none. And what we get out of that option T
is you've got a semi-group K over that option T.
And with that semi-group K, what that enables us to do is you can take two services,
and you've got this neat little operator in cat, so it's combined K.
The symbolic version of it is less than plus greater than.
You can take two services and glue them together like that.
So you've got one service that tries to handle the request, and if it can't handle the request,
it will delegate on to that other service that you have combined K onto it.
And if none of the services that you've chained together in that way work, then you end up, the backend knows how to handle an F of none, and it knows to turn that into a 404 response.
There's a lot there, i think i follow so so a service is so it returns a none um in this option
t and then you're saying uh the the service as a whole is a semi-group so then semi-groups expose
like concatenation so you you concatenate together these these services so that's how you can combine
services and then and then if you still have a none,
then you have some sort of handler that returns to your 404. Did I get it vaguely right?
Exactly. Yeah. So what we were describing before with the request to F of A and A to F of B and
B to F of response, that's a way of decomposing services. And then what we just talked about is a way of combining services.
And then we've got a third way that you can stack things up. We've got this concept of middlewares
in HTTP for us. And what middlewares do is that's a function of one service to another service.
So that's a way of doing, I like to think of that as vertical composition, whereas what we just described is horizontal combination.
But with vertical composition, what you can do is you can take a service and you can change a request on the inbound side of the service,
or you can mutate the response on the outbound side of the service.
And that enables you to layer extra concerns on so you don't need to handle it in every route inside of your service, but build up these other things. The term middleware, that's borrowed, I think, from Rack as the first place
I've seen that in the Ruby community. And that lets you add on concerns like gzip compression.
So you can check the headers on the way in for what's accepted. And if it's acceptable to gzip
the response, you can gzip the response on the way out. And then your service doesn't need to
worry about that. Or authentication is a middleware where you take a service
and then you wrap some authentication logic around it
and make sure that the service only gets invoked
if the user is authorized to use it.
The middleware is applied to all routes, I guess.
Is that correct?
Correct, yeah.
So a middleware is a function of one service to another service.
And yeah, it just wraps around. If you're familiar with Java servlet programming,
it's like a servlet filter. Other libraries will call those interceptors. There's various
terms for it. You find this concept over and over again, way back in the history of web frameworks.
We just call it middleware because we're pretty Rack and WSGI inspired,
but it's the same concept. And the reason it takes a service and returns a service is
because it's just wrapping it and putting some extra functionality inside and then calling it.
Exactly. Yeah. And that ends up being totally transparent to the service that you're wrapping,
and it ends up being transparent to the backend. The service is just the contract layer between the people who are implementing their business logic and the people
who are implementing the backends to run the business logic. I think it's pretty neat. I
imagine I want to talk to you a little bit about people learning this framework, but I think that
if you know the concepts, it seems to come apart nicely, right? You're saying, oh, to write a route, you just do pattern matching, right?
Your service just has some case classes.
And then to combine these services, well, it's a semi-group
and you just kind of concatenate them together.
And then to do a filter-style thing,
you just take one service and emit a new one that wraps it.
The concepts seem very clean.
Yeah, that's definitely been a design goal is what we want to do is we want to use scala concepts and well-known fp concepts
wherever they apply so the idea of combining two things together we could have come up with some
ad hoc way of combining two services together but if you look to the functional programming
community for inspiration how do you combine two things well you've got a semi-group. That's a way of combining things.
Now, ours happens to be higher kinded. We've got that type parameter in there. So it's a semi-group
K rather than a semi-group, but same basic concept. That's something that was already
laying around. So rather than inventing new syntax, we're able to take something that you
can use to combine things that have nothing to do with HTTP, and we're able to reuse that vocabulary. So anybody who's already
immersed in this, they understand that. Now, not a lot of people understand semi-group K coming in,
so that's something that we've taught to other people. But that ends up being cool, too, because
then when they see semi-group K outside of HTTP for us, they recognize that these things all start
to click together once you get immersed in this community.
Or as you were saying with pattern matching, that's something that you don't even really need to be into functional programming to do.
The reason we like the HTTP for S DSL is if you understand how case classes work and how extractors work,
you can get a pretty firm grip on how to match requests and do your request routing.
We're just using basic Scala concepts for
that. Anywhere we could reuse something where it's going to interoperate well with what people
already understand, that's something that we've strived to do. Yeah, it's interesting. I mean,
one thing is that maybe you're bringing people up to speed on these concepts, but these concepts
aren't unique to your framework. I know in the play the play framework i may get this wrong but uh they have
like action filters and i'm pretty sure their action filters are just like klyzelys but they
call them something different like they just call them action filters but the methods kind of map
they have like a and then yes that's something that we've gone back and forth on a lot in terms
of what is the value of type aliases and we tried to hide some of those things behind type aliases.
So we would use the real functional types underneath the covers.
We would alias things so we weren't afraid to use a Clisley.
But maybe we didn't want to call it a Clisley.
Maybe we wanted to put an alias on that.
And a little bit of feedback we've gotten from the community is
it's easier to just learn the common term for these things
than it is to learn your aliases for them. Even if you're using the right types under the covers, those
aliases add confusion. So there are a few aliases that we've deprecated over the years. We've still
got some, but there's some that we've deprecated over the years for just that reason, where people
are actually pretty capable of learning that vocabulary and want to because that's the same
thing that they can use elsewhere.
What have you found are the learning difficulties of people coming to this library?
I think one of the things that's been a hard leap for people is they're used to future and then they see these IO types and they're pretty similar in terms of, yeah, they model asynchronicity,
they're a monad, so I can flat map those. People come into it with that sort of understanding. But the idea of eagerness
versus laziness, that's one thing that's tripped people up a little bit. It's they're getting up
to speed on the idea that they're describing this computation, but it isn't running yet in
the same way that a future is. If I had to say what's the biggest difference between an IO and
a future, it's that when you construct a future, it starts executing right away. It's already running in the
background, whereas when you construct an IO, that's not going to run until you tell it to run.
People run into some surprises with that, and they don't understand, well, why don't you have
a binding for future? You can bring all these other things. You can bring an IO or a task. Why
can't you bring a future? Well, some of the tricks that HTTP for S uses
are built on top of that laziness
and having the backend in control of what's running.
If we want to get really deep into the weeds here,
the key version or the key distinction
is that future can't have a sync instance
because it can't delay its effects.
And that's something that we need to do in HTTP for S.
It can't have a sync instance. Describe sync.
Sync is one of the type classes that comes from Cat's Effect. It's one of the super classes
of the effect type class that I keep talking about. And sync
is something, it extends monad, it extends a couple other things.
One of the big operations that it adds is it has a way of
delaying a computation.
And that notion of delaying a computation doesn't exist in future.
Once you construct a future, it's already running.
And a good example of why that hurts in HTTP for us is,
let's go back to that example of combining services with the combine K operation,
where you execute one service,
and then if it falls through, you execute another service.
Well, if you look at future,
what happens is you've got to run both sides of that.
So you run one side of it, you get your IO.
You run another side of it, you get your IO.
And then you look at those IOs.
To combine those IOs, you need to have both I.O.s.
And then you look inside of that first I.O., and then if you've got a sum on that, you don't need the value of the second I.O., so you don't actually need to execute that.
But you needed to construct that I.O.
That's the problem that we run into when we use future, is if you're trying to use future as your base monad, you invoke one service
and you get a future and you invoke another service and you get a future. Let's say that
the first service responds. And when you look at that first service response, so you don't need
the value of that second service, but that second service has gone ahead and executed this effect
anyway. And maybe that effect is to execute a delete operation that mutates your database
when that request should have never run.
So that's what we get out of laziness. You're able to build up these constructs using laziness
that you can't do with future. So that's a funny example. So for instance, like it could,
it could be just matching routes and the first service matches on something. But then if that
first service wasn't there, it would fall through to some delete. And that's going to get executed anyways, because if in fact it wasn't a future,
is that right? Right. So delete is an extreme example. Ideally, you don't have a delete as
your fall through case. That's a little bit of a contrived example. But you can think of things
that do hurt you where it's an expensive call. So maybe you've got one service out in front that operates off of a cache. And then if you don't have a cache value of it that you fall through
and you do the expensive call, well, if you try to combine those two services together,
that's a very natural way of combining two services. If you do that with future in this
architecture, what happens is you call the cache and then you call the underlying value.
And the underlying value is computed even if you have the cache value
because you've returned a future from each of those
that you're trying to combine together.
So in terms of doing destructive updates,
that's probably not so much a real-world concern.
But in terms of doing something expensive,
doing more work than you need to do,
and that kind of gets to the idea of it being lazy.
We call it lazy for a reason. You don't do any more work than you need to do. And that kind of gets to the idea of it being lazy. We call it lazy for a reason.
You don't do any more work than you have to do.
Future is just a bad effect system, maybe.
Do you agree?
Future is not ideal for functional programming.
Let's put it that way.
There's been a lot of debates over future over the years.
So if you're not doing functional programming,
future has some really nice aspects to it. So if you're not doing functional programming, future has some
really nice aspects to it. So future is really good at fairness with its execution contexts.
It's good at memoizing values. So you refer to a future and you refer to it multiple times and you
don't have to recalculate that value. So there are some nice things about future,
but none of those things are really nice once you enter the functional programming realm.
Memoization, if you're not thinking in terms of FP,
you just want to avoid repeating work.
When you think in terms of FP,
you want referential transparency.
So the memoization, that can be good or bad
depending on your perspective
and what you're trying to accomplish.
I think we're deep in the weeds of FP concepts.
So I think we've already lost anybody that doesn't know these terms.
So I might as well just ask it.
You said before you can combine services because they're a semigroup.
Why aren't they a monoid?
Can't you have an empty service?
They could be a monoid, but then what you have to have is you have to have a zero value.
So what would be the zero value for a response that would respect all the monoid laws?
That's something that ends up being a little bit tricky.
So a knee-jerk reaction to that is your zero value might be a 404 response.
Yeah, yeah. That's what I was thinking be a 404 response. Yeah, yeah.
That's what I was thinking.
Your 404 could be your empty case.
Okay, and now let's talk about combining things with identity laws and so forth.
So if you have a 404 as your base case, is your 404 because you have actively asserted that there is nothing there?
Or just because you're saying, this is something that I don't know how to handle. So maybe you've got something where you have handled
the request and you want to absolutely say there is nothing here, there should be nothing here,
I don't want any other services consulted, I want this to be a 404. If you use a 404 as your zero
and you try to combine things, if a service tries to say, I am asserting that nothing is here,
I insist on returning a 404, well, when you combine that,
the combination operator is going to look into that and say,
oh, a 404, if it's a 404, I'm going to try to consult this other service anyway.
Maybe that's okay, maybe it's not.
I guess it depends on your perspective on things.
Because, yeah, if a service doesn't match anything,
you want to go to the next service.
Right.
So when you treat things as a monoid
where you're looking at a response with a zero
rather than a monoid K where you're looking at two effects
and seeing whether you're combining those
irrespective of the value type they're in,
that's where you run into trouble.
It's the idea of an implicit 404 because the request fell through
versus an explicit 404, which is something that is really meaningful.
Yeah, I believe I understand the complexities there.
So we used to have this idea of a special fall through response. This was
terrible and I'm glad that we got rid of it. We used to have a monoid rather than a semi-group K
and the monoid, we had a special response, which was something that would return a 404
and then to treat it as a 404 that fell through versus a 404 that doesn't fall through.
We would look at that by reference equality and see, oh, this is the magic 404. And that was just a very ad hoc concept that
confused a lot of people. That was not one of our better design decisions. So that was something
that was stripped out in favor of the architecture that we have now. So the answer is we tried your
idea and it was horrible and we're glad we got rid of it yes that's funny um i saw i saw this uh
i saw this talk that you did that began with the statement uh http applications are just
klyzely functions from the streaming request to the polymorphic effect of a streaming response
so so what's the problem? Which is hilarious.
How about streaming?
How does streaming work?
So streaming is something that we have built in from the very beginning.
We've talked a lot in terms of FP and in terms of making sure that we're not running our effects too soon and how you're able to asynchronously generate a response.
But when you look at what a response is and a request is, it goes deeper than that,
because you don't just have a single atomic chunk as a request or a response.
You have these bodies, and these bodies can be very large.
They can be as large as gigabytes, and they arrive over the network not all at once.
You don't just typically get a gigabyte, snap your fingers, and it's all there. It's something that streams in. So what we wanted to do was we wanted to,
from the very beginning of the library, come up with a streaming approach to handling
request bodies and response bodies. So if you look at our request and our response definition,
you'll see that the body is defined as an FS2 stream of bytes.
And that's something that fits very nicely into this functional ecosystem as well.
FS2 is functional streaming for Scala.
That's what the name means.
I guess it's FS squared.
I'm assuming that.
But functional streaming for Scala.
And you're able to consume these streams in a functional way. So you take a stream
and then you compile it down to an effect. So when you want to decode a body, what you do is you take
a stream and then you compile it and then you're able to fold that down so you get an F of A.
When we talked before about how you take a request and turn it into an F of A, the way that you do
that in parsing the body is you take the
FS2 operations, which takes a stream of F to byte, and it's able to return you an F of A through its
various operations. That's how you do the decoding. And then likewise, you can encode things. So you've
got your B, and you can turn that into a stream of F of byte. And you could put that into your
response wrapping around the status and any headers that you need to add. And you could put that into your response wrapping around the status
and any headers that you need to add.
And you can encode things out that way.
Nice.
So we're using FS2 as the kind of way
to get streaming?
Correct.
Yeah, and that ends up being very nice
because if you look at the servlet API,
the only way that you can consume a body that way is you've got Java IO.
It's got this listener interface on it so you can do non-blocking, but it's very complicated.
The typical way of doing that is you use the old Java.io, and that's a blocking interface.
There isn't anything functional about it.
You're mutating the stream as you go along.
Whereas by using FS2 streams, it lets us, in the same spirit as the rest of the application,
it lets you describe the computation.
You're describing how you're going to decode it,
and then at the end of the world,
everything compiles down into the F effect,
and the backend knows how to run that.
So it fits in very well as far as that goes.
Yeah, it's definitely a very clean way of structuring things
that you come up with.
I'm still surprised that you aren't saying, though, that the difficulty we have is people getting up to speed on just the conceptual terminology, I guess, associated with the project.
That hasn't been a problem?
Like Streams, Cliesley's, Semigroup K?
I think people embrace the vocabulary pretty well.
It's the concepts of laziness and not mutating all over the place.
That tends to be the bigger leap that people need to make
in terms of learning new words.
People absorb new words for things pretty easily.
There's a lot of hand-wringing in the FP community
that we have these scary words for things.
Instead of just calling it an
effectful function, we call it a Kleisli. Or instead of just calling it combinable,
we call it semi-group K. These are these words that we worry are putting off people. And people
who aren't immersed in functional programming, they're intimidated by these words. So maybe
that worry is true to some degree. But as people get in there and try to grasp the concepts,
I think they have more trouble with the concepts themselves
than the naming of the concepts.
Yeah, I suppose that makes sense.
So when is HTTP4S not appropriate?
Is there cases where I shouldn't be using it
as my web framework du jour?
Ah, excellent question.
So I would say that the JVM is too heavy for a lot of services. So those ping routes, if you look at the tech and power
benchmarks, you'll see that the JVM frameworks tend not to be at the top of the very simple
things. If you've got very simple needs where you're not going to benefit from this scalability of complexity that
you get with functional programming, or you're not going to benefit from having a long-running
server process that really warms up and has well-tuned garbage collection and things like
that. If you're just doing very trivial things, then I think you don't want to be on the JVM
altogether and you might want to look elsewhere. If you are committed to the JVM altogether and you might want to look elsewhere. If you are committed
to the JVM areas, you might want to look elsewhere. If you're not committed to learning the functional
concepts, if you are happy with futures and don't want to wrap your futures in IO, if you want to be
more in the mainstream, if you want to have your backend be built on top of ACA where you're just
asking actors and you don't want to learn any of these other on top of Akka, where you're just asking actors
and you don't want to learn any of these other things that wrap around it, then yeah, maybe
HTTP for us is not for you in that case.
There's definitely a learning curve there.
You have to buy into functional programming, and then you start to try to pick up other
things off the shelf, and you find that they're built on futures or they're built in immutable
style, and maybe you need to write a little light wrapper around that. Now, I personally think the benefits of that tend to pay off for those wrappers. So
I'm in favor of that. I'm in favor of teaching people that and a team that's willing to learn
that. I think they can be very successful with HTTP for us. A team that doesn't want to climb
that learning curve would probably be better off looking elsewhere. Yeah, and I think the unit testing alone,
like I guess there's more overhead perhaps
to learning some new concepts,
but the amount of time that is just spent,
you know, writing unit tests
or just trying to test things that are not a function, right?
That have all these contexts and mocking
and it seems like you could pay off
that learning very quickly.
Yeah, I think testing is one area that people end up being absolutely delighted.
Another one is we've got a nice relationship between the server and the client in terms of
if you want to provide a mock client, what you can do is you can take the HTTP service interface
and you can do client from HTTP service. So you can return your canned
responses in there. And then if you need to test a client, you've just got that client based on
that service that you can control. Or if you want to do a proxy server, you're able to take a client
and you can make that to an HTTP service where anything that comes in, you just delegate the
requests and you run them through the client.
There's that relationship because everything is built on top of that basic foundation of a request to an F of a response.
There's a close relationship between a client and a server
where you've got good benefits on the testing end going one direction
or you've got benefits on the proxy end going the other direction.
So where do you see HTTP for us going? Does it still have things
to accomplish and new features? Absolutely. So we would like to continue to add more backends.
We've got that nutty backend of the pipeline. Jose Cardona is working on that one. Christopher
Davenport is working on a pure FS2 TCP-based backend. That would eliminate one of our dependencies,
and it would be streams all the way down.
So we've got a couple more backends that we would like to roll out.
One thing that I would like to see more of
is I'd like to see more libraries written on top of it.
So a lot of people have associated HTTP4S with its DSL.
That's the case class extractors that you see all over the place.
That's a really good way of writing services,
but it has some limitations in terms of trying to come up with appropriate responses.
If you've got a route that matches certain methods,
but it doesn't match the method that you have requested,
that's something in pure HTTP you're supposed to return a 405 response.
But we don't have a way of saying, well, this request almost matched if you issued with one of these methods that would have matched, but we don't support
this method. There's a special response that you're supposed to return in an API for that.
The case class extractor DSL doesn't work very well for that, but there is something out there,
HTTP for us directives, which lets you compose services in a different way, and that's able to
handle it. That one is inspired by the ACA directives.
So that's an alternative way of writing services that comes with its own set of trade-offs.
Or we've got another library out there, Row, which is a really cool library.
It uses a bunch of implicit tricks to capture context about the function,
and it knows what response types can be brought in, how you're decoding things,
and it's able to compile your service definition down
into a swagger definition.
So you've got API documentation that's machine generated.
So that's three different ways that we have right now of writing services.
And I would like to see even more ways of writing services.
So I hope to see more of those emerge over time as well.
Yeah, so I would like to see the reverse.
So start from an open API spec and generate the scaffolding of the service.
Ah, that's interesting.
There is a project, I think it's Andy Miller,
he has a project out there where you take it
and we can generate an HTTP for us client off of that.
I don't think we have anything doing the server scaffolding,
but that's a very good idea as well. Yeah, or both ways. I just, it might be a strange request,
but it tends to be like if we're building some little service or something that we kind of start
with an API and we just kind of use like Swagger to kind of say like, this is what it might look
like. You know, cause I think APIs, like, you know, your interfaces are important, right? So sometimes we start there. And once you've started there,
I mean, I guess generating the other way becomes less useful.
Yeah, the problem if you're generating that server scaffold is how do you keep it up to date as you
start to implement those endpoints? And then you add something else into your Swagger definition?
How do you merge the scaffold of the new endpoints with the
implemented endpoints you already have keeping that in sync can be a challenge no that's definitely
true and uh yeah and translations like you know that the type system of of swagger is is not the
the richest um so i think there'd be some translation challenges there but uh
yeah that's my two cents yeah that would be a fun project yeah so i guess one other thing that i'd
like to see in the future of hdp for us is we would really like to steer it toward a 1.0 and
i'm a little bit embarrassed because the project has been out there since, I believe, 2013 is when we started the project.
Here we are halfway through 2018, and we're still on version 0.x.
And a reason for that is there's been a lot of churn in the Functional Scala community in terms of what libraries that we use.
Our project is older than CATS itself. It's certainly older than CATS Effect.
We've had some churn in the foundation libraries that we use and now we're getting to a
point where cats is at 1.0 cats effect as it's 1.0 release candidates that's coming very soon
there's a branch for fs2 1.0 so those are our three real core foundational libraries those
have finally committed to yes this is something that we're going to do long-term support on
we finally have a stable api and that's kind of removed our we're going to do long-term support on. We finally have a stable API.
And that's kind of removed our excuse for not having a long-term stable API. So once those
all hit 1.0, that's something that we need to get very serious about as well and make sure that
people can build on it with confidence. Has that been a source of difficulty for you, just the
dependency churn, I guess, like the dependencies you're using changing? Yes. So dependency churn, I guess, like the dependencies you're using changing?
Yes. So dependency churn, when those libraries change beneath us, that's been the primary motivator for us doing a breaking release. And we're able to fix a lot of our own things. So
it gives us an excuse to fix a lot of things that we didn't like about ourselves as well.
But that's been the motivating factor for when we do those breaking releases,
is that churn underneath us.
We started out, we started on Scala Z and Scala Z Stream.
And then we migrated as Scala Z Stream moved over to FS2.
We were trying to support both Scala Z and cats together.
And that was just too much maintenance.
So we ended up dropping Scala Z support.
If you want to use Scala Z, you still can.
There's a shims library for that by Daniel Spiewak. So there's a pretty good story if that's the way you go. But we were
able to just standardize on cats and cats effect. And now we're not trying to maintain multiple
branches at once. That's really been a blessing for our development because we've got a lot of
contributors, but still we'd rather be developing HTTP rather than developing compatibility layers and doing merges.
I can understand that.
One thing I wanted to ask you about was how you find working as a remote developer.
Let's see.
This is my fourth job working remotely now, and I absolutely love it.
It's been just a really good fit just for family reasons.
Being able to greet the kids as they come off the bus, give my wife the freedom.
She can go get an office job.
Her job doesn't relocate as well as mine does.
So that way I'm able to be the stay-at-home parent, but also the working parent.
So that's been really good.
And I'm able to expand out and work with people that I wouldn't get to work with otherwise.
I live in Indianapolis, which is a great town.
I love it here, but we've got a relatively small tech community.
And I'm involved in the local meetups, and that's great.
But being able to work with Californians and work with people elsewhere around the world on a daily basis, that's been something that's been really good living in one of these smaller tech towns as well.
Yeah, I think it's the future.
This is my third job as a remote developer,
and I live in a small town as well.
And I remember, like, I think the end of the first week
when I started my first job of working from home,
and I remember thinking, like,
I don't think I'm ever going back and do an office.
Right.
Yeah, it's just great because we worked in,
we're all enthusiastic about FP and FP is growing by leaps and bounds, but it's still a tiny percentage of the jobs out there. So when you're in a small town and you're working in a niche
technology, and then there are certain things that you like to focus on within that niche,
you're in a niche of a niche of a niche. And at that point, the geography becomes a constraint. That's really nice to shed. Definitely. Definitely. Well,
thank you so much, Ross, for taking the time to talk with me. Great, great project. And I hope
you and the people working on it continue the great work. Thank you. I appreciate the invite.
Really enjoyed the interview.