4 ms·
If you're using the functional options pattern, and you're trying to == or type-assert one of the options, you're the one doing something abnormal. Maybe it ma
by kbolino 2y ago
If you're using the functional options pattern, and you're trying to == or type-assert one of the options, you're the one doing something abnormal.
Maybe it makes sense in the broader context of what you're trying to do, but then I'd say that the functional options pattern isn't the right fit in the first place.
Also, "side effect" in computer science has a pretty straightforward definition, and equality comparisons are not side effects.
- Groxx 2y agoSide effect: agreed, but the way you used it makes no sense in that definition (arguments cannot have side effects on a function call, they're resolved before the function is called), and the main other option in Go is to pass a struct or separate typed arguments which also has no side effects, and the only other interpretation is "the side effects you can trigger in the function based on the argument" which is identical in all scenarios... so I figured you were using it in the more colloquial way where it's closer "observable behavior [of the argument's type]". Or "the unintended consequences of a design decision".
- kbolino 2y agoIn 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.