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:
|
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:
|
defn calc_formula = ( a : int, b : int, is_gain : bool )->(:float) {
...
if (is_gain) {
}
...
}
|
The function itself looks fine, but what happens to the caller?
|
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:
|
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
.
|
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 enum
s, 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.
|
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:
|
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:
|
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.
|
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.