[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)