Readit News logoReadit News
eudoxus · 3 years ago
I'm all for questioning the validity of certain code patterns, but there's some issues with this post.

Functional Options (or config/option initialization) shouldn't really ever happen in a "hot path" where performance really matters, as these are usually one off steps at the time of construction/initialization. As with most things in Go, start with usability/readability then measure and tune when/where needed.

With that in mind, the author doesn't give a concrete example of when a Functional Option pattern might be used in a hot path, in which case, certainly agree there are better patterns to use.

Then adds the benchmarks which (ignoring function inlining) are relatively comparable for Functional Options vs Config Struct, with the notable increase when using interfaces (as with many things in Go). But these results are still on the order of ~100ns. I think more accurately they can be characterized as "Relatively" slow.

infogulch · 3 years ago
This is the proper frame to analyze this issue. If you're using Functional Options to configure a long-running http server once at startup, the cost is so small that you've already spent more money thinking about it for 1 minute than it will ever cost in compute runtime. But if you're using it once per request over thousands of requests, or once per record with thousands of records per request then maybe it's time to consider using a more lightweight configuration pattern.
jeffbee · 3 years ago
The functional pattern on every request is quite common. Think gRPC-go's withContext(withDeadline()) pattern.
chimeracoder · 3 years ago
> Functional Options (or config/option initialization) shouldn't really ever happen in a "hot path" where performance really matters, as these are usually one off steps at the time of construction/initialization.

That's not true at all. As just one counterexample, the place where I have spent the most time wrestling with functional options is with OpenTracing, where performance overhead absolutely does matter.

onionisafruit · 3 years ago
They said “shouldn’t” not “doesn’t”. A good rule of thumb is don’t use open tracing in any functions where you expect to measure anything less than 10ms.
jerf · 3 years ago
I dunno about the Go community as a whole, but /r/golang discussions have been trending back to just using configuration structs, rather than any of the other fancy options proposed over the years.

One of the advantages it has is that it's simple, so it works with all the language mechanisms quite naturally. Do you want to factor out a particular set of three settings? Just write a "func MyFactoredSettings(cfg *ConfigStruct)" and do the obvious thing. Do you need more arguments for your refactoring for some reason? It's a function, do the obvious thing. No mysteries.

I am reminded of the function programming observation that functions already do a lot on their own and are really useful. Additional abstractions around this may superficially look neater in isolation but I have been increasingly suspicious of anything that makes it harder to take a chunk out of the middle of my code and turn it into a function, and despite the name, "functional options" kinda have that effect. (Go is obviously no Haskell here... not many things are Haskell... but it's at least a similar issue. Anything getting in the way of basic refactoring with functions should be looked at suspiciously.)

(I would also say that while you can refactor functional options, there is something about it that seems to inhibit people from thinking about it. Similar to "chaining" that way.)

kodah · 3 years ago
> discussions have been trending back to just using configuration structs, rather than any of the other fancy options proposed over the years.

It may look a little more fat, and probably copies some fields that will end up in configuration but...

1. Go is very adept at copying large structures 2. A fully scaffolded struct is far easier to read than something hidden inside a function somewhere

nmilo · 3 years ago
I mean, of course they're slow, it's varargs (on the heap, garbage collected), dynamic function closures (on the heap, garbage collected), and a series of indirect function calls. You really don't need a bunch of benchmarks to tell me it's slow, I believe you. But as much as it pains me, and any premature optimizer, to write "...func(*config)", I don't see the problem unless you find it in a hot section of real code and then do real benchmarks on the code to solve a real problem; these blog post benchmarks are not helpful. I bet regexp.Compile is slow too, but I don't complain about it until I find it in a hot section of code.
brandonbloom · 3 years ago
My preferred idiom is essentially the command pattern:

    type Frob struct {
      SomeFlag bool
      AnotherArg string
    }

    func (args Frob) Do() FrobResult {
      // ...
    }

    // Later:

    res := Frob{SomeFlag: true}.Do()
This saves the stuttering of `Frob(FrobOptions{`, should have identical performance to that, with nicer syntax, and has a smooth upgrade path for all the sorts of things folks do with the command pattern (such as logging, dynamic dispatch, delayed execution, scripting, etc).

qaq · 3 years ago
Very neat way to do this.
codedokode · 3 years ago
Don't understand why Go developers choose the most complicated solutions. Function returning function returning function is an awful style of coding that is hard to read. I see at least two simple ways to pass options:

1) named arguments:

createFoo(barness: "bar", bazness: True)

