Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Functions / methods in Scala. How does this work?

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 following a course that introduces Akka HTTP. The code works, but I do not understand the route method:

import akka.http.scaladsl.server.Directives._

def route = path("hello") {
    get {
      complete("Hello, World!")
    }
  }

We are defining a method route that is being declared the value of path (imported from the line above), but then inside the path function we have something called get that I don't understand.

And when I am declaring path as a method, am I overriding it, or what's going on?

I would love if someone can explain what is going on, line by line. And don't mind that it is Akka involved. I want to know about the Scala syntax.

=================================================================

Thanks for all the great answers. I think I get it!

So to summarize my version of it.

path() is a function that wants a string. It returns another function that wants a Directive. And in Scala lingo we can do some kind of currying to directly send a directive to the returned function.

So everything in the block {} is sent to the function that path() returns. And since a block in Scala always returns the last line we are returning the get which by the same principles we call with complete.

get is also a function, that takes one parameter and that can be written as a block. This is equivalent to just writing get(complete("Hello, world")).

Thanks again!

like image 388
Kristoffer Avatar asked Feb 11 '19 13:02

Kristoffer


People also ask

How do functions work in Scala?

In scala, functions are first class values. You can store function value, pass function as an argument and return function as a value from other function. You can create function by using def keyword. You must mention return type of parameters while defining function and return type of a function is optional.

What is the difference between functions and methods in Scala?

Difference between Scala Functions & Methods: Function is a object which can be stored in a variable. But a method always belongs to a class which has a name, signature bytecode etc. Basically, you can say a method is a function which is a member of some object.

How do you call a method in Scala?

Method Invocation is a technique that demonstrates different syntax in which we dynamically call methods of a class with an object. There should not be any space between the invocation object/target and the dot(.) nor a space between the dot and method name.

What is a method function?

Method and a function are the same, with different terms. A method is a procedure or function in object-oriented programming. A function is a group of reusable code which can be called anywhere in your program.


2 Answers

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.

like image 186
Travis Brown Avatar answered Sep 30 '22 21:09

Travis Brown


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:

  • Currying: it's kinda "grouping arguments together";
  • Curly braces instead of round braces: to give a different look-and-feel to the call;
  • by-name parameters: to pass a code that will be evaluated only when needed.

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.


like image 39
ColOfAbRiX Avatar answered Sep 30 '22 20:09

ColOfAbRiX