I need to make a quick variation of a function. The calculation, or behaviour, differs slightly on the needs of the caller. I throw in a bool
parameter to do this switch. It’s fast and easy, yet I’m almost always disappointed later. It’s a bit hard to read from the call side. An enum, or distinct functions, would be cleaner.
Setup
The basic example here is you have a function:
1 2 3 |
defn calc_formula = ( a : int, b : int )->(:float) { ... } |
And you have a new call-site that needs to slightly alter the behaviour. So you stick a boolean on to trigger that switch:
1 2 3 4 5 6 7 |
defn calc_formula = ( a : int, b : int, is_gain : bool )->(:float) { ... if (is_gain) { //something slightly different } ... } |
The function itself looks fine, but what happens to the caller?
1 |
var v = calc_formula( ia, ib, true ) |
When I’ve just modified the calc_formula
function then I’ll know what that true
means. But what happens when I come back months later. Or what if somebody else sees this code. They’ll have to flip to the function definition to understand the mysterious true
value. It’s not a typical parameter to this formula, thus it can’t be inferred from context.
Enum
This looks easier to read:
1 |
var v = calc_formula( ia, ib, calc_formula_type.is_gain ) |
I can immediately see I’m using an is_gain
variant calculation. All I’ve done is swap out the boolean parameter for an enum.
1 2 3 4 5 6 7 8 |
enum calc_formula_type { standard is_gain } defn calc_formula = ( a : int, b : int, opt : calc_formula_type )->(:float) { ... } |
I’d like a language that can contextually resolve enums, so I can just type something like
calc_formula( ia, ib, is_gain )
. Having to remember the enum type names is kind of annoying. I’ll probably have some way to do this in Leaf.
Distinct functions
An alternative is to use distinct function wrappers and hide the underlying function.
1 2 3 4 5 6 7 8 9 10 11 |
defn calc_formula = ( a : int, b : int ) -> { return calc_formula_impl(a,b,false) } defn calc_formula_is_gain = ( a : int, b : int ) -> { return calc_formula_impl(a,b,true) } defn calc_formula_impl = ( a : int, b : int, is_gain : bool ) -> { ... } |
The callers look good: calc_formula(ia, ib)
or calc_formula_is_gain(ia, ib)
. It still involves a boolean on the actual function implementation. Though it isn’t as bad since the only callers of the calc_formula_impl
know about it, and they’re defined right beside it. The context makes it easy to understand that the boolean parameter is.
Deciding which to use
Both solutions make the calling code easier to understand.
The distinct functions form comes at the cost of boilerplate wrapper functions — I’m never a fan of boilerplate code. However, if the formula really is distinct, then it makes sense to have separate functions. If the two functions truly feel like two separate functions then I do prefer this form.
Consider for example sin
and cos
. They are basically the same function with a phase offset. I’d truly hate to see something like this in code:
1 |
var q = sin( angle, sin_mode.cos ) |
Sure, the implementation is nearly identical, but they feel like very distinct functions.
I also don’t like flags that change the purity of the function:
1 2 3 |
var bond = calc_bond( params ); var rbond = calc_bond( params, bond_mode.register_global ); |
The second form both calculates a bond and registers it in a global table, whereas the first bond is a clean function without any side effects.
That is, I put up with the boilerplate code if the flags approach “feels” wrong.
Virtuals
If the function is a virtual member in a class I’ll nonetheless lean toward the flags approach. A series of functions can often place a burden on derived classes: the boilerplate multiplies.
A nice combined solution is to make a protected enum virtual and expose the wrapper functions in the base class.
1 2 3 4 5 6 7 8 9 10 11 |
class shape { defn calc_nominal_bounds = -> { return calc_bounds( bounds_type.nominal ) } defn calc_render_bounds = -> { return calc_bounds( bounds_type.render ) } protected abstract defn calc_bounds = ( how : bounds_type ) -> ( : rect ); } |
This lets me call the functions with the nice form my_square.calc_render_bounds()
but avoids having to overload multiple definitions in each derived class.
Categories: Programming, Use Case
In Python, you can actually use the name of the parameter inside the function call:
v = calc_formula(ia, ib, is_gain=True)
Making it easy to understand yet still using standard boolean.
Yes, this is another good approach. I’ve used it often while doing Python coding.
Named parameters (at call site) could perhaps also be an option if the language supports them.
I think your distinct function approach is closer to the answer. Named arguments, if you think about it, are an anti-patterns, because they almost always imply an “if” expression in the body, which makes it complex for no good reason. IMO you should write functions as small as possible, and compose them to achieve complex behavior. That should yield less “if”s, less named or boolean arguments.
I in general like avoiding `if` as well. But most of the common languages actually don’t provide good ways to do function comprehension, especially where it is something in the middle of the function that needs to behave differently.
Closures and lambdas help, as do C++ templates, but it still often feels very difficult to do basic function patterns.
For using an enum with a virtual function I don’t like the fact that a derived class cannot extend the enum.
In this case it is perhaps desired, but I certainly know of situations where I’d like what you want.
In the calc_formula_impl case, I like it when the language easily allows to make the _impl function private to the two other functions. For example in OCaml:
let calc_formula, calc_formula_is_gain =
let calc_formula_impl is_gain a b = … in
calc_formula_impl false, calc_formula_impl true
Swift can handle context-dependent enums: https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/NestedTypes.html
I thought I remember reading about this before. See also http://ernstsson.net/post/27787949222/boolean-parameter-elimination
What is function comprehension? Is it like a list comprehension in functional programming?
most languages (without named parameters) can do something like
var is_gain
var v = calc_formula( ia, ib, is_gain=true )
Ha! That’s clever. It looks like that would work in C, in which assignment operations are expressions.