2) struct with default values:

createFoo(FooConfig{barness = "bar"})

Go might not have these features, but I guess it is easier to add them than to invent weird "function returns function" tricks.

With functional options the code needs to be duplicated: first, you need to define a field in a struct and then a functional option that sets that field to a given value. With ideas above no duplication is necessary.

jen20 · 3 years ago
> Function returning function returning function is an awful style of coding that is hard to read.

Although I hate functional options for their lack of discoverability, the idea that currying is "awful" is pretty far fetched.

munificent · 3 years ago
Given that the parent comment says "hard to read", an obvious charitable interpretation of their comment implies "with Go's syntax".

Currying in languages that use different syntax is orthogonal to the point being made here.

codedokode · 3 years ago
Using currying by itself is fine and easy to understand. But if you write a function that returns a function that returns a function — that's not easy.

For example, compare these two functions:

function (a, b) { return a + b; }

function create_adder(a) { return function(b) { return a + b;}}

The purpose of first function is easier to understand. And if you rewrite last function according to modern trends, it can become even less readable:

const create_adder = (a) => (b) => a + b;

mixedCase · 3 years ago
Go developers came up with this pattern to deal with the language's limitations. Go's core team would probably advocate for a mutable configuration struct with a magical interpretation of zero values, and/or a constructor-by-convention and advise users to "just not make mistakes".

If we had non-zero default values or named arguments, this pattern wouldn't exist.

skrtskrt · 3 years ago
As a former Python dev, default arguments are nice but they get abused to hell.

Need to add functionality to something? Don't think! Just add an argument with a default to the current behavior, all the way up and down the stack.

Now you have just one API that does everything! Just set 20-40 parameters to decide the behavior.

tptacek · 3 years ago
There would still be reasons to have configuration structs if we had named arguments.
eru · 3 years ago
> Function returning function returning function is an awful style of coding that is hard to read.

Hey, that's basically how any function with more than one argument is implemented in Haskell. (Look up currying.)

It's not so much that this style is 'awful' in any universal sense; it's more that Go is terrible, terrible host language for anything in this style.

codedokode · 3 years ago
Haskell hides this complexity and allows you to write just:

add x y = x + y

There is no functions returning functions here, just a normal addition. It is perfectly readable.

kllrnohj · 3 years ago
Neither of those address the issue of scoped overrides of a setting. The benefit of the functional options approach is they return the inverse setting, so you can very trivially do scoped overrides for things like log levels. Eg:

   prevVerbosity := foo.Option(pkg.Verbosity(3))
   foo.DoSomeDebugging()
   foo.Option(prevVerbosity)
(better yet, use defer to restore)

Your examples seem to be reducing the problem space to exclusively object creation time. And in that case, yes named params or a struct with default values work great. But they work a lot less great when you're talking about changing an existing object, as now your default values can't just be the actual default values, but rather optional values since you need to distinguish between "set to a value that happened to be the same as the default" and "didn't set a value at all, so don't change it".

iainmerrick · 3 years ago
Is that really such a common case? Obviously it depends what you’re configuring, but I definitely would not expect that it’s typically OK to jump in and modify the configuration of an object that’s already in use. What if it does do some expensive one-off setup using the supplied config at object creation time?

If you really need a general scoped override, it could be done in the config struct approach just by copying and restoring the entire config. This might be expensive if the struct is big, but on the other hand, you could change multiple properties at once which doesn’t look possible in the function-based approach.

nmilo · 3 years ago
You're conflating developers who write in Go with developers who write Go. It's easier for me to write weird "function returns function" tricks than it is to fork a compiler.
unboxingelf · 3 years ago
Go definitely has these features and it’s the standard way to pass config.

CreateFoo(FooConfig{bar: “baz”})

rndmio · 3 years ago
The interesting part of this article was the subjective reasons to avoid functional arguments. As the article says you most commonly see them used in initialising something, the performance difference identified in the benchmark is unlikely to matter, it’s more a matter of preference and aesthetics.
bbkane · 3 years ago
One thing that I find nicer with functional options is building tree-like data structures.

My command line parsing library uses them to declaratively build CLI apps with arbitrarily nested subcommands.

Some examples at https://github.com/bbkane/warg/tree/master/examples

twic · 3 years ago
Oh that's nothing. In Java I came up with a way to pass config options using method references as keys, so you can write something like:

  createFoo(with(FooOptions::enable, true), with(FooOptions::size, 7));
And processing the options involves serialising each method reference to work out what it is!