04 Nov 2014, 14:30

functional options

I found Dave Cheney’s post about options in function signatures interesting.

I have, of course, been guilty of adding a new parameter for each option until the signature is ridiculously unwieldy. Moving on to his example of a config struct, this seems like a very common solution to this problem in Javascript (this was advocated by folks I’ve worked with in the past), but it’s always struck me as inherently opaque. I wanted to know how to get a NewWidget, and now I have to read all about these Widget options to make sure everything is how I think it is! Also, there’s the zero value issue Dave mentions, but that’s a problem specific to structs (and Go), since this pattern in Javascript can differentiate between 0 or null and undefined. Anyway, I agree that it’s probably a better solution than ever-longer parameter lists.

The initial example of functional options left me quite unconvinced. I will include this one.

func NewServer(addr string, options ...func(*Server)) (*Server, error)

func main() {
    srv, _ := NewServer("localhost") // defaults

    timeout := func(srv *Server) {
        srv.timeout = 60 * time.Second
    }

    tls := func(srv *Server) {
        config := loadTLSConfig()
        srv.listener = tls.NewListener(srv.listener, &config)
    }

    // listen securely with a 60 second timeout
    srv2, _ := NewServer("localhost", timeout, tls)
}

At this point, I was thinking “Why does the user of NewServer need to know so much about the internal structure of a new srv?” It seemed like a major step backwards. But then it became apparent that that wasn’t what was intended. Instead, the idea was to expose config functions from the configured package to be used in whatever way is appropriate (I’ll come back to that).

config := loadTLSConfig()
srv, _ := serverPackage.NewServer("localhost", serverPackage.setTimeoutSeconds(60), serverPackage.useTLS, serverPackage.useTLSConfig(config))

This isn’t the code he used, but it preserves the important details from his penultimate example, I believe. A minor nit, here, is that I’d prefer everything that is similar look similar, so passing a function that just sets a flag should look the same as passing one to which you have to provide an argument. But here’s the thing that I don’t get about using functional parameters: if you’re going to expose a bunch of configuration functions anyway, why bother passing them around? Instead we might have a bunch of methods on the result:

config := loadTLSConfig()
srv, _ := serverPackage.NewServer("localhost")
srv.setTimeoutSeconds(60)
srv.useTLS()
srv.useTLSConfig(config)

The only reason I can see is to centralize error handling, which (A) only matters for Go, and (B) it removes useful choices from the user of the package. “Okay, so the cert we were given is invalid, but let’s start up the non-TLS part of the site anyway and complain”. This is a decision that has to be made at useTLSConfig-time, and shouldn’t be buried.

I don’t think that we can settle on a single pattern for configuration. Sure, having separate constructors for all possible permutations is silly. Sure, there are issues with the difference between default and zero values (though passing maps in Javascript doesn’t have this problem, as mentioned above). But there really are major differences between a server that uses TLS and one that doesn’t; if there’s a single option that implies a whole different set of configuration parameters, it’s reasonable to create a different constructor for it, I would think.

So we have separate entry points or constructors, structs, parameters, functional options, and methods.

When we’re designing the API for our package, we have to consider all of these, and use the appropriate ones for those options that are best suited for them.

  • constructors for distinct things (in usage, if not type)
  • structs when you need a bag of scalar options
  • parameters to the constructor for required options
  • methods for non-required options where failure to apply the option can result in decisionmaking
  • functional options for non-required options that cannot fail to be applied or where failure means the whole constructor must fail
comments powered by Disqus