[HN Gopher] Go: Functional Options Are Slow
___________________________________________________________________
Go: Functional Options Are Slow
Author : zdw
Score : 49 points
Date : 2022-05-25 03:44 UTC (19 hours ago)
(HTM) web link (www.evanjones.ca)
(TXT) w3m dump (www.evanjones.ca)
| rndmio wrote:
| 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.
| eudoxus wrote:
| 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 wrote:
| 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 wrote:
| The functional pattern on every request is quite common.
| Think gRPC-go's withContext(withDeadline()) pattern.
| brandonbloom wrote:
| 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).
| bbkane wrote:
| 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
| spockz wrote:
| What 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 wrote:
| I think that there is no benefits, it is just a workaround for
| lack of named arguments and default values for struct fields.
| nemothekid wrote:
| The 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.
| Someone wrote:
| The blog post that introduced it
| (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 wrote:
| Your chaining example and the defer example can't both work
| together, since they both rely on different return types for
| SetVerbosity.
| skrtskrt wrote:
| It also means there's no worry about breaking changes to a
| `Config` struct
| nmilo wrote:
| 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.
| codedokode wrote:
| 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.
| mixedCase wrote:
| 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 wrote:
| 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 wrote:
| There would still be reasons to have configuration structs if
| we had named arguments.
| jen20 wrote:
| > 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 wrote:
| 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.
| eru wrote:
| > 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.
| jerf wrote:
| 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 wrote:
| > 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
___________________________________________________________________
(page generated 2022-05-25 23:00 UTC)