9 ms·
Go: Functional Options Are Slow
- twic 4y agoOh 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!
- rndmio 4y agoThe 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.
- jerf 4y agoI 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 4y 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
- eudoxus 4y agoI'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 4y agoThis 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 4y agoThe functional pattern on every request is quite common. Think gRPC-go's withContext(withDeadline()) pattern.
- wbl 4y agoIf you are trying to shave nanoseconds off an RPC you have architectural issues.
- jeffbee 4y agoIf you approach every program with that attitude you will never have an RPC subsystem where nanoseconds matter. You will be trapped in a self-fulfilling process where nanoseconds don't matter because the system is slow.
- bbkane 4y agoOne 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 https://github.com/bbkane/warg/tree/master/examples
- spockz 4y agoWhat are the benefits that this functional options style offers? Is it that you can add more options without having to define a new field in a struct? One could even add all the With methods to the struct to get some fluent/builder pattern. Edit: is it so that you don’t need to instantiate a new struct at each call site?
- codedokode 4y agoI think that there is no benefits, it is just a workaround for lack of named arguments and default values for struct fields.
- Someone 4y agoThe blog post that introduced it (https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html https://commandcenter.blogspot.com/2014/01/self-referential-...) mentions func DoSomethingVerbosely(foo *Foo, verbosity int) { prev := foo.Option(pkg.Verbosity(verbosity)) defer foo.Option(prev) // ... do some stuff with foo under high verbosity. } but I don’t see why that’s better than func DoSomethingVerbosely(foo *Foo, verbosity int) { prev := foo.setVerbosity(verbosity)) defer foo.setVerbosity(prev) // ... do some stuff with foo under high verbosity. } It also mentions that it allows you to “set lots of options in a given call”. That, you could sort of accomplish by having the setProperty methods return the changed object, thus allowing chaining (e.g. foo.setVerbosity(v).setDryRun(true)). This allows both that defer and setting lots of options in a single call. Given the limited features of go, it’s a nice hack, but I don’t like it. To me, it doesn’t feel like it fits the philosophy of go.
- djur 4y agoYour chaining example and the defer example can't both work together, since they both rely on different return types for SetVerbosity.
- nemothekid 4y agoThe main benefit is you can have configuration options without having to specify all values, and also have non-zero-value defaults. Lets say you had something like Sarama's config struct which contains 50 or so config knobs. The following is will lead to some terrible defaults: NewConsumer("kafka:9043", Config{ClientID: "foo"}) Here, with this config, there is a config option `MaxMessageBytes` which will be set to 0, which will reject all your messages. What Sarama does is, you can pass a `nil` config which will load defaults, or: conf := sarama.NewConfig(); conf.ClientID = "foo" conf.RackID = "bar" NewConsumer("kafka:9043", conf) and so on. This is ok but can be cumbersome, especially if you just need to change one or 2 options or if some config options need to be initialized. Also someone can still do &Config{...} and shoot themselves in the foot. The functional options style is more concise. NewConsumer("kafka:9034", WithClientID("foo"), WithRackID("bar")) I used to be a fan of this style, and I even have an ORM built around this style (ex. Query(WithID(4), WithFriends(), WithGroup(4))), but I think for options like these a Builder pattern is actually better if your intention is clarity.
- codedokode 4y agoDon'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.
- mixedCase 4y agoGo 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.
- tptacek 4y agoThere would still be reasons to have configuration structs if we had named arguments.
- skrtskrt 4y agoAs 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.
- codedokode 4y agoPython just gives you a choice, use named arguments or refactor your API and use objects for options. It is up to you how you are going to use it. What I don't like though is when all those options are hidden behind two-stars keyword argument and it is difficult to understand what options are allowed, what they mean, what type they have and so on: def example(**options): ...
- brandonbloom 4y agoMy 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 4y agoVery neat way to do this.
- nmilo 4y agoI 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.
- kevindong 4y agoSure the relative difference between the fastest and slowest approach is 5x. But the absolute difference is still just 125.23 ns. For some perspective, 1 millisecond / 125.23 ns = ~7,985. There certainly are cases where that slowdown matters. But for the vast, vast, VAST majority of applications, it is completely irrelevant.
- marginalia_nu 4y agoDunno, it's not unheard of for a program to do the same thing over and over again, sometimes millions of times. Every slow program consists of a bunch of individually fast instructions.
- zaphar 4y agoIf your hot loop is doing configuration setup then your code has bigger problems than this particular pattern.
- marginalia_nu 4y agoYou could also be configuring many things.
- saturn_vk 4y agoIndeed, but you should probably do that right before looping
- marginalia_nu 4y agoRight, but if you needed to configure a million different things, wouldn't you also loop the configuration process?
- deleted 4y ago[deleted]
- newbye3 4y agoPerhaps this is the kind of problem in which a macro system, like that in common lisp, allow you to solve the problem paying the prize in a little more compilation time but not in runtime.