1. 11
  1.  

  2. 12

    This is a useful technique but I think the author goes too far when advocating for universal replacement of conditionals with lambdas.

    In some cases it could be an improvement to usability and reliability, in others it isn’t.

    In the real world example given, I would argue the refactored version is worse to use and you really aren’t gaining anything. The argument that lambdas are better than conditionals seems to be based on the idea that the conditionals could be faulty and the caller doesn’t really know what’s going on. But if you’re dealing with pre-made lambdas supplied by the same library as the method you’re calling, is that really fixing either of those problems? If you don’t trust the method to have a conditional that works or does what you think it will, why would you trust that the lambdas are going to work and do what you think they will? Just because they in theory don’t have conditionals themselves? Flow control bugs are only one class of bug.

    In the example given, the option to dry run or not dry run is now being buried inside of an options instead of being clearly set on the method call. That seem like a loss of code and usage clarity. And now the logic of doing a dry run or doing a publish is separated into two lambdas and not in the actual publish method. All around it seems like a loss.

    Back when OO was really gaining popularity in the early 90s, some people learned the wrong lessons for it and advocated for bad practices. I think the same thing is happening with functional programing now.

    Lambdas are extremely useful and awesome tools. But replacing all conditionals with them is not an improvement or a good use of them, IMHO.

    1. 4

      The issue is much larger than just booleans. Fundamentally, it’s about taking semantics, and packing them into data values (booleans), feeding them into functions, and unpacking the data values into semantics (code).

      Instead of developing bit protocols to serialize intentions, why not just propagate our intentions directly into the code?

      Indeed, you do that by pulling out those semantics (blocks of code) from the site of the conditional (inside the callee) and into the caller. The callee then doesn’t need to make any decisions, and neither callee nor caller are required to serialize intentions to and from bits (or other data values).

      Coding in the if/then/else style is pervasive, possibly because there existed a point in time when programming languages did not let you easily or performantly “propagate intentions”. But every time we engage in that style of programming, we are packing our intentions into bits at the caller site, and unpacking them at the callee site, and the use of an inter-program serialization protocol for semantics can and clearly does lead to lots of bugs.

      Others may have different preferences, but as for me, I prefer to propagate intentions directly into the code; and to remove control from a callee to make the callee simpler, easier to reason about, and easier to test.

    2. 5

      This post seems to be arguing for two things that are actually unrelated to conditional expressions, which are unavoidable:

      1. The type signature of a function with multiple parameters that share the same non-descriptive type (bool, int, etc) can be made more descriptive and less error prone (i.e., wrong argument order) by introducing new types.

      2. You can change a function to be more general purpose by parameterizing them with functions in the quintessential functional style used by functions like map or filter.

      I think introducing a lot of tiny, single-purpose, two-valued sum types like in (1) is ugly from a code aesthetics point of view. What we’re really after here are compiler-checked names for arguments, which can be achieved using records (example in ML):

      match : string -> { ignoreCase : bool, globalMatch : bool } -> string -> bool
      

      My problem with the overall argument about “removing conditionals” is in the real-world example provided, where a function is basically just split into two pieces, and the caller is assumed to be passing a constant value that represents either the piece that performs a dry run, or the real deal. What if that value is dependent upon a dynamic value, or some other more complicated situation? The conditional would just move somewhere else:

      if dryRunCmdOption() then publish dryRunOptions else publish forRealOptions
      

      I don’t think breaking functions into pieces to avoid a conditional expression is a rational design choice.

      1. 6

        The conditional would just move somewhere else…

        That sums up my read of this article as well. The author seems to be making an argument that either ignores Conway’s Law or underhandedly exploits it by trying to move decisions from the callee to the caller. Any code that has value is going to need to make decisions of some form, and the author hints at this in the aside “now the function match has been simplified so much, it no longer needs to exist, but in general that won’t happen.”

        They might be trying to push the decision making off to some other team as you describe, but what’s to stop that team from passing the buck further? It seems that you would end up pushing everything to the “business” people who write configurations, which leaves you with code that works perfectly but doesn’t do anything, and pathologically complex configurations that never quite work but hold all the cards. Which, I guess, is exactly what happens in shitty enterprise-ware.

        The point of decision is where the work happens, by definition. There are better and worse places to put that depending on context, but pretending it can be entirely shoved-off is not realistic.

        1. 1

          Introducing sum types for things like booleans does, in my experience, reduce errors; but I wasn’t arguing for that. I was arguing for one step beyond that, which is the elimination of the sum type (boolean or otherwise) by extracting the effect of the branch into a lambda, which is passed by caller to callee.

          1. 2

            I use this based style quite a bit. As @orib points out, it’s basically the Strategy Pattern. The choice I make between when to use an if and when to use this is how “open” I want the policy to be. When in doubt, I go for a function because that is the more general solution. IMO, it also makes what the code does much easier to understand. You may not know what code is called at each stage, but the sequence of things that are supposed to happens becomes more apparently when one doesn’t have the if statements everywhere.