This pattern generalises to any method where you are currently returning a boolean…. but it is “true” for many different reasons and “false” if and only if none of the other reasons apply.
As a concrete example consider this commonish task….
Decide whether or not you must rebuild something depending on whether the output exists and / or whether the output is older than any of the inputs it depends on.
Pretty much what “make” does for you.
But suppose you are writing a method “must_build?”, the “obvious” return type is boolean true / false.
I have settled on a better pattern….
I always return a frozen const string.
Why? Because it enables me to efficiently inform my user why I made the choice I did.
For example, (speaking ruby, but the pattern translates to other languages)
BUILD_REASON_OUTPUT_DOESNT_EXIST = 'Building because output doesnt exist".freeze
BUILD_REASON_OLDER_MTIME = 'Rebuilding because output had older mtime than input".freeze
BUILD_REASON_DONT_BUILD = 'Not rebuilding because file is up to date'.freeze
def must_build?
return BUILD_REASON_OUTPUT_DOESNT_EXIST unless FileTest.exists? output_file
return BUILD_REASON_OLDER_MTIME if
File.stat(output_file).mtime < File.stat(input_file).mtime
BUILD_REASON_DONT_BUILD
end
Usage….
build_reason = must_build?
# Use object identity equal?()
if !build_reason.equal?( BUILD_REASON_DONT_BUILD)
build
end
# Log what you are doing and why....
log build_reason
This is a pretty natural idea in Haskell. People usually call the Maybe type the type which captures “failure”, but it’s really just any type A adjoined with a special “other” value. In this case, the lack of reason to build.
Notably, this is halfway to avoiding Boolean blindness. The next step would be to package information necessary to build into the build reasons so that the build literally could not proceed unless a valid reason exists.
Ocaml has polymorphic variants which are nice for representing errors with monad control flow. Basically you have a type called something like
resultthat looks something like and Either:Polymorphic variants are variant types that do not need a type declaration. They also allow subtyping, so one can do:
And can use it like:
And
bigger_funhas the type:Yes, polymorphic variants are nice. The “problem” is a loss of exhaustiveness. Obviously it’s only a problem when it’s a problem.
In Ocaml you don’t. Your can prove you handle all possible errors, but you cannot prove that you handle too many.
The nice thing about booleans is there’s only two values: true and false. With this pattern, there’s an infinite number of possible values even in type-checked languages.
Seems like you want a self-describing enumeration, where the range is bounded, and an explanation is associated with each value. Enumerations are nice because all possible values are kept together. You can instantly tell if a function is poorly factored when you see True/False/FileNotFound.
That’s because it introduces a new type but Ruby lacks facilities to delimit it. See my Haskell example and search online for the phrase “stringly typed”.
Ruby lacks an enumeration first class type.
Personally I don’t overly miss it, since the C enum is such a weak concept.
On a slightly meta note, I enjoy posts like this, just discussing a certain way of going about doing something. Its nice to hear some polite discourse about different methods and strategies, you dont really get that anywhere else…
Thanks, I have been looking for exactly “polite discourse about different methods and strategies”.
With ideas like this…. it seems “Good To Me”… but it’s always a good thing to bounce ideas off other minds to solidify and grow it… and where need be change or kill the idea.
I don’t much like this pattern. Looks like you’re using Ruby, why don’t you just throw an exception?
Yup, that is an option.
But the reasons are not “an exceptional event”… I will be generating one every time round my inner loop….
The frozen const string trick keeps the object generation (and garbage collection) in check.
I guess I could keep a frozen Exception for each eventuality.
Hmm. In the C++ world the advice is throwing, stack unwinding and catching exceptions is expensive, save them for exceptional events.
I wonder if that is still valid advice in Ruby?
Ok, if it’s not an exception event then yeah, don’t use an exception. The problem I have with your solution is that it’s not opaque enough to users. In C you would have an enum and functions to turn the enum into a string. Even if you kept frozen strings I’d still provide an API to transform the opaque value into a string (which would just be identity here) so that people don’t get the wrong idea that the string is part of the API.
Ruby style typically doesn’t do “opaque”.
I can’t actually think of another place where I have seen the “opaque handle” pattern used in Ruby. (Sure, “C” level things are opaque, but libraries written in Ruby?
Yup, you can do it, Array of things, use an integer index into that array, wrapped the integer in a class as a handle.
Seems overweight and I can’t say I have ever seen it done in the standard libraries.
The const’s, rather than the string, are part of the API and you can use .equal on them.
That is what I document as being part of the API. To a certain extent I want to leak a string out of the API. I want to be able to output it “straight to the eyeballs”.
It doesn’t seem like a file not existing is an exceptional event. It’s pretty normal, in fact.
Sure, then no exception. I don’t think a string’s are a good solution here though. Mixing the abstract identifier with the message tends to end poorly.
I don’t like this pattern, because it allows for many human errors, and there are plenty of better ways to achieve what you are looking for. Like a verbose flag/mode, where this information is provided to the user through a different channel than the return value.
This generalizes; a bare boolean in an API is usually a mistake, IMO. If you have a two-state system, it’s still worth giving those states labels; replacing with an enum is not hard.
But a String is almost as bad. It doesn’t really imply the semantics, and it enables a lot of operations that are actually irrelevant (why would you ever want to toUpper() or slice() a build failure reason?) In your example labels you’re using the constants like an enum - as matgreenrocks suggests, better to actually use an enum. The compiler may even be able to warn you if you failed to handle a particular case.
Except in a language that doesn’t have enum (like Ruby) or a very weak enum ( like C)
The thing is it isn’t a String that’s the API, its the CONST. ie. You don’t use string compare, you use object identity.
An OOP approach that I think is clearer and less likely to break in odd ways:
That will create one item of garbage per test… But I guess you could create frozen versions it and maybe a singletonclass…
And then another bout like that for the other reason, and one that returns false for the already built.
But seems excessive and uses I Ruby feature I have never really liked.
If I was going to wrap it any further I would just add a helper method to do (and name/describe/document) what is happening with the .equal?()
For any non-trivial build process I have difficulty imagining that this kind of micro-optimization would make a significant difference.
Ah, but the cases everybody cries most about are….
Yup. I do benchmark and measure.