> Problem: Any time you see this you actually have two methods bundled into one. That boolean represents an opportunity to name a concept in your code.
This is only true if you're using literal booleans at you call sites. If the booleans are coming from somewhere else, like user input, you just moved your single if statement in the method out to every call site.
> Context: You are calling some other code, but you aren’t sure if the happy path will suceceed.
> Problem: These sort of if statements multiply each time you deal with the same object or data structure. They have a hidden coupling where ‘null’ means someting. Other objects may return other magic values that mean no result.
Passing a default value works, but in Java 8, returning an Optional is cleaner. And if you wanted a default value, you can still do
repository.getRecord(123).orElse("Not found");
Edit: Also, the coping strategy link [1] he gave to remove exceptions sounds a lot like restarts in Commons Lisp's condition system.[1] https://silkandspinach.net/2014/11/06/on-paperboys-newsagent...
(defmethod make-sound ((b bear) (loud-p t)))
;; bear makes loud sound
...)
(defmethod make-sound ((b bear) (loud-p null))
;; bear makes quiet sound
...)
If we call (make-sound bear-instance nil), the second method is invoked because loud-p is specialized to the null class, whose only instance is nil. (There is a type nil also, whose domain is the empty set: it has no instance.)If we call (make-sound bear-instance <any other value>), then the first specialization takes over. It's specialize to T, the grand superclass, so it is a fallback that takes anything. Any value other than nil serves as a Boolean true in Lisp.
It's a magic of Lisp that T stands for "any type" as well as "true". :)
I don't know whether treating Booleans as method parameters like this is good design, but I don't see any immediate down sides.
At some point applying "anti-if" does involve major refactoring, so the outer style of your code might need to change a bit.
Of course like all design patterns it's more of a thought experiment than a total prescription IMO. But taking the thought experiment to its logical conclusion can also yield interesting results.
Some people might throw out strawman arguments about design patterns, but I'll end up reading the strawman and be like " yeah that does sound like a good idea". It's pretty hard to overestimate the value of regularity in code.
I guess the author would argue that the boolean should be pushed up to UI code. Arguably that domain logic belongs in the UI. Possibly a utility method in the UI package.
That being said, with all patterns there are exceptions. Following something to absolute purity is going to result in unclear code - polymorphism purity can rapidly cause a spaghetti codebase. Even GoF patterns can easily result in messy code. All patterns are blueprints, not rules.
On the topic of boolean values, I'm becoming more and more convinced by the anti-bool pattern. Normally this involves using an enum (usually bit field) when you first declare a parameter.
FileOptions options = FileOptions.None;
options |= temporary.checked ? FileOptions.Temporary : FileOptions.None;
options |= encrypted.checked ? FileOptions.Encrypted : FileOptions.None;
FileUtils.createFile("name_temp.txt", "file contents", options);
createFile now includes an 'if' and is at odds with anti-if (which is a pattern that I agree with). This is exactly my point, irrespective of whether you use multiple methods (createFile, createTemporaryFile, createEncryptedTemporaryFile, etc.) or polymorphism (File, TemporaryFile, etc.); you're going to end up with unclear code. Increasing clarity is the whole purpose of patterns. Composition (which bitfields are a form of) is the clear winner here and so the anti-if pattern has to be eliminated.See also: efficiency. There's always a way to make it more efficient.
The point of passing boolean params instead of named functions is that most of the time there is shared code between the two paths and not literally all the code is enclosed in either the if or the else block. If the author was intending just to restrict to that one specific case where there was no overlap whatsoever, then I guess I would agree but I'm not sure how often that happens.
Polymorphism has some advantages but it's certainly got it's own troubles in terms of understand-ability. The author links to an article which even states "I think that we hate conditionals more than we should. Introducing polymorphism too soon will complicate your code, making it hard to understand and test." It then goes on to give an example where it says polymorphism is actually the right choice, to avoid two switches. I'd say that at least this trivial example would be best handled by some data-driven approach. Look up salaries in a database instead.
The inline statement one can certainly be better in some cases, but the example given is quite poor. What does "foo && bar || baz" mean? Well, now I have to remember the order of operations ... 'or' after 'and' I guess. Surely this is easier to misread than the if statement example, which in my mind closely matches how I think. At least it should be "(foo && bar) || baz". But even then, that only works when the actual return type is boolean and there are no state changes made.
I don't really see how using an Optional type would remove the if(null) checks. It just makes the meaning explicit, which is good but not remedying the original problem.
5 seems nice.
If you have this
method(bool arg)
{
// block A
if (arg)
{
// block B1
}
else
{
// block B2
}
// block C
}
you can replace it with method1()
{
A();
B1();
C();
}
method2()
{
A();
B2();
C();
}
where A(), B1(), B2() and C() are new methods that contain the previous blocks. This makes the code clearer, IMO.Depends on how much state you need to pass between A, B1, B2 and C.
If you always use `Option` (or whatever equivalent your language/base library provides) and banish `null` from your source code you've effectively avoided all null pointer exceptions and can therefore remove all null checks.
Yeah, the benefit is not really the Option (which you still have to check), the benefit is when there's no Option. I'm not really sold on there being a benefit in Java (looks like those are the code samples in the article?), but in languages where `null` is not a part of the type, and your parameter is just a plain, `int`, say, then you know you don't need to check if it's `null`. In other languages, you either have to always check, or rely on a convention or code flow that has the null checks happening earlier.
The cases where there's a lot of commonality are cases for polymorphism instead. If you have something like:
def doSomething(useCache: Boolean) = {
//long and complicated function
if(useCache) cache.lookup(foo) else calculate(foo)
//more long and complicated stuff
}
then it's probably worth breaking that out as trait FooProvider { def get(foo: String): Foo }
object CachedFooProvider extends FooProvider {
def get(foo: String) = cache.lookup(foo)
}
object CalculatingFooProvider extends FooProvider {
def get(foo: String) = calculate(foo)
}
and passing the FooProvider instead - that way you pass something with a clear semantic meaning rather than a bare Boolean.> I'd say that at least this trivial example would be best handled by some data-driven approach. Look up salaries in a database instead.
Very much disagree. Every time I've seen a program do logic based on what was in the database it's been very hard to debug or reason about. Anything you can possibly do to make it unit-testable instead of needing to test against a prod database dump is worth it.
> Surely this is easier to misread than the if statement example, which in my mind closely matches how I think.
Disagree. The if looks like control flow when it's not actually control flow. If you're just doing Boolean logic, make it look like Boolean logic.
> But even then, that only works when the actual return type is boolean and there are no state changes made.
Yes, that's exactly the point. It's well worth forcing those things into different functions, so that you can inspect what's going on. Particularly when debugging, that means you can step over the conditional and see what the result was, rather than having to step through each if because you never know when one of the branches might actually do something.
> I don't really see how using an Optional type would remove the if(null) checks. It just makes the meaning explicit, which is good but not remedying the original problem.
Optionals have polymorphic methods so you can express most common use cases directly rather than having to branch, i.e. rather than:
val user = if(null == userId) null else userRepository.lookup(userId)
you can just do val user = userId flatMap userRepository.lookup
map/flatMap/foreach each have their own specific semantics so it's easy to see what's going on, whereas an "if(null == userId)" could mean anything. (If your logic really doesn't fit into any of the standard use cases then you may need to use an if even with an optional, but such cases will stand out when reading the code - as they should).http://www.codeproject.com/Articles/42732/Table-driven-Appro...
The point is that the desired mapping is known not just at compile time, but is explicitly known to the programmer. So it is clearer if it right there in the executable code rather than hidden away in some moving part (the mapping data structure).
Midway between nested conditionals and lookups is using flags and a tablesque series of conditionals. Redundant but easily understood (and therefore testable).
I first learned about this technique from Code Complete (linked in the TFA). IIRC, McConnell used the term "truth tables".
"...since the code is far less branchy."
Code construction GoF style Design Patterns just introduce another level of indirection. That's it. (Insert cliche here.)
"Design Patterns" are overused, especially in the Java world (eg J2EE/Spring, Service Providers, Strategies, DI, IoC).
The "design" part is figuring out the least complicated balance between conditional branches and call stack depth, between composition and inheritance.
Rant over.
function true(x, y) { return x; }
function false(x, y) { return y; }
There's an example of Church encoding used in anger at http://www.haskellforall.com/2016/04/data-is-code.html[1] - http://git.savannah.gnu.org/gitweb/?p=smalltalk.git;a=blob;f...
[1] - http://git.savannah.gnu.org/gitweb/?p=smalltalk.git;a=blob;f... and http://git.savannah.gnu.org/gitweb/?p=smalltalk.git;a=blob;f...
The solution here disperses the responsibility of keeping sumOf safe to its callers, all over the place, instead of a single location in sumOf.
(I suspect the author would prefer jumping in a lake over programming in Go.)
I think it is also worth mentioning that passing a new ArrayList over a null value is somewhat wasteful of memory. Though the reference to ArrayList will cease to exist, its allocated memory will hang out until the next GC phase. In the words of Bartleby, I would prefer not to.
I get the principle that is being advocated, but the solution code doesn't pass practical muster for me.
https://docs.oracle.com/javase/7/docs/api/java/util/Collecti...
No dynamic heap allocation necessary.
I know it's extreme but is there any way to reduce (lets say 5 to 1) conditions?
I think he was developing Fortran apps.
Anecdote: This very same professor asked us to do a matrix operation (I don't remember what) without using if once. He said it would be faster than using ifs. Many of us couldn't. Then he revealed his solution, he was getting first indice of first row and doing some operation and getting the last indice of last row, then second indice of first row and so on. One of my friend told that if he wrote the algorithm plain using ifs, it would be faster. Professor told it's impossible.
In the end of the day the version which was written using ifs was a lot faster than the version without ifs. Because of the changing indices cause a lot of cache misses but getting matrix elements sequentially used cache correctly.
It however, do represent branching, and in a hotspot it can be more efficient to simply calculate both the if and the else and just choose the right value, rather than having a branch misprediction.
In any case, always profile, and identify issues before doing any optimization.
if (!condition) {
// do stuff
}
When I want to add an else case, I swap the order so the condition is no longer negated. if (condition) {
// do other stuff
} else {
// do stuff
}
To me it's easier to read a non-negated condition and the else case then feels more natural. (It avoids having to internally think about else case as "condition not not satisfied".) function getColor(str){
if(str=='blue'){ return '#0000ff'; }
if(str=='red') { return '#ff0000'; }
}
getColor = {
blue: '#0000ff',
red: '#ffoooo'
}
The second one also has if statements inside the implementation of the hash map, but it isn't actually polluting your business logic. The question is no longer "what do you do when the input is like this or like that", but "lookup the corresponding output".The distinction is nice because I know that getColor[thing] will likely have no side effects. I can treat it, on a theoretical level, strictly as a map.
Of course if the problem is "implement this on a machine without branching" this isn't super useful but most problems are not that.
Edit: The conditional abstractions like map are useful in the sense that the conditions that they support are very simple, in case of map it is if supplied key equals that key, whereas if/else statements can be used to create any kind of complex conditions.
IMHO These are by far the two most important ways to get rid of complex code embedded with ifs.
One very good example of this is erlangs factorial method.
factorial(0) -> 1;
factorial(N) -> N * factorial(N-1).
There are many other nice examples at the erlang documentation:https://www.erlang.org/course/sequential-programming#funcsyn...
On the other hand, the article recommends using sub-type polymorphism, which is also unavailable in many languages.
I think the article was written by a company who made a static code analysis tool. They described how there wasn't a large difference in quality between FOSS and proprietary code, except for one detail: FOSS code used "else" statements much less.
Anyone have any idea what I'm thinking of or if I'm even remembering it correctly?
> Pattern 1: Boolean Params
This solution suffers from what we typeful programmers call “Boolean blindness”: https://existentialtype.wordpress.com/2011/03/15/boolean-bli... . When you compute a bit, what you are actually interested in is the meaning of the bit, which isn't included in the bit itself. For example, if the bit is set, then “x” is less or equal than “y”; if it's unset, then “x” is greater than “y”.
> Pattern 2: Switch to Polymorphism
Dynamic dispatch (what the author wrongly calls “polymorphism”) can be used for open-ended case analysis. But it suffers from two drawbacks: (0) Unless you have multimethods, case-analyzing two or more values at the same time is a bitch! (1) Even if you do have multimethods, the open-ended nature of this whole business makes static exhaustiveness checking (making sure that no case is missing) impossible.
> Patte[r]n 3: NullObject/Optional over null passing
Null objects are still falling back to pattern 2, and optionals are severely underpowered in languages without pattern matching and compile-time exhaustiveness checks. For instance, one of my favorite patterns is implementing operations (say, “merge”) on non-empty collections, then extending them to possibly empty ones:
(* pe = possibly empty *)
datatype 'a pe = Empty | Cons of 'a
(* extend a merge operation on non-empty collections
* to possibly empty ones *)
fun pointed _ (xs, Empty) = xs
| pointed _ (Empty, ys) = ys
| pointed op++ (Cons xs, Cons ys) = Cons (xs ++ ys)
fun merge (xs, ys) = ... (* possibly complicated *)
and mergePE args = pointed merge args
How do Java-style optionals help?> Patte[r]n 4: Inline statements into expressions
This only makes the problems associated to Boolean blindness even worse.
> Pattern 5: Give a coping strategy
What if there is no sensible default value? Just... no.
---
What you really need is algebraic data types, pattern matching and exhaustiveness checks.
> [Pattern 1] Solution: Split the method into two new methods. Voilà, the if is gone.
And so is parametrization.
> [Pattern 2]: Solution: Use Polymorphism. Anyone introducing a new type cannot forget to add the associated behaviour.
OTOH you make extensions more complex. I usually write in lisp and DO NOT use polymorphism for this but etypecase in the base implementation of a method (switch/case where the default case automatically throws an error). This allows people who extend my code to implemented a method specific to a type that my base implementation handles and thereby overriding my implementation with minimal effort.
> [Pattern 3]: Solution: Use a NullObject or Optional type instead of ever passing a null. An empty collection is a great alternative.
This is, IMHO, not relevant at all - the decision how null values are handled simply has to be documented so programmers can make an informed choice. The only exception I accept is an empty collection which I'll always prefer over null, assuming that all other code handles empty collections gracefully.
> [Pattern 4]: Solution: Simplify the if statements into a single expression.
Works for the contrived example (and we all know that the code is really bad). For more complex code I rather follow the logic line by line instead of mentally disassembling a 10 line boolean expression.
> [Pattern 5]: Solution: Give the code being called a coping strategy.
No, just no. One does not aim at removing exceptions. Those are to be handled by a higher layer who actually knows how to handle it. If one uses default values one still has to check on that layer with the added burden of using a default value that is guaranteed to never be returned as a real value so we can make the distinction between record found/record not found.
Similarly here, I think it's good to know how to get rid of an if statement, but keep in mind that sometimes you might want to introduce an if statement. Sometimes you want polymorphism and sometimes it's better to do the same thing as pattern matching.