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