DEV Community

Aaron Schlesinger
Aaron Schlesinger

Posted on • Originally published at arschles.com on

Functional Programming in Go With dcode

Back in 2017, I gave a talk at GopherCon about doing functional programming with Go. I went up on stage and talked about some FP basics like Functors, higher-order functions and so on. I heard some feedback that doing FP in Go isn't practical until Go has generics (fret not, folks, contracts are on the table!).

Well, that didn't stop me. I even made a gopher on gopherize.me to hold onto the spirit of FP on Go:

FP Gopher

It's true that contracts will be a huge boon for functional Go code, but there's lots of functional programming we can do in Go without generics.

You don't need generics to do FP in Go.

dcode

When I started writing that talk in 2017, I created the github.com/go-functional GitHub organization to hold experimental projects for doing FP in Go.

Beyond the code I showed on the slides, which is in core, I've been prototyping some other functional libraries like quicktest and pure functional SQL query builders. The one area that I really latched onto was JSON, and that's how dcode was born.

Inspiration

A long time ago, I built web services using Scala and lift-json. That library provides a pure functional tree representation for a JSON object. It's also relatively fast for deep traversals.

More recently, I found the Elm language, and particularly its JSON parser. This parser interface is a concise DSL that lets you say you you want to traverse a JSON object. It's incredibly self-documenting, even when dealing with large JSON objects. In fact, the DSL tends to look similar to JSONPath.

The Elm API makes the lift-json tree representation accessible to anyone, without requiring them to write their own tree traversal code.

Usage

You write one line of code to traverse any number of levels into a JSON object as necessary.

For example, if you have this JSON (adapted from the JSONPath page):

{
  "firstName": "John",
  "lastName" : "doe",
  "age"      : 26,
  "address"  : {
    "streetAddress": "naist street",
    "city"         : "Nara",
    "postalCode"   : "630-0192",
    "accessInstructions": {
        "gate": true,
        "message": "Please dial 0123 so I can buzz you into the gate"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

And you want to get to $.address.accessInstructions.message (that is, the value of the message field inside the address access instructions object), you'd write this code to crate a decoder that, when it's called, will try to pull that value out of any JSON you give it.

decoder := Field(
    "address",
    Field("accessInstructions"),
    Field("message", String()),
)
Enter fullscreen mode Exit fullscreen mode

There's also an accumulator interface so you don't have to nest all those calls to Field:

decoder := First("address").Then("accessInstructions").Then("message").Into(String())
Enter fullscreen mode Exit fullscreen mode

Why?

dcode obviously doesn't fit every use case. If you're expecting to decode a big JSON object with lots of keys, and the incoming JSON value is the same every time, you'll most likely be more successful writing a struct and using encoding/json to decode into it (although dcode does have functionality to decode into structs).

This library does well, however, in a few cases:

  • You need to traverse deep JSON objects to fetch values. This use case is common with programs that need to use a small part of a large JSON API
  • You don't know the "shape" of the object ahead of time. Often, you'll want to try several different decoding strategies at a time. This use case is common with web applications that deal with legacy API clients

Since dcode is based on functional programming principles, it also comes with two helpful patterns:

Composable

Each Decoder that you construct (with Field or First()...Into()) is a stateless function that is approximately:

func (d *Decoder) Decode(b []byte, i interface{}) error
Enter fullscreen mode Exit fullscreen mode

Note: I've written this function here as an example. It doesn't exist in the library, but you can use the top-level Decode function instead to get the same result.

Of course, this function signature looks a lot like (encoding/json).Unmarshal, which is nice for familiarity.

More importantly, these Decode functions are built up by composing other Decode functions together. The Field or First()...Into() interfaces both enable that composition.

This API makes it obvious how to structure your JSON decoding logic, and trivial to refactor it. It also really encourages the same kind of composibility as Decoders "leak" into other parts of your codebase

Bonus: dcode itself takes heavy advantage of composition. Check out Map and OneOf for examples.

Loosely Coupled

Decoders are instructions for decoding a JSON object, and they can exist alone, without the values that the JSON should be decoded into. This lets you pass Decoders to any function so that it can decode any JSON into any value as it sees fit. In the future, we may consider adding functionality to serialize and deserialize Decoders themselves, so you can pass them over the network.

To contrast, if you use encoding/json to decode JSON, you usually create a struct to define the "shape" of the JSON. This API requires that the values (struct fields) and the decoding instructions (the reflected field names, or struct tags) are tightly coupled.

Get Started!

Go check out the README for more on how and why to use dcode. Of course this is a work in progress, so if you find something missing, wrong, or just plain broken, please don't be shy and file an issue!

Keep on rockin', Gophers

Top comments (0)