Introducing Mu-Haskell v0.1

Introducing Mu-Haskell v0.1

Companies have embraced microservices architectures as the best way to scale up their internal software systems, and divide work across different company divisions and development teams. Microservices architectures allow teams to turn an idea or bug report into a working feature or fix in production more quickly. However, microservices are not without costs. Each service needs to include an implementation of the protocol, the encoding of the data for transmission, and deal with metrics, among other things. Many of these needs are shared between most microservices, so it makes no sense to reimplement them every single time.

At 47 Degrees, we have been working for quite some time on Mu, a library to develop microservices with the smallest amount of work possible. The goal is for you to focus on your domain logic, instead of worrying about format and protocol issues. Following good practices, we encourage you to document your services and data schemas in one of the existing Interface Description Languages, such as Avro and Protocol Buffers, streamlining interoperation between systems.

Mu was initially developed in Scala. Today we are happy to announce Mu-Haskell, a set of libraries with the same focus, which exploit the possibilities of the Haskell language and ecosystem! This first release comes with full support for building gRPC microservices, shifts a lot of checks to compile-time schemas, and readily integrates with well-known Haskell libraries for many tasks, including persistence and logging.

Since this is an Open Source project, we tried to improve the Haskell RPC ecosystem along the way and contributed to different OSS libraries as a result, but that is a subject for a future blog post! 😉

What makes Mu-Haskell special?

In essence, Mu-Haskell is a set of packages to build both servers and clients for (micro)services. Following the ideas in its Scala sibling, Mu-Haskell regards schemas as the “source of truth,” that is, the common interface to which all systems in an organization should adhere. Given that focus, we have tried to extract as much juice from it, taking advantage of the type-level techniques in Haskell.

With a Mu-Haskell server, all the known information about your data schemas and your RPC methods is represented in the type level. This is just a fancy way to say that this information is available to the compiler, which can perform additional checks to see that everything fits. For example, if the shared schema now requires an additional field in a record, but your Haskell code has not been updated yet, an error appears the next time you compile your project.

If you have been following the Haskell ecosystem lately, you might have seen this shift towards more compile-time checks. For example, Servant is a library with a similar focus to Mu, but for REST services, in which routes are also represented at type level. On a more mundane task, pathtype ensures that all your filepath manipulation is correct at compile time.

A quick & small example

Let us build a server for a small gRPC service, the one from gRPC Quickstart Guide. The service definition is contained in the quickstart.proto file, written in the Protocol Buffers language. This file declares both the schema for the messages and the signature for the methods in the service:

service Service {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest { string name = 1; }
message HelloReply { string message = 1; }

The entire Haskell code that exposes such a service follows.

grpc "TheSchema" id "quickstart.proto"

newtype HelloRequestMessage
  = HelloRequestMessage { name :: Maybe T.Text }
  deriving ( Eq, Show, Generic,
             ToSchema   Maybe TheSchema "HelloRequest",
             FromSchema Maybe TheSchema "HelloRequest" )
newtype HelloReplyMessage
  = HelloReplyMessage { message :: Maybe T.Text }
  deriving ( Eq, Show, Generic,
             ToSchema   Maybe TheSchema "HelloReply",
             FromSchema Maybe TheSchema "HelloReply" )

sayHello :: (MonadServer m) => HelloRequestMessage -> m HelloReplyMessage
sayHello (HelloRequestMessage Nothing)
  = pure $ HelloReplyMessage (Just "hello, mysterious person")
sayHello (HelloRequestMessage (Just nm))
  = pure $ HelloReplyMessage (Just $ "hi, " <> nm)

main :: IO ()
main = runGRpcApp 8080 $ Server (sayHello :<|>: H0)

There are four main sections in this code. The first one, beginning with grpc, is responsible for reading the .proto file and turning all those declarations into type level information. Since we really want to encourage everybody to use schemas, we intend to provide easy ways to import them. Right now, gRPC service definitions are fully supported, and Avro and JSON Schema are in the works.

The next step is to define some Haskell data type corresponding to the message types in the gRPC definition. Although in some cases those data types can be inferred from the schema itself, we have made the design choice of having to write them explicitly, but check for compatibility at compile-time. The main goal is to discourage making your domain types simple copies of the protocol types. The link between the schema entities and the Haskell types is made by ToSchema and FromSchema. Under the hood, these lines provide conversion to a uniform representation of data, which can later be serialized to many different formats.

In some sense, these first two blocks only declare how the service looks, but not what it does. The simplest way to implement a service is to define one function for each method. You can define those functions completely in terms of Haskell data types; in our case HelloRequestMessage and HelloReplyMessage. However, in gRPC, both requests and responses are serialized using Protocol Buffers, in which all record fields are optional. Thus, we need to handle both the cases in which the name is missing (called Nothing in Haskell), and when it is available. Once again, thanks to Mu type-level encoding, the compiler checks that we have not forgotten any case.

The final two lines just make this file into an executable that exposes your service at port 8080. You are only required to write the implementation of each method in your service here. You can test the service using applications such as BloomRPC.

Uuuuhh, Monads!

To perform its duty, methods may need to query databases, or, in general, talk to other systems. Following common practice, we just allow them to perform any IO action. However, you are encouraged to keep the pure parts of your server separate, and only use IO when required.

In this example, calls to the right method in the server always succeed. But this might not be the case. What if we have to look up the name of the person in a database and that name is missing? In Mu-Haskell, implementations for services may also end in an error condition. MonadServer is a synonym for those monads that support those two sets of capabilities: running IO actions and ending with an error.

Conclusion

As you can see, building services with Mu-Haskell is quite straightforward. Making your schemas available at compile time gives you a boost of confidence (and we plan to get even more out of it). Give mu-haskell a try today (maybe following our intro guide) and let us know what you think! 🚀


If you are interested in reading more, head to our docs and examples!

The active development of Mu-Haskell is proudly sponsored by 47 Degrees, a Functional Programming consultancy with a focus on the Scala, Kotlin, Swift and Haskell programming languages.

Ensure the success of your project

47 Degrees can work with you to help manage the risks of technology evolution, develop a team of top-tier engaged developers, improve productivity, lower maintenance cost, increase hardware utilization, and improve product quality; all while using the best technologies.