HTTP services behind RPC message based gateways with Tapir and ZIO

Summary

I will cover a practical technique to implement "lambda-like" HTTP services behind RPC-as-message-passing gateways with ZIO and Tapir libraries. The advantage of such an approach is that you can keep the business logic and interfaces of the service(s) separate from the transport layer; and since the eventual transport layer is HTTP, simultaneously provide nice local testing tools and API documentation. The technique will involve some complexity on the type level, and we will try and evaluate the tradeoffs at the end of the post. Code is here.

Background

This post is inspired by a situation that came up at work. We have APIs that are accessible over HTTP but are backed by message queues. The "gateway" to translate is almost transparent (± a few extra headers) for both the caller and callee and we want to take advantage of existing HTTP tooling (OpenAPI specs, test clients). A previous version would manually translate the HTTP semantics (header parsing, query parameters, etc.) which was quite fiddly and there was no API spec that clients could use.

The Tapir project is a Scala library that allows you to declaratively define service endpoints (separately from their implementation) and interpret both HTTP servers and HTTP clients using a variety of different libraries (usually with a flavour matrix of underlying HTTP library e.g. http4s, armeria or netty, and effect type e.g. Future, ZIO, F[_] etc.). There is also support for executing HTTP endpoints on AWS Lambda, so this idea of interpreting an HTTP endpoint definition for something that abstracts away the actual HTTP server from you is not new.

I have also been trying out ZIO 2.0 after a long time of using cats-effect. This post will assume some familiarity, so I will cover the basics quickly (assuming prior experience with cats-effect IO or similar). Unlike an IO[A] which represents an unevaluated program (() => A) returning A and possibly throwing Throwable during its eventual evaluation, a ZIO[R, E, A] is a little bit more sophisticated. It represents an unevaluated program R => Either[E, A] that can also "die" with a Throwable but has a dedicated error channel for "business" errors that need to be handled. The R is called an environment type and along with the error handling really sets ZIO apart from any other effect system. It has long been known that constructors do not compose, so if your app is made up of multiple "modules" then techniques like dependency injection or the Cake (anti)pattern have been used to bring composition back into play. With ZIO, the R is an intersection type on the type-level and a type-keyed map at runtime - this allows (almost) seamless composition. This topic is easily a separate article though, so I recommend to go and read the ZIO docs and play with it yourself.

Tapir Endpoints

Suppose you have an API endpoint /foo that you can 'GET' to retrieve all the foobars in your system. (I decided to use these classic names instead of something concrete because the concretisation probably won't help). You might be tempted to write something like:

@app.route('/foobar')
def get_foos():
    return load_foobars_from_database()
val route = path("foobar") {
  get {
    complete(loadFooBarsFromDatabase())
  }
}

(using flask and akka-http respectively)

This is fine, and very little code, but has the following disadvantages:

  • error handling is not explicit (and by looking at the type signature only you cannot determine what kind of handling to expect)
  • does not help in providing an API spec to clients
  • advanced features like streaming request/response bodies are typically under a different API

A Tapir endpoint is a precise model of the endpoint, covering error type, secure inputs, public inputs (query parameters, form bodies etc.), the ability (or not) to processing bodies in a streaming fashion (or even use WebSockets). Here is an example definition:

  val listFooBars: PublicEndpoint[Unit, Error, List[FooBar], Any] =
    endpoint
      .get
      .in("foobar")
      .out(jsonBody[List[FooBar]].description("All of the foobars"))
      .errorOut(errorBody)

Let's examine the type signature with the help of the docs (some copy-paste involved):

type Endpoint[A, I, E, O, R] = ???
type PublicEndpoint[I, E, O, -R] = Endpoint[Unit, I, E, O, R]
  • A is the type of security input parameters
  • I is the type of input parameters
  • E is the type of error-output parameters
  • O is the type of output parameters
  • R are the capabilities that are required by this endpoint’s inputs/outputs, such as support for websockets or a particular non-blocking streaming implementation. Any, if there are no such requirements.

That is rather a lot of type parameters! but also something I think you can get used to quickly when you read PublicEndpoint[Unit, Error, List[FooBar], Any] as

an endpoint with no auth, no inputs, errors of type Error, returning a List[FooBar] with no specific requirements on runtime behaviour (such as streaming bodies).

This is also not more complex (and arguably less so) than the previous definitions, just more verbose because we've declared everything upfront instead of gradually.

Our whole foobar service (with 3 endpoints) is declared as

  sealed trait Error
  object Error {
    case object EmptyMetadata extends Error
    case class NotFound(id: UUID) extends Error
  }

  val uuidBody = stringBody.map(UUID.fromString(_))(_.toString)
  val errorBody = jsonBody[Error]

  val addFooBar: PublicEndpoint[(FooBar, String), Error, UUID, Any] =
    endpoint.post
      .in("foobar" / "add")
      .in(jsonBody[FooBar].description("The foobar to add"))
      .in(header[String]("X-Foobar-Meta").description("The metadata to add"))
      .out(uuidBody.description("identifier"))
      .errorOut(errorBody)

  val getFooBar: PublicEndpoint[UUID, Error, FooBar, Any] =
    endpoint.get
      .in("foobar" / path[UUID])
      .out(jsonBody[FooBar].description("The foobar, if found"))
      .errorOut(errorBody)

  val listFooBars: PublicEndpoint[Unit, Error, List[FooBar], Any] =
    endpoint.get
      .in("foobar")
      .out(jsonBody[List[FooBar]].description("All of the foobars"))
      .errorOut(errorBody)

And because tapir supports a Redoc interpreter, we also have an OpenAPI spec with a web UI running with the line (ignoring the few more lines required to bring up an actual web server):

RedocInterpreter().fromEndpoints[RIO[FooBarService.Environment, *]](FooBarServiceEndpoints.All, "foobar service", "0.0.1")

Which I think is very neat and means you can put your effort into learning type systems and not YAML schemas for OpenAPI. Or rather, you are here already, so you probably don't need to learn more about type systems 😂

There is one final detail, namely how the server "logic" is attached to an endpoint definition. This is straightforward, the I (input) type is converted to an Either[E, O] where E is the error type and O is the output type. For the GET /foobar endpoint, this means a function signature of Unit => F[Either[Error, List[FooBar]]].

The type complexity comes from trying to shove the ZIO[_, _, _] type into the F[_] signature of the effect type. The choice of F[_] with one parameter is natural only because such types are pervasive (think Try, Future, IO) across the ecosystem, but a RIO[R, *] is equivalent (and also the same as ZIO[R, Throwable, *]). The * is the kind-projector syntax we have to use in Scala 2 to have an anonymous type so that we do not have to write out the named type RIORA[A] = RIO[R, A] to make the RIO[_, _] "fit" into F[_]. Simples! ...

It looks like this (repeating some definitions from earlier):

type Environment = Any // not actually Any

object FooBarServiceEndpoints {
  val listFooBars: PublicEndpoint[Unit, Error, List[FooBar], Any] =
    endpoint.get
      .in("foobar")
      .out(jsonBody[List[FooBar]].description("All of the foobars"))
      .errorOut(errorBody)
}

object FoobarService {
  def list(): ZIO[Environment, Error, List[FooBar]] = ZIO.dieMessage("implement the service logic") 
}

val endpointWithLogic: ServerEndpoint[Any, RIO[Environment, *]] = 
  FooBarServiceEndpoints.getFooBar.serverLogic(FooBarService.get(_).either)

That .either at the end changes ZIO[R, E, A] into a ZIO[R, Nothing, Either[E, A]] which is what Tapir expects to have. This is actually because of the choice of effect type F[_] - it only has a single hole for the result type, so the only way to represent an explicitly typed error is to have F[Either[Error, Success]]. The first Any type in ServerEndpoint is the stream/whatever capability which is unused here.

Implementing a Backend

Most of the backends that Tapir supports are backed by real HTTP server implementations, and they typically support features like WebSockets and streaming bodies which are not interesting for our message-based gateway (though I wonder if you could shoehorn the web socket protocol into the message queue we are using).

The message representation that I am using for this example project is as follows:

case class Message(headers: Map[String, String], body: Array[Byte]) {
  lazy val method = Method.unsafeApply(headers("X-Request-Method"))
  lazy val uri = Uri.unsafeParse(headers("X-Request-URL"))
}

case class MessageResponse(inner: Message)

The method/uri fields are niceties so the code stays compact (and reuse the sttp/tapir parsers). They could live in the message to ServerRequest converter too.

After looking at the existing implementations of backends, there are a few pieces that need to be implemented:

  1. How to go from our Message to the Tapir ServerRequest representation that deals with headers, url parameters etc.
  2. How to go from the response body back to MessageResponse (called ToResponseBody in Tapir)
  3. How to go from a list of ServerEndpoint to a handler that can process Message => F[MessageResponse]

All of these are implemented in MessageTapirBackend.scala and explained below.

Message => ServerRequest

The first is straightforward - we need to implement the methods on ServerRequest using the Message:

case class MessageServerRequest(message: Message, attributes: AttributeMap = AttributeMap.Empty) extends ServerRequest {
  override def protocol: String = "HTTP/1.1"
  override def connectionInfo: ConnectionInfo = ConnectionInfo(None, None, None)
  override def underlying: Any = message
  override def pathSegments: List[String] = message.uri.pathSegments.segments.map(_.v).toList
  override def queryParameters: QueryParams = message.uri.params
  override def attribute[T](k: AttributeKey[T]): Option[T] = attributes.get(k)
  override def attribute[T](k: AttributeKey[T], v: T): ServerRequest = copy(attributes = attributes.put(k, v))
  override def withUnderlying(underlying: Any): ServerRequest = 
    MessageServerRequest(underlying.asInstanceOf[Message], attributes)
  override def method: Method = message.method
  override def uri: Uri = message.uri
  override def headers: Seq[Header] = message.headers.map((Header.apply _).tupled).toSeq
}

Notes:

  • there is no connection-level or protocol information, so we default to 'HTTP/1.1'
  • the path segments, query parameters, other headers are extracted from the underlying Message from the broker
  • the AttributeMap is a sttp/tapir construct to add the ability to tag requests/responses e.g. for telemetry/tracing purposes
  • there is no request body handling

The request body handling is treated separately (presumably to accommodate streaming bodies), as follows:

class MessageRequestBody[R]() extends RequestBody[RIO[R, *], NoStreams] {
  override val streams: capabilities.Streams[NoStreams] = NoStreams
  override def toStream(serverRequest: ServerRequest): streams.BinaryStream = throw Unsupported.Streams
  
  override def toRaw[Req](serverRequest: ServerRequest, bodyType: RawBodyType[Req]): RIO[R, RawValue[Req]] = {
    val underlying = serverRequest.underlying.asInstanceOf[Message]
    bodyType match {
      case RawBodyType.StringBody(charset) => ZIO.succeed(new String(underlying.body, charset)).map(RawValue(_))
      case RawBodyType.ByteArrayBody       => ZIO.succeed(underlying.body).map(RawValue(_))
      case RawBodyType.ByteBufferBody      => ZIO.succeed(ByteBuffer.wrap(underlying.body)).map(RawValue(_))
      case RawBodyType.InputStreamBody     => ZIO.succeed(new ByteArrayInputStream(underlying.body)).map(RawValue(_))
      case RawBodyType.FileBody            => ZIO.die(Unsupported.Files)
      case _: RawBodyType.MultipartBody    => ZIO.die(Unsupported.Multipart)
    }
  }
}

Notes:

  • NoStreams is a pre-defined capability that indicates a lack of streaming support
  • RawBodyType/RawValue are defined in the library
  • the .underlying is Any so we have to explicitly cast :sadface:
  • there are different bodies we can get internally, and we deal with strings, byte arrays, NIO byte buffers and InputStreams
  • file/multipart bodies do not make sense for our use-case (no large payloads)
  • I used ZIO.die instead of ZIO.fail because really if something is unsupported it should fail the whole server

ToResponseBody[MessageResponse, _]

The second (going from the tapir representation back to a MessageResponse) is also fairly mechanical:

class MessageResponseBody() extends ToResponseBody[MessageResponse, NoStreams] {
  override def fromRawValue[Resp](
      v: Resp,
      headers: HasHeaders,
      format: CodecFormat,
      bodyType: RawBodyType[Resp]
  ): MessageResponse = {
    val arr = bodyType match {
      case RawBodyType.StringBody(charset) => v.asInstanceOf[String].getBytes(charset)
      case RawBodyType.ByteArrayBody       => v.asInstanceOf[Array[Byte]]
      case RawBodyType.ByteBufferBody      => v.asInstanceOf[ByteBuffer].array()
      case RawBodyType.InputStreamBody     => v.asInstanceOf[InputStream].readAllBytes()
      case RawBodyType.FileBody            => throw Unsupported.Files
      case _: RawBodyType.MultipartBody    => throw Unsupported.Multipart
    }

    MessageResponse(Message(mkHeaders(headers), arr))
  }

  private def mkHeaders(headers: HasHeaders): Map[String, String] = headers.headers.map(h => h.name -> h.value).toMap

  override val streams: capabilities.Streams[NoStreams] = NoStreams
  override def fromStreamValue(
      v: streams.BinaryStream,
      headers: HasHeaders,
      format: CodecFormat,
      charset: Option[Charset]
  ): MessageResponse = throw Unsupported.Streams
  override def fromWebSocketPipe[REQ, RESP](
      pipe: streams.Pipe[REQ, RESP],
      o: WebSocketBodyOutput[streams.Pipe[REQ, RESP], REQ, RESP, _, NoStreams]
  ): MessageResponse = throw Unsupported.Streams
}

Notes:

  • again, Any that needs to be cast to the right type
  • about half the boilerplate comes from having to not implement the websocket/streaming support
  • there is a similar RawBodyType pattern match to get the type of the body given back, but this time no RawValue wrapper
  • we additionally have to put the headers back (which means the conversion from and to a message is not symmetric)

Message => F[MessageResponse]

And lastly, to tie it all together we want to implement the interpreter function that can go between the input and output messages:

final case class MessageTapirBackend[R](endpoints: List[ServerEndpoint[Any, RIO[R, *]]]) {

  private[this] implicit val monad: RIOMonadAsyncError[R] = new RIOMonadAsyncError[R]()
  private[this] implicit val bodyListener: BodyListener[RIO[R, *], MessageResponse] = new MessageResponseListener

  private val interp = new ServerInterpreter[Any, RIO[R, *], MessageResponse, NoStreams](
    serverEndpoints = FilterServerEndpoints(endpoints),
    requestBody = new MessageRequestBody,
    toResponseBody = new MessageResponseBody,
    interceptors = Nil,
    deleteFile = _ => ZIO.die(Unsupported.Files)
  )

  def handle(in: Message): RIO[R, RequestResult[MessageResponse]] =
    interp.apply(MessageServerRequest(in))

}

Notes:

  • the RIOMonadAsyncError actually comes from a sttp package rather than a tapir package
    • there are no other implementations for ZIO[R, E, A]
    • it is necessary because tapir does not depend on any other library that defines the basics like MonadError
    • I am so far unconvinced that leaving Throwable as the ZIO error type (with RIO) was the right choice (more on that later)
  • it is unclear why we need the FilterServerEndpoints and why it is not the default
  • I am also unsure in what scenarios we would want to use the MessageResponseListener (that is implemented as a stub) or the interceptors
  • file deletion is unsupported but required in the interpreter

Have a 🍪, because the brain blood sugar level has surely dropped.

Testing

I decided to test directly with the whole foobar application, including the stub repository that stores all the foobars in memory. Here's what that looks like:

  val backend = 
    MessageTapirBackend[FooBarService.Environment](FooBarServerEndpoints.allRIO())

  test("GET /foo should list all foobars")(
     for {
       // 1. set up test data
       _ <- FooBarRepository.store(TestData.Foobar1)
       _ <- FooBarRepository.store(TestData.Foobar2)
       expected <- FooBarRepository.list()
       // 2. run the HTTP method through the backend
       resp <- backend.handle(TestData.ListMessage).flatMap(toResponse)
       // 3. get data out
       got <- parseJson[List[FooBar]](resp.body.get.inner.body)
     } yield {
       assertTrue(
         expected.size == 2,
         expected == got,
         resp.code == StatusCode.Ok,
         resp.header("Content-Type").contains("application/json")
       )
     }
  ).provide(FooBarRepository.stubLayer)

  // ....
  def toResponse[T](res: RequestResult[T]): Task[ServerResponse[T]] =
    res match {
      case RequestResult.Response(r) => ZIO.succeed(r)
      case RequestResult.Failure(failures) =>
        ZIO.fail(new RuntimeException(s"failures detected: ${failures.mkString(",")}"))
    }
  
  def parseJson[T: Decoder](bytes: Array[Byte]): Task[T] =
    ZIO.fromEither(parse(new String(bytes)).flatMap(js => js.as[T]))

This is no different from using any other backend except for the message parsing at the end, which I wanted to do by hand to ensure that things are actually happening as expected within the tapir server endpoints. The tests passed on the first go.

Observations

After I figured out the interfaces I needed to implement, it was a matter of solving the type puzzle (also known as type driven development). I am sure there are a significant number of people that would balk at the idea, but in my experience it is a lot simpler than to solve some global mutable state race condition type heisenbug (or indeed, a gradle build issue, which turns into the same thing). It really only requires listening diligently to what the compiler is telling you. I haven't had to change the server backend code when I wrote the tests, though I did have to add type annotations and use .asInstanceOf a few more times than I would have liked.

Using tapir for this exercise, I was glad that only about 120 lines had to be dedicated to dealing with the custom message handling. Tapir 1.0 only came out in June 2022, so I expect that a 2.0 will at some point have a lot more ergonomics (and safety, by dropping the use of Any) improvements as time goes on and the design is refined. The only thing I am slightly worried about is the use of RIO instead of URIO as the F[_]. I think it means that any exceptions thrown outside the server endpoints' Either[Error, Success] return type will go to a default exception handler rather than being the end of the server, and as by definition these are really exceptional, it feels that they should go to "defect"/"die" side of the ZIO error channel. I will try and clarify with the maintainers if my understanding is somehow incorrect.

Scala 2 is also showing its age. Unfortunately we are still behind on the Scala 3 migration, but there were a few type inference bugs that I encountered and really cryptic error messages that I think I wouldn't have seen in the new version.

So, to summarise: it took about 120 lines to implement a custom backend for tapir that allowed handling arbitrary message types as HTTP endpoints, and a couple of hours to integrate this all together (plus a good few hours to write down the experience). The typing complexity is probably quite high though - you are not only dealing with the ZIO types, but also with the Tapir endpoint definitions, the "capabilities", and the F[_] as a ZIO[_, _, _] conversion. I hope that this post demonstrates that there is no magic involved and that such an approach is flexible for any type of messaging system that supports messages with bodies and header key/values.

We gained the following from this approach:

  • A clear separation between the protocol (HTTP), the business logic and the transport layer
  • A precise description of behaviour for our endpoints
  • Some OpenAPI specs
  • A client implementation (should we decide to write integration tests going through the HTTP gateway for our system)
  • A second server implementation using a real HTTP server that we could use for local testing (since we probably don't want to run the gateway on our dev machines all the time)

On balance, I would call that a win. Code is here - I hope you find it useful and/or educational.

Smol blog refresh

The blog has been refreshed slightly:

I'm generally in the process of migrating the non-github (forks, stars) content off gitlab and on to Sourcehut. I find the interface refreshingly minimalistic, the ideology agreeable and some of the features (like strict CSP) good for keeping the web open and tracker-free.

Linux Desktop Tweaks in 2021

Prelude

I have been using Linux since secondary school, with the first distro likely being Red Hat 9. I quickly moved to try others, like SUSE Linux 9, which I remember offering a much nicer GUI interface (KDE 3). Being a schoolkid, I had a lot of time on my hands to just explore the environment and it wasn't long until I stumbled upon Gentoo in 2004. I'm not so sure now that hours of compiling kernels to suit your exact hardware configuration (on a 500Mhz Pentium 3 Coppermine speed beast) was a typical pursuit of fun, but I feel like I learnt a lot from this period.

In this article, I want to share some of my current tips regarding my setup.

Current desktop setup

I have a number of PCs that are both work devices and some personal devices as well. My distro of choice is currently Fedora Workstation. I find it to be a good balance between having the latest kernels (and fixes for newer hardware that come with them), release cadence (every 6 months) and stability. Having once found rebuilding Gentoo packages every day to be unproductive (true story!), I tried switching to both Arch and openSUSE Tumbleweed (both are rolling release distros). Things still broke way too frequently so I had to switch to a more stable alternative. During a couple of work projects I got a lot of experience building RPMs for RHEL 6/7, and Fedora, being the eventual upstream of RHEL releases seemed like a sensible choice.

I recently upgraded to Fedora 33 - surprisingly this cause a few small things that broke and this motivated me to write a post.

I use two desktop environments - sway and lxqt. (Screensharing is still a weak point in Wayland, so I drop down to LxQt for that)

sway + supporting programs

Sway is a tiling window manager with almost complete compatibility with i3wm, but using Wayland instead of X11. The list of utility programs is small and fairly common:

  • bemenu - text program launcher
  • i3status-rust - statusline showing things like load average, temperature, battery information
  • kanshi - display profile configuration (e.g. if you are docked, have left and right screen at correct scaling)
  • mako - notification daemon
  • rot8 - screen rotation daemon (for 360-style devices)

systemd-resolved and name resolution

In Fedora 33, the default DNS management switched to systemd-resolved. This broke programs running in Wine for some reason, and I decided to skip using it for now. My main home office network has a DNS server that I trust, and for life on the road I choose to use the VPN provider's DNS server exclusively. I feel that for me, there is no benefit in having yet another DNS layer to configure, so I decided to revert.

However, life is no longer as simple as editing /etc/resolv.conf because certain software (like wg-quick) expects to see the resolvconf executable in $PATH, so the steps I took are:

  1. Edit /etc/NetworkManager/NetworkManager.conf
    • In the [main] section, add dns=default to bypass systemd.
  2. systemctl disable systemd-resolved
  3. Install openresolv to get a resolvconf executable
    • I didn't find a package so just cloned the git repo and make install DESTDIR=/usr/local

sharing the PulseAudio daemon between users

I use a couple of different users locally, and sometimes it is very useful to hear sounds from multiple sessions at once. The default configuration starts a pulseaudio daemon per user, and the output device becomes locked to whichever user started using it first. This is sensible from a security standpoint but a single-human, multi-user scenario has different needs.

Pulseaudio is usually activated via a socket when a program needs to use audio output (I am not sure exactly by which mechanism), but we can start it directly when sway is started:

  1. Edit your ~/.config/sway/config to start pulseaudio
    • exec pulseaudio -D
  2. Edit /etc/pulse/client.conf with
    • default-server = unix:/tmp/pulseaudio-socket

This will make clients look for the unix socket and talk to the single server (assuming your sway session is started first). This is still not ideal, but I feel this is better than allowing a pulseaudio daemon systemwide.

fixing gpg-agent and yubikey permissions

My git commits are signed with a GPG key that is generated on a Yubikey. The key seemed to be detected by the agent and gpg2 but signing did not actually work. After some research, I came up with the following:

  1. Remove OpenSC with dnf remove opensc
  2. Stop and disable pcscd (this will be problematic if you have other smartcards for example):
    • systemctl disable pcscd.service
    • systemctl disable pcscd.socket

The closest description to the issue seems to be bug #1893131 so I hope it will be resolved in the future.

disabling text-to-speech daemon

Text-to-Speech is enabled by default, which is a nice touch should you need it, but it's a daemon potentially taking up resources, so I disabled it via systemd disable speech-dispatcherd.service. Check

disabling avahi/zeroconf

I saw some CPU spikes from the zeroconf daemon avahi, so I disabled it via systemctl disable avahi-daemon.socket and systemctl disable avahi-daemon.service. A network that has proper routing infrastructure in place has no need for it, so I'm happy to disable it.

enabling persistent storage of logs

See this gist for how to configure systemd-journald if the storage directory is not yet setup.

enabling hardware accelerated video decoding in firefox/wayland

Since June 2020, you can enabled it via some config changes in about:config.

make moby/docker work again

The currently shipping version of moby (19.03.13) at the time of writing does not support CGroups v2. You have to prevent systemd from creating them by appending systemd.unified_cgroup_hierarchy=0 to the kernel cmdline:

  • grubby --args="systems.unified_cgroup_hierarchy=0"

Conclusion

Linux on the desktop is a pretty smooth experience for experienced users these days (compared to a decade ago when you had a much bigger minefield to navigate), though giving a Gentoo installation to your grandmother may be premature still. Wayland makes navigating mixed-DPI environments relatively painless, and in general the UI interface feels a lot smoother than in the X11 days. There are niggles that are being worked on, such as the missing screen sharing support (in Chrome for example here)

As a software developer targeting mainly backend systems running on Linux, the lack of friction when developing on the same platform as the deployment target is clear. As a personal user, the amount of control and finetuning that can be applied to a system that (mostly) works out of the box is phenomenal. The tuning does not disappear between releases (hello Big Sur) and telemetry is not randomly reenabled between patch updates (hello Windows 10). Fedora so far has been the closest to things mostly just working while staying close to the edge of development.

Why Scala?

A hand drill may not always be the right tool

Why am I writing this?

Clients, recruiters and fellow software engineers usually ask what makes it "better" than the incumbent industry language - Java. I get a lot of questions from people that perhaps have not encountered Scala before asking why they would use it over something that most people (for some definition of most that probably includes Computer Science graduates) are already familiar with. To make the (hopefully only) analogy - if you have a hand drill that works, why would you want a power tool? One answer could be scale and convenience - you can build a fine piece of furniture with a hand drill, but if you need to make many holes in a concrete wall, it is much more practical to use a power drill with a hammer function.

This article will attempt to explain why I prefer Scala over Java for most tasks where one could pick either. The usual disclaimer applies - it's just one (albeit very powerful) set of tools in the programmer's toolbox, and will not help you be better at anything if you use it incorrectly (such as drilling in the wrong place or with the wrong technique. Ok, analogy_count++). I will start at a very basic technical level and increase the level of detail as the article continues.

TL;DR;

Scala can be a very productive language to develop in - a business domain can be modelled consisely (using case classes), complex problems can be broken down into simpler pieces (using function composition and pattern matching) and some classes of errors can even be eliminated at compile time (using typeclass derivation as an example). It is indeed a more complicated language than Java but overall I think the gains are worth the initial ramp-up time.

Language background

As an introduction - Java is a general-purpose object-oriented programming language first released in 1995, with a static type system that includes parametric polymorphism and inheritance, targeting a runtime system with garbage collection and a bytecode VM (the Java Virtual Machine or JVM). Scala is a multi-paradigm general-purpose programming language (object-oriented and functional) first released in 2004, with a static type system (that has all the features of Java's and more), targeting primarily the JVM, but also the web (with Scala.js) and native code (with Scala Native). It is telling that IDEs offer support for automatic conversion of Java to Scala but not the other way round.

The above paragraph contained quite a bit of jargon - let me try to define some of the terms:

functional:

ability to treat functions as a first-class value; also the general term for programming techniques grouped around that idea, such as: immutability, parametricity, currying

inheritance:

ability to get the behaviour and data (methods and fields) of parent class (superclass) by a single syntactic keyword (usually extends)

garbage collection:

memory is allocated dynamically at runtime and is freed by collection at some later point dynamically determined at runtime (in contrast with statically allocated memory where both the lifetime and sizes are known at compile time)

general-purpose:

equally applicable to all domains (i.e. nothing specific to make video games vs. text editors for example)

object-oriented:

able to associate data (fields) with code (methods), usually using a convenient syntax

parametric polymorphism:

able to write a function that can use the same code to perform an operation across different types (like an addition operator of a semigroup e.g. the natural numbers)

statically typed:

every value has a specific type known at compile-time (e.g. you can differentiate between a integer and a piece of text)

Terms that will be used later are not going to be explained but only linked to definitions or examples elsewhere (to reduce the scope of the article).

What features in Scala help the most?

As long as two languages are Turing Complete any programs written in one language can be written in another. However, this is not very useful - it may be far more appropriate to write a program in C vs. a program in Javascript depending on factors like tooling, ecosystem support, the environment where the program is going to run, the amount of resources available to the program etc.

Both languages run on the JVM, and both are used for backend development in small to large companies. To usefully compare Scala to Java we need to talk about specific features, my selection of which is:

  • case classes
  • pattern matching
  • implicits (given in Scala 3)
  • macros (mostly library usages though)

Case classes

and Algebraic Data Types

A case class is a container for immutable fields and (optionally) other behavior. (In Java terminology, they are value-based classes). For example, suppose you have a bus position you want to represent that contains the following information:

  • bus identifier (and maybe line, initial departure time, vehicle id)
  • coordinates of bus
  • timestamp

You might initially model everything using String (example in JSON):

{
    "bus": "42",
    "coordinates": "0.0,0.0",
    "timestamp": "2019-01-01T12:23:22.813Z"
}

And in Scala:

final case class BusPosition(bus: String, coordinates: String, timestamp: String)

val bus = Bus("0.0,0.0", "42", "2019-01-01T12:23:22.813Z")

println(bus.bus)
println(bus.coordinates)

At first, it seems fine to use String everywhere, but there's a bug in the above code. (The mistake is known as stringly-typed code and is not specific to either Scala or Java). Let's try again:

import java.time.Instant

final case class TransportLine(name: String) extends AnyVal
final case class VehicleId(id: String) extends AnyVal
final case class Coordinates(latitude: Double, longtitude: Double)
final case class BusVehicle(id: VehicleId, line: TransportLine, initialDeparture: Instant)
final case class BusPosition(bus: BusVehicle, coordinates: Coordinates, timestamp: Instant)

val bus = BusPosition(
    BusVehicle(VehicleId("01fa3-a35"), TransportLine("42"), Instant.of(2019, 1, 1, 12, 15)), 
    Coordinates(0.0, 0.0), 
    Instant.of(2019, 1, 1, 12, 23, 22, 813)
)

The above:

  • is very concise (all definitions in one place)
  • has a different type for every field in the BusPosition class (which also makes the type definition easier to read without having to read the names)
  • defines by virtue of being a case class: .equals, .hashCode and .toString as well as .copy which allows you to make a copy with only specific fields changed.

The extends AnyVal is known as a value class and allows to define a class with a single field that is not actually allocated at runtime. It allows us to clearly say what we mean when we write "42" for example - it is a transport line, but without any overhead except to declare the type and use it.

How to represent this in Java? There are still at least two camps about how to define an equivalent in Java - either with a series of classes with getters and setters, or with immutable fields as above.

With immutable fields, it would look like (split across several files):

import java.time.Instant

final class TransportLine {
    public final String name; 
}

final class VehicleId {
    public final String id;
}

final class Coordinates {
    public final Double latitude;
    public final Double longitude;
}

final class BusVehicle {
    public final VehicleId id;
    public final TransportLine line;
    public final Instant initialDeparture;
}

final class BusPosition {
    public final BusVehicle bus;
    public final Coordinates coordinates;
    public final Instant timestamp;
}

This is far less concise than Scala, and we still haven't defined object equality (so you can't use this representation if you want to compare two BusPosition in tests without defining equals yourself or getting the IDE to generate it for you). If you want to use the getters/setters style, then you have to write even more code, and lose immutability as a result. (Immutability is not just important for reasoning about the code, but also for safe publication in the presence of concurrency). You can also use Project Lombok, which will instrument your code at runtime, but this doesn't come as a built-in language feature.

Scala field accesses are actually using compiler-generated getters for uniform field/getter access, which means that if you define:

final case class Coordinates(latitude: Double, longtitude: Double)
final case class BusPositionLatLon(lat: Double, lon: Double)
final case class BusPositionCoordinates(coordinates: Coordinates) {
    def lat: Double = coordinates.latitude
    def lon: Double = coordinates.longitude
}

val posA = BusPositionLatLon(1.2, 3.4)
val posB = BusPositionCoordinates(Coordinates(1.2, 3.4))

posA.lat == posB.lat
posA.lon == posB.lon

Then you have a uniform access syntax to both fields and methods defined without parens (the convention is to typically leave out the parentheses if the method is not performing any side effects).

But this is just the basics (pattern matching is covered in the next section). Typically you have a situation where you want to model a case where you have a common type but different specific instances. Let's take the simplest (perhaps not the most illuminating example) of Optional

  • where you have either:

  • something containing a value

  • nothing without any value

In Java, this is modelled as a single class Optional. In Scala, this is modelled with the abstract class scala.Option which has a subclass Some for the case with a value, and a subclass None without. Actually the definition is a little more complex:

package scala

sealed abstract class Option[+A] extends Product with Serializable { ... }

final case class Some[+A](value: A) extends Option[A]
case object None extends Option[Nothing]

In the above:

  • + is a variance annotation that implies Option is covariant
  • sealed keyword tells the compiler that all the subclasses of Option are known at compile-time
  • case object None defines a singleton object that is an Option[Nothing]. This can be a singleton because Option is covariant, and Nothing is the bottom type (a subtype of every other type)

Why is the definition like it is and why is it better than Java's Optional?

We now have two concrete types: Some and None, and there are no other subtypes of Option. This means Option meets the definition of a Algebraic Data Type (or ADT for short), specifically a coproduct. The fact that Option is sealed allows us to do pattern matching exhaustively. This gives enormous type safety that simply isn't available in Java with instanceof. With ADTs we also have a uniform way of treating any type that is defined with sealed uniformly, which is a win for being able to reason about code.

With Optional, we cannot determine whether a value exists without calling the .isDefined or .isEmpty method, and certainly not by examining the types. But the example above is just one example - there are many instances where we would like to have compile-time certainty about handling all the cases of something. It is possible to use ADTs in Java with libraries like dataenum.

Pattern matching

Perhaps the case for case classes wouldn't be so strong if Scala was lacking another critical feature: pattern matching. Pattern matching is not just a safer version of instanceof in Java, it also allows to match on fields inside case classes, to do name binding, on values or to write guards for specific conditions. From the excellent Neophyte's Guide to Scala, patterns everywhere chapter:

final case class Player(name: String, score: Int)

def printMessage(player: Player) = player match {
  case Player(_, 0) => println("Try harder ;)")
  case Player(_, score) if score > 100000 => println("Get a job, dude!")
  case Player(name, _) => println(s"Hey $name, nice to see you again!")
}

In the above example:

  • destructuring the case class Player by putting name bindings to fields of Player, and _ underscore otherwise
  • used literal value 0 to distinguish the case where the score was zero
  • used a pattern guard to check for high scores

This could have been written in Java as follows:

public void printMessage(Player player) {
    if (player.score == 0) {
        println("Try harder ;)")
    } else if (player.score > 10000) {
        println("Get a job, dude!");
    } else {
        println("Hey " + player.name + ", nice to see you again!");
    }
}

And given the above was a pretty simple example, the code looks very similar.

But what about the case when ADTs are involved?

For example, if we want to model the bus location problem again, but now we have other types of transport:

final sealed trait TransportVehicle {
    def location: Coordinates
}

final case class Bus(name: String, location: Coordinates, sittingCapacity: Int, standingCapacity: Int) extends TransportVehicle
final case class Car(licensePlate: String, location: Coordinates, numSeats: Int) extends TransportVehicle
final case class RidesharingVehicle(company: String, id: Long, location: Coordinates, capacity: Int) extends TransportVehicle
final case class Tram(name: String, location:  Coordinates, sittingCapacity: Int, standingCapacity: Int) extends TransportVehicle

def findVehicleForThreePeople(vehicles: List[TransportVehicle]): Option[TransportVehicle] = vehicles.find {
    case bus: Bus               => bus.sittingCapacity + bus.standingCapacity >= 3
    case car: Car               => car.numSeats >= 3
    case rs: RidesharingVehicle => rs.capacity >= 3
    case tram: Tram             => tram.sittingCapacity + tram.standingCapacity >= 3
}

The above function is still a little simplistic (hard-coded constraints, no interface for capacity calculation) but it is still:

  • Clear and concise - the business logic (seats >= 3) is encoded in one place without any unnecessary detail
  • Exhaustive - if you add a new vehicle type, the compiler will warn that you have a new case to handle

In Java, this would look like:

public Optional<TransportVehicle> findVehicleForThreePeople(List<TransportVehicle> vehicles) {
    return vehicles.stream().filter(vehicle -> {
        if (vehicle isinstance Bus) {
            Bus bus = (Bus)vehicle;
            return bus.sittingCapacity + bus.standingCapacity >= 3;
        } else if (vehicle isinstance Car) {
            Car car = (Car)vehicle;
            return car.numSeats >= 3;
        } else if (vehicle isinstance Tram) {
            Tram tram = (Tram)vehicle;
            return tram.sittingCapacity + tram.standingCapacity >= 3;
        }
    }).findAny();
}

Did you spot the missing case above? The compiler didn't. At least there is a proposal for pattern matching in Java now.

Update Feb 2021: And now pattern matching is going to be released in Java SE 16

Implicits/given

given is a new mechanism coming in Scala 3 that improve upon the shortcomings of implicits in Scala 2.

Implicits are I think the feature in Scala that polarizes opinions the most. You either love it for the boilerplate reduction or hate it because of the perceived complexity and arcane rules of resolution (or because someone in your old project went wild with implicit conversions and made a mess).

There are 3 uses of implicit in Scala 2 at the moment:

  • Implicit classes, which allow you to declare extension methods on classes that you either didn't define yourself or are not able to change for backwards compatibility reasons
  • Implicit conversions, which allow the compiler to auto-magically convert a value from one type to another
  • Implicit parameters, which allow the compiler to inject an instance of a given class at compile-time, based purely on the imports and the implicit scoping rules

The details have been widely covered: Implicit Design Patterns in Scala or Implicits, type classes, and extension methods to give just two good posts.

For me, implicits are a great way to change run-time problems into compile-time problems (eliminating errors earlier) - especially when doing dependency injection or encoder/decoder definition.

Let's the take serialization example for JSON. In Java, you typically have a runtime mechanism for that using a library like Jackson that is reflection-based. If you look at some common Jackson exceptions, you can see things like:

  • JsonMappingException: Can not construct instance of
  • JsonMappingException: No suitable constructor
  • JsonMappingException: No serializer found for class

These can disappear if instead you use a library like circe which can do the work at compile-time:

// model definition
sealed trait SiteUser
final case object Anonymous extends SiteUser
final case class LoggedInUser(id: Int, name: String) extends SiteUser

// json protocol definition

import io.circe.generic.extras.Configuration
import io.circe.generic.extras.semiauto._

implicit val configuration: Configuration = Configuration.default.withSnakeCaseMemberNames.withDiscriminator("type")

implicit val siteUserEncoder: Encoder[SiteUser] = deriveEncoder

// usage
import io.circe.syntax._

val anon: SiteUser = Anonymous
val loggedIn: SiteUser = LoggedInUser(42, "bob")

println(anon.asJson)
println(loggedIn.asJson)

The above will print:

{
  "type" : "Anonymous"
}
{
  "id" : 42,
  "name" : "bob",
  "type" : "LoggedInUser"
}

If you add more cases to SiteUser or more fields, the derived encoder will keep compiling unless the fields have no encoder already defined for them. The resolution is done at compile-time because implicit is the mechanism under the hood. This guarantees that you will never get a runtime error unless the JSON itself is invalid (which is handled by all the fallible conversions returning an Either[Error, A] ADT).

Macros

Another advanced mechanism that Scala offers for generating code at compile-time is macros (scalameta). You typically won't use the feature directly unless you're a library author, so why is it relevant here?

Anecdote: at one of the projects recently I had a case where the JSON structure of an upstream system and the BigQuery database structure was identical (and was guaranteed to stay identical). To avoid duplicating code (and writing lots of error-prone manual mapping), I decided to reuse the case classes from the JSON world in the database mapping. BigQuery naturally only returned Map[String, Any] so I had to find a way to automatically populate the relevant case class. This was a perfect case for generic derivation (see type classes and generic derivation for a more complete example).

I wanted to have a trait (typeclass) that looked like:

trait Decoder[A] {
    def decode(bigQueryResult: BQ): Either[Error, A]
}

But not have to write it manually for each case class I wanted to query. Luckily, the magnolia library made it fairly straightforward to create a derivation for that, and I had a working example I could use within two days (for a fairly complex data type with lots of nested lists, maps, etc.). The initial time investment probably was similar to writing the manual mapping by hand, but since the investment is generic over any case class type, no mapping would need to be done manually in the future, saving time and reducing errors.

There is no direct equivalent of a macro library for Java - there is asm which does bytecode manipulation, but that is a lot lower level and magnolia supports coproducts and case classes which do not exist as high-level concepts in Java.

Summary

Scala for me offers both simple and advanced mechanisms to manage complexity (via ADTs, case classes and pattern matching), eliminate run-time errors by pushing checks to compile-time and conciseness. The reduction in code is significant because it both reduces bugs (no bugs in code that isn't written) and allows you to get up-to-speed with a new codebase quicker (there is less to read). The functional style of programming and the typelevel ecosystem of libraries make constructing software safer (at compile-time even before tests are involved) and more productive (due to the high level of genericity and low level of boilerplate). Functional style code typically allows for better local reasoning which is easier on the brains of developers and code reviewers.

I also ommitted a lot of smaller feature that Scala has such as for comprehensions, companion objects, type aliases, as well as patterns when writing scala code like typeclasses.

One disadvantage is that it will not take one day or even a week to get to know Scala well. I also feel that prototyping a new service when requirements are changing quickly is also quite difficult due to the static typing - sometimes it is easier to let everything be a string or a map, in which case a more dynamic language like Python or Clojure may be quicker to use initially.

Please leave comments or take a look at my example-pure-todomvc for more concrete example of what a Scala microservice that implements TodoMVC looks like.

Update Feb 2021: Have a look at the excellent bootzooka project, that offers up-to-date and comprehensive template to start building a microservice from

scala.world 2016 conference notes

Scala world 2016

Scala Hikers @ Scarfell Pike - see here for more

I attended the scala.world conference that took place in the Lake District between the 12th and 13th September 2016. These are some notes on the conference, mainly for the benefit of my colleagues but also as a convenient way to start off the SOLID Ninja blog.

The hiking

Notes on scala.world would not be complete without mentioning the hiking that took place over the preceding weekend. The Lake District weather gods were favourable throughout the conference which was unusual to say the least (at least if you live in England). On the Saturday there was an organized hike to Scarfell Pike which is the tallest point in England and on Sunday there was a choice of cycling and hiking again. Needless to say even the "easy" route does not look like a bike ride through the Netherlands.

The hiking was a great way to meet people and talk about life rather than SI-2712.

The conference

The schedule was finally up on Monday morning about 4am - so the conference could begin!

In no particular order, these are the talks/workshops I made notes on:

Martin's Keynote

Martin Odersky was up first with a keynote titled "Compilers are (in memory) databases" (which admittedly has been given before so check below for the YouTube video). It is very nice to see that dotc has got lots of small, reasonable passes in the compiler rather than a few big monolithic passes.

watch on youtube

CBT

CBT is in my opinion the S(imple)B(uild)T(ool) that should have been - Christian did a great talk explaining how cbt emerged from the design of SBT. The slides are not yet up - here is the best I could do:

CBT looks very promising because:

  • The model is much simpler than SBT so you do not need to know about scope axes
    • I doubt anyone will be writing a book about cbt though.
  • It's fast! (both to startup and resolve dependencies)
    • No ivy involvement helps a lot in this respect, though support for coursier is currently not in a good shape
  • There is no DSL - everything is a project and tasks are functions, so you can write your own plugins etc. the same way you always write code (in Scala)

Generic derivation

I did not go to this one, but Travis Brown did an interesting talk on Generic derivation (in Circe, which is a fast json library)

The Type Astronaut's Guide to Shapeless

With a title like that it was going to be hard to disappoint, and Dave Gurnell's talk about shapeless was very good:

The worked example about doing automated case class migration (using HList union and intersection) in particular was both useful and easy to follow.

The type astronaut's guide is currently in development on GitHub.

A whirlwind tour of scala.meta

Ólafur Páll Geirsson presented a workshop on using scala.meta to automatically transform scala programs (in one example, by replacing usages of Throwable with the NonFatal(_) extractor):

watch on youtube

At this point the Internet access at the workshop was not very good, but luckily the workshop is available here: scala.meta-workshop

The Metaprogramming 2.0 talk is worth watching as well.

Talks about the Free Monad

There were a few talks/workshops on the Free Monad which is (and has been) a fairly hot topic in the past year.

Raul Raja presented a talk Run wild, Run free which gives a very natural exploration of the problem of composing your computations starting with for-comprehensions and exploring until you arrive at the free monad.

Pawel Szulc also gave a workshop on free monads: make-your-programs-free. The core idea is that while actually implementing the interpreter you get to a point where you are wrapping higher-level operations with low-level 'plumbing' code. One way of solving this problem is by definining an interpreter from a higher-level algebra to a lower-level algebra and implementing them separately (which you can see if you follow the commits in the workshop).

There were plenty of mentions to libraries that help you with free monads:12 and it would not be fair to not talk about the downsides of Free as well3.

Tuesday Keynote

The keynote on Tuesday was Dick Wall and Josh Suereth talking about all the uses of fors: Use the fors, Luke. It started off a super friendly to beginners and at the end also addressed the reasons why perhaps a for is not the ultimate tool for Monad composition (shhh, that's a lie). Unfortunately slides/video are not up yet.

Managing your Resources

This talk was one of the highlights of the conference for me I think - resource management is one of the problems in Scala that is difficult to solve because of a lack of a lifetime system as found in Rust say. And while there are more elegant ways to close() your resources after yourself, most of the solutions require either some wrapping construct (such as a for or a monad or a simple loan-pattern style closure). It is rare to see such simplicity (in Scala):

watch on youtube

The presentation is available but it's in Keynote format which GitHub does not have a reader for.

Here is a working example from the presentation:

ScalaFiddle

Scala officially entered the stage of languages with an online interactive sandbox, called ScalaFiddle. Play with it - it has decent highlighting, speed and libraries that you can use just by clicking and importing.

Tales from Compiling to the JVM

This was an interesting talk by Lukas Rytz about certain things that had to change in the Scala 2.12 compiler because of Java 8. Like not initializing static final variables outside the class initialization block. Talk is not up yet unfortunately.

Typelevel hackday

The third day of the conference was a Typelevel hackday which started off with a few talks in the morning. Of these I only took some notes for two of them:

  • Cats in London - this was Pawel Szulc attempting the what must be by-now infamous interview problem of the Checkout with apples and oranges using Cats and the Free monad
  • Array-based collections - was a nice talk about a collection library that is very different - these ones are immutable and backed by a single flat array

Other References

  • John de Goes ran an Advanced Functional Programming with Scala training workshop for 2 days prior to the conference and the linked notes look great
  • Monix was mentioned as a library quite often and I was not fully aware of it before coming to the conference

So here is the presentation from flatMap Oslo 2016 talking about it:

watch on vimeo