I am new to Scala and have a hard time understanding all the ways of declaring and using functions. Can someone please explain, step by step, what is going on here?
I am
In your snippets there are several features of the Scala language and the compiler, let's analyse the ones I know:
def route = ...
defines a functions with no arguments and with result type determined by the return value of its body.
path("hello") {
...
}
I'm not familiar with the path
function itself but it seems as if there are three things going on in that snippet:
I don't want to spend time describing all of them as internet is full of resources that explain them greatly. But I want to link at least this great introductory article that helped me a lot in my early days.
The linked article shows you a full example on how to use all three features to build your own control structure like the one of the code you're using.
Moving on, the bit
get {
...
}
is again an application of the above points but this time there isn't currying so the curly braces are the only argument to the function.
complete("Hello, World!")
Is just a plain old function call.
In short that code uses some "tricks" that transform a function call into sometehing that resembles a special language construct and this can create confusion for the beginners.
This tecnique is used frequently to write Domani-Specific Languages (DSL) in Scala.
It might help if you see the code snippet like this:
import akka.http.scaladsl.server.Directives._
def route: Route = path("hello") {
get {
complete("Hello, World!")
}
}
I added the Route
type to show you that you are simply building a route using a syntax provided by Akka HTTP that allows you to define common matching criteria at a higher level, and nest specific criteria inside that section. Here you are using the routing DSL functionality in Akka HTTP. The routing DSL brings some implicits into scope. Using the path method ensures that you are able to handle requests coming into the host for the path host/hello
, which means your host can now handle get request for the path /hello
. The code body inside of the path directive represents additional matching criteria to check when we have a proper path match. The complete method knows how to convert to HttpResponse
. Here you are completing with "hello world", a plain text.
You might have additional HTTP methods standard requests like post, put, delete, as the case may be. Or even custom HTTP methods.
This is a convenient DSL for handling HTTP requests in Akka-HTTP. Check the Akka-HTTP doc here
You don't necessarily need to understand everything in this answer to use akka-http effectively, but I guarantee you there will be times—probably sooner rather than later—that you will be fighting with the compiler and will just want all of the fancy syntactic sugar to go away, and the good news is that there are tools that make this possible (the bad news is that once you get rid of the fancy syntax the reality can be a terrifying mess).
The first thing to note is that while the curly braces here may look a lot like scope or definition delimiters from Java or other languages, they're really just applying methods to arguments. You can do the same thing with parentheses:
scala> import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Directives._
scala> val route = path("hello")(get(complete("Hello, World!")))
route: akka.http.scaladsl.server.Route = ...
And while these get
and complete
things may look like keywords or something, they're really just static methods on Directives
(approximately—read this whole thing for the full story), so the following is also equivalent:
scala> import akka.http.scaladsl.server.Directives
import akka.http.scaladsl.server.Directives
scala> val route = Directives.path("hello")(
| Directives.get(Directives.complete("Hello, World!"))
| )
route: akka.http.scaladsl.server.Route = ...
That hopefully explains some of the syntax, but there's still a lot of invisible stuff going on here. If you're in a REPL, you can use scala-reflect's reify
as an extremely helpful tool to help make this stuff visible.
To start with a simple (unrelated) example, you might wonder what's happening when you see Scala code like "a" * 3
, especially if you know that Java strings don't have an *
operator, so you open a REPL:
scala> import scala.reflect.runtime.universe.reify
import scala.reflect.runtime.universe.reify
scala> reify("a" * 3).tree
res6: reflect.runtime.universe.Tree = Predef.augmentString("a").$times(3)
And there's the desugared version, showing the implicit method that's being applied to the string to give it the *
operator.
In your case you could write something like this:
scala> import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Directives._
scala> import scala.reflect.runtime.universe.reify
import scala.reflect.runtime.universe.reify
scala> reify(path("hello")(get(complete("Hello, World!")))).tree
res0: reflect.runtime.universe.Tree = Directive.addByNameNullaryApply(Directives.path(Directives._segmentStringToPathMatcher("hello"))).apply(Directive.addByNameNullaryApply(Directives.get).apply(Directives.complete(ToResponseMarshallable.apply("Hello, World!")(Marshaller.liftMarshaller(Marshaller.StringMarshaller)))))
We can reformat the reified expression for readability:
Directive.addByNameNullaryApply(
Directives.path(
Directives._segmentStringToPathMatcher("hello")
)
).apply(
Directive.addByNameNullaryApply(Directives.get).apply(
Directives.complete(
ToResponseMarshallable.apply("Hello, World!")(
Marshaller.liftMarshaller(Marshaller.StringMarshaller)
)
)
)
)
If you add a couple of imports, this is also perfectly legal Scala code:
scala> import akka.http.scaladsl.server.{ Directive, Directives }
import akka.http.scaladsl.server.{Directive, Directives}
scala> import akka.http.scaladsl.marshalling.{ Marshaller, ToResponseMarshaller }
import akka.http.scaladsl.marshalling.{Marshaller, ToResponseMarshaller}
scala> val route = Directive.addByNameNullaryApply(
| Directives.path(
| Directives._segmentStringToPathMatcher("hello")
| )
| ).apply(
| Directive.addByNameNullaryApply(Directives.get).apply(
| Directives.complete(
| ToResponseMarshallable.apply("Hello, World!")(
| Marshaller.liftMarshaller(Marshaller.StringMarshaller)
| )
| )
| )
| )
route: akka.http.scaladsl.server.Route = ...
To explain this step by step, we can start with path("hello")
. We can see from the API docs that Directives.path
doesn't take a string, but rather a PathMatcher
, so we know that an implicit conversion from String
to PathMatcher
is kicking in, and in our fully desugared version, we can see that here:
Directives.path(
Directives._segmentStringToPathMatcher("hello")
)
And sure enough if we check the docs, _segmentStringToPathMatcher
is an implicit conversion of the appropriate type.
A similar thing is happening in complete("Hello, World!")
. Directives.complete
takes a ToMarshallableResponse
, not a String
, so there must be an implicit conversion kicking in. In this case it's ToResponseMarshallable.apply
, which also requires an implicit Marshaller
instance, which in this case it gets via an implicit conversion from a ToEntityMarshaller
to a ToResponseMarshallable
, where the ToEntityMarshaller
instance is Marshaller.StringMarshaller
, and the converter is the Marshaller.liftMarshaller
part:
Directives.complete(
ToResponseMarshallable.apply("Hello, World!")(
Marshaller.liftMarshaller(Marshaller.StringMarshaller)
)
)
Remember how above I said get
was just a static method on Directives
? That was kind of a lie, in the sense that while it is a static method on Directives
, we're not calling it when we write get(...)
. Instead this get
is actually a no-argument method that returns a Directive0
. Directive0
is a type alias for Directive[Unit]
, and while Directive[Unit]
doesn't have an apply
method, it can be implicitly converted into a thing that does, via the addByNameNullaryApply
method on Directive
. So when you write get(...)
, Scala desugars that to get.apply(...)
and then converts the get
value into a Route => Route
function, which has an appropriate apply
method. And exactly the same thing is happening with the path("hello")(...)
part.
This kind of thing may seem like a nightmare, and as a long-time Scala user I can tell you that it definitely often is. Tools like reify
and API docs can make it a little less horrible, though.
There are many things that are going on here and this is a pretty complex example to understand scala. But i'll try.
The route
's type is Route
, which is a type alias defined as type Route = RequestContext ⇒ Future[RouteResult]
where RequestContext ⇒ Future[RouteResult]
is a function that consumes RequestContext
and produces Future[RouteResult]
.
path
is a method that creates a Directive[Unit]
. There is an implicit conversion that converts Directive[Unit]
into a function Route => Route
(simplified). A function can be called by method apply
or with compiler sugar as (???)
or {???}
.
get
is a method that creates a Directive[Unit]
too and similar approach applies to it.
complete
is of type StandardRoute
that extends Route
.
Knowing all this, we could uglify your example that will be written as
path("hello").apply { ctx =>
val inner: Route = { ctx =>
ctx.complete("done")
}
get.apply(inner).apply(ctx)
}