1. 5

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
          File.stat(output_file).mtime < File.stat(input_file).mtime


build_reason = must_build?

# Use object identity equal?()
if !build_reason.equal?( BUILD_REASON_DONT_BUILD)

# Log what you are doing and why....
log build_reason

  2. 6

    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.

    data Reason
      = OutputDNE
      | OlderMTime
      deriving (Show, Eq)
    class ReasonString a where
      reason :: a -> String
    instance ReasonString Reason where
      reason x = case x of
        OutputDNE -> "Building because output doesn't exist"
        OlderMTime -> "Rebuilding because output had older mtime than input"
    buildReason :: Thing -> Maybe Reason
    main = do
      case buildReason thing of
        Nothing -> putStrLn "Not building"
        Just r -> do
          putStrLn "Building!
          putStrLn ("Reason: " ++ reason r)
          build thing

    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.

    1. 2

      Ocaml has polymorphic variants which are nice for representing errors with monad control flow. Basically you have a type called something like result that looks something like and Either:

      type ('a, 'b) result = Ok of 'a | Error of 'b

      Polymorphic variants are variant types that do not need a type declaration. They also allow subtyping, so one can do:

      val do_thing : blah -> (success, [> `Failure1 of string | Failure2 ]) result
      val do_other_thing : blargh -> (other_success, [> `Failure2 | `Failure3 of int ]) result

      And can use it like:

      let bigger_fun () =
          do_thing blah
          >>=? fun result ->
          do_other_thing zoom
          >>=? fun result2 ->
          Ok (result, result2)

      And bigger_fun has the type:

      val bigger_fun : unit -> ((success * other_success), [> `Failure1 of string | `Failure2 | `Failure3 of int]) result
      1. 1

        Yes, polymorphic variants are nice. The “problem” is a loss of exhaustiveness. Obviously it’s only a problem when it’s a problem.

        1. 2

          In Ocaml you don’t. Your can prove you handle all possible errors, but you cannot prove that you handle too many.

    2. 5

      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.

      1. 4

        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”.

        1. 2

          Ruby lacks an enumeration first class type.

          Personally I don’t overly miss it, since the C enum is such a weak concept.

        2. 4

          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…

          1. 1

            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.

          2. 1

            I don’t much like this pattern. Looks like you’re using Ruby, why don’t you just throw an exception?

            1. 2

              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?

              1. 1

                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.

                1. 1

                  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”.

              2. 1

                It doesn’t seem like a file not existing is an exceptional event. It’s pretty normal, in fact.

                1. 1

                  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.

              3. 1

                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.

                1. 1

                  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.

                  1. 1

                    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.

                  2. 1

                    An OOP approach that I think is clearer and less likely to break in odd ways:

                    class ShouldBuild < Struct.new(:reason)
                      def must_build?; true; end
                    class AlreadyBuilt
                      def must_build?; false; end
                    def determine_build_state
                      return ShouldBuild.new(BUILD_REASON_OUTPUT_DOESNT_EXIST) unless FileTest.exists? output_file
                      return ShouldBuild.new(BUILD_REASON_OLDER_MTIME) if
                          File.stat(output_file).mtime < File.stat(input_file).mtime
                    build_state = determine_build_state
                    unless build_state.must_build?
                    # Log what you are doing and why....
                    log build_state.reason
                    1. 1

                      That will create one item of garbage per test… But I guess you could create frozen versions it and maybe a singletonclass…

                      BUILD_REASON_OUTPUT_DOESNT_EXIST = 'Building because output doesnt exist"
                      class << BUILD_REASON_OUTPUT_DOESNT_EXIST
                         def must_build?

                      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?()

                      1. 1

                        For any non-trivial build process I have difficulty imagining that this kind of micro-optimization would make a significant difference.

                        1. 1

                          Ah, but the cases everybody cries most about are….

                          • They don’t trust the dependency tracking of the build, because they don’t understand why it did ( or didn’t rebuild) something.
                          • the “null build” case. ie. Change one file out of thousands, recheck the dependencies and do the right thing. In which case your speed is largely governed by Whether things are “hot and in cache” for the disk buffers and directory caches. how little garbage you generate!

                          Yup. I do benchmark and measure.