3 ms·
In its simplest form, a functional option is: type Option func(ptr *T) The entire purpose of this function is to be called, and when called, to change som
by kbolino 2y ago
In its simplest form, a functional option is:
type Option func(ptr *T)
The entire purpose of this function is to be called, and when called, to change something about the value pointed to by ptr. That seems like a classic case of side effect to me, since the function returns no value (hence it's not a "pure function") but instead mutates some state (which lives beyond the function call).
For example, we can make such an option like this:
func SomeOption(optionalArg string) Option {
return func(ptr *T) {
ptr.otherField = optionalArg
}
}
The options would then be accepted by another function, such as:
func Example(requiredArg string, options ...Option) T {
val := T{someField: requiredArg}
for _, option := range options {
option(&val)
}
return val
}
In the body of Example, there's no need to use == on an Option. Each Option does its thing when called, and then we move on to the next one. We would use all of these things like this:
foo := Example("someFieldVal", SomeOption("otherFieldVal"))
Obviously, this is a trivial example, but I think it illustrates the point that functional options are used for their side-effects. If you need to do fancier things than just unconditionally set a field, you do all of that in the option itself (either before creating the closure, or inside the body of the closure).
- Groxx 2y agoYou're describing the absolute basics of functional options to someone who, by cautioning about a niche usability issue with this exact implementation, has demonstrated a solid understanding of them.
- kbolino 2y agoI've re-read everything you've written so far, and I think we're just in violent agreement. It would have helped if you provided some explanatory code, but the word "middleware" in your original post (which may have been edited in? if not, sorry, that's on me) clarifies a lot for me. If you do need to == or type-assert the options, then don't make them functional. That seems to be what you were saying with `type thearg string` earlier. However, I think my original example with an interface illustrated another/better way to solve the same problem: make the options self-describing.
- Groxx 2y agoSort of. My issue with using closures as your functional option implementation type (i.e. "functional options" is "call this function to get an opaque arg" which does not imply that you need to use a closure, it's just a common implementation strategy) is that when a library does it, it prevents anyone from writing middleware that is interested in argument equality. Among other useful desires. That's quite common in my experience, and working around it on the library-user side is rather painful, as it often requires reimplementing roughly the entire API so it can be controlled. The alternative is to have your return type be an opaque interface, which can have any implementation (including closures): type Option { apply(...) // privately implementable only // or just have an empty `private()` or something } type someoption string func (s someoption) apply(...) { // whatever is needed, it can be changed any time } func SomeOption(val string) Option { return someoption(val) } wrap(thing, SomeOption("arg")) ^ in this, the wrapper can check if options include `SomeOption("arg")` if that becomes useful for some reason. Quite often that reason is "tests", but I do keep running into other scenarios where it'd be useful too. I've helped multiple people work around it by reflecting on the function's signature, constructing the private arg type, calling the func, and comparing the mutated private apply-arg-type to a known value, but this isn't always possible and those cases are stuck either not doing the thing they wanted, or writing tons of code to wrap the types completely. Using a more concrete type also logs/prints/debugs usefully by default in most cases, giving you `someoption{"arg"}` instead of `(fntype)(&0x3768dh893)`. As a library author you generally cannot predict this kind of need, and using function closures is by far the least flexible and least-user-friendly option for your users. So I recommend not doing that.