IF-less programming
alisnic.github.com
alisnic.github.com
" ifs are a smelly thing in a Object Oriented language."
What's smelly is such a absurd assumption. Yes, let's replace every small decision in our program with inheritance and another level of abstraction, that will work out swell
Please go read an Assembly manual because you have obviously forgotten what a computer is and does
And then we end up with unmaintainable systems with a class hierarchy higher than the Empire State.
Of course, as mentioned, replacing an if with a "software structure" (like pattern matching in an functional language) is beneficial, but to call it a code smell is ridiculous
It is partially my fault because I failed to express what I really ment. English not my native, so sorry.
What I really ment in this sentece is "why using (a lot of) ifs can be a bad practice in a Object Oriented language"
Or maybe better: how can ifs be replaced (sometimes) using OO principles
If you've been writing OO code for 10-15 years, you evolve into a model which is pretty if and block free. It just sort of happens one day. It isn't decomposed into millions of classes either - just a data model and some kind of effector such as specification/Builder/visitor. SRP only needs to go as far as method really as well.
I don't use functional pattern matching either.
If your code reads like math, I expect that its probably done right.
var minutesToAddToAlert = (timeToAddInMinutes) + (900 * ((timeToAddInMinutes +
(currentTimeOfDayInMinutes - 510)) / 540)) +
(2880*(((currentTimeOfDayInMinutes + (1440*dayOfTheWeek)) +
((timeToAddInMinutes) + (900 * ((timeToAddInMinutes +
(currentTimeOfDayInMinutes - 510)) / 540))) ) / 7200));
var nextAlertDate = currentDate.AddMinutes(minutesToAddToAlert);
There is definitely a lot more to it than just this, but I don't like blocks of code that don't fit on my vertical 24 inch screens. So I always end up converting "logic" (if's and else's) to math. It's a pet peeve of mine (as is overusing ternary operators as my own personal way of doing haiku), and while I certainly decrease the vertical size of the code itself, it makes it a hell of a lot harder to understand for everyone else. I then have to write comments explaining what stuff like this does, and those comments can get rather obtuse depending on the time of day.Everyone winces when they see my commits with this message: "Math. Converted X and Y to M-A-T-H. Math. :D"
var accumulator = new TimeSpan();
accumulator.Add(TimeSpan.FromMinutes(currentTimeOfDayInMinutes));
accumulator.Add(TimeSpan.FromDays(dayOfTheWeek));
// ... etc - can't be bothered to do the rest...
var nextAlertDate = currentDate.Add(accumulator);
No magic numbers, no arithmetic and clear to anyone who understands the TimeSpan class (which is the all powerful lord of the fourth dimension in .Net).Oh and I work on a 1280x800 screen on an old ThinkPad T61. If the function doesn't fit on that, I'm concerned :)
In my defense, moving the logic to a pure arithmetic solution resulted in a procedure that takes a bit less than one third of my screen, while the old one was at least 6 or 7 pages of scrolling through nested conditionals.
This is not about getting rid of "if" (as he mentions in the talk), this is about structuring large software systems for maintainability, extensibility, testability, etc.
In any decent language, if will be an expression, not a statement. There is no difference between if and a function.
In Lisp: (setq a (if b c d))
Only Lisp implements them as a function (more precisely, a macro), but to the programmer there's no difference between using if and calling a function.
Of course, they don't compile into function calls in assembly code :)
You can write: a = b ? c : d; in C/C++, you can't write a = if(b) { c; } else { d; }.
http://www.lispworks.com/documentation/HyperSpec/Body/03_aba...
IF can't be implemented as a function because of evaluation rules.
I still thought you could pass macros around like normal functions in Common-Lisp. The Lisp interpreter I wrote for fun could do that since the function value is resolved before arguments are evaluated for late macro expansions.
def iff(cond, x, y)
if cond then
x
else
y
end
end
puts iff(true, 1, puts('2'))
echoes 2
1
if may behave as a function, but you can't write a function that acts like if.edit: R does have real lazy evalution, e.g.
> iff <- function(cond, x, y) { if (cond) x else y }
> iff(TRUE, 1, cat('foo'))
[1] 1 if x then y else z
is sugar for: case x of
True -> y
False -> z
There have been suggestions to replace this sugaring with a proper "if" function, e.g.: if :: Bool -> a -> a -> a
if True x _ = x
if False _ y = y
to remove arguably redundant syntax, but it seems unlikely that this change would happen. See more here: http://www.haskell.org/haskellwiki/If-then-elseThere is a huge difference between an if and a function. Consider SICP where lazy evaluation is introduced (read it if you haven't). An if statement encompasses the lazy evaluation as a native concept whereas a function's arguments are always evaluated (assumption for now - please carry on reading).
So consider an if statement as a control flow structure which automatically supports lazy evaluation and leaves the return semantics up to the user. This abstraction is clean from the highest level, right down to the CPU which uses conditional branching to perform the if operation. There is a 1:1 match all the way down.
The function on the other hand does not natively support lazy evaluation and enforces the return semantics. You then have to apply a lazy evaluation mechanism over the top (another layer of abstraction!). This layer of abstraction conveniently ends up using conditional branching to perform the if operation when it gets to CPU level. There is not a 1:1 match all the way down.
Big hint there: your functional if at the end of the day is just a compiler abstraction over a state machine which uses ifs.
As for OO, the industry I think has decided who won that battle.
But alas, no religious war is required. Use what tools work for you.
"your functional if at the end of the day is just a compiler abstraction over a state machine which uses ifs"
Yes, that's the 'sad' part of it. In the end there's the Intel/AMD/ARM chip and nothing more
Perhaps, but I would rather have testable code than untestable code that is nothing more than a series of unnecessary, nested if blocks. We have all seen code that has a structure like
if (depObj1.isSomething()) {
if (depOboj1.getAttr() == CONST1) {
// Do something
} else if (depObj1.getAttr() == CONST2) {
// Do something different
}
} else if ... {
// repeat with minor differences
}
Most of the time the above structure can be abstracted. Abstracting It also has the benefit of making this code more testable: you don't have to set up many different objects, just one.The OP is correct: in OO languages if statements are a code smell. Like all smells, though, they are not hard and fast rules, but rather indicators that things could probably be done better. No one, certainly not OP, is recommending abandoning if statements. What he is recommending, and I agree with, is that they can and should be avoided, and certainly not turned to as a primary tool in the toolbelt.
Now, you could make depObj1 of type Obj1 and Obj2 to replace the if, which may simplify testing because the method is "simpler" in Obj1/Obj2/ObjBase but in the end it isn't!
Why? Because your test of Obj1/2 has to take care of their dependency to ObjBase. It's usually a dependency hell, needing lots of workarounds to test properly.
For example I recently did some work on a graphics library which had a lot of ifs to do with line and fill styles. By changing those into an inheritance hierarchy I found a couple of places where clauses had been missed and so fixed some bugs, but more importantly when we needed to add new fill styles which were more complicated to draw it became very easy to turn the fill style objects into fillers which had all the responsibility for filling paths, not simply setting the graphics state.
But this doesn't mean that it's better to completely eliminate ifs
You do introduce more named test points, but you risk of creating too many tests at a too small granularity, that will only slow you down for no good reason. Consider using a code coverage tool to help you navigate all the branches, without explicitly naming them.
Keep in mind readability. Jumping around source files is costly on the reader, don't break the flow if you don't have to.
I would suggest that decision tables, rule engines, workflow, DSLs, even finite automaton and other abstractions etc are better than having many many objects that are avoiding ifs :)
If we wanted to program in assembly, we would be programming in assembly.
OO polymorphism is closed for functions but open for extension. You have a fixed number of functions (methods), but you don't need to update all the call sites if you add a new data type - merely implement all the methods.
The requirement for what needs to be open and what can be left closed is what determines which choice is better.
For example, a GUI framework is best left open for extension, because every application GUI usually ends up with specific widgets custom-designed for that app - sticking with standard widgets tends to make apps form-heavy and lacking in polish. But for the widgets to work in the framework, they need a fixed set of methods to be consistently manipulable.
A compiler AST is best left open for functions, because the majority of work on an AST is in algorithms that transform the AST. The language changes at a much slower pace than additional optimizations and analyses, and frequently new language additions can be represented in terms of existing AST constructs (where they are syntax sugar, effectively built-in macros). So having a more fixed set of AST node types is less of an issue.
Choosing one or the other on the basis of the orthodox religion of OO or functional programming, meanwhile, is just obtuse.
That said I do think all things in moderation. At some point if you abstract the logic so far out humans have a hard time reading it or computers have a hard time optimally running it then you've lost the gains. I can think of several times when I was coding something using a class and noticed I had to write a lot of logic to really use the class the way I wanted. I wish in those situations the class was better designed to avoid spaghetti code to use it.
That said many times the use of an object is nothing like what the original author thought it would be. It is hard to think in advance about every potential use case, shoot for the most common cases and make it all readable. Hopefully later someone else will come along and make it more useful to the real world use cases.. :-)
Note that it's relatively straight forward to express the function-extensibility through the visitor pattern in an OO language; in a functional language, you could probably implement your own dynamic dispatch technique to get the type-extensibility.
In both cases, you'll end up with some boilerplate, depending on your language. E.g. for a complex language with lots of different AST node types, you'll end up with lots of tiny classes, mostly just implementing stupid constructors.
Frequently you want to be able to pass arguments down and return values up the tree traversal. Using visitors, you need a whole separate class for each unique function arity, or you have to use tedious little data carrier instances. Sometimes you want to switch between them half-way through the traversal, e.g. use your constant-folding visitor during an optimization visit, and the visual overhead of constructing visitors and keeping track of them, making sure they're all linked together properly, is really ugly. Don't even think about matching the visitor method on more than one parameter type, like you might do with e.g. overload resolution of binary operators. I've been there working on a commercial compiler, and I don't want to go back there again.
Haskell can implement the OO style using existential types (forall), a bit like Go's interfaces, except once you put a value into an existential type variable, you can't typecast it back out again.
Not only that, but using the visitor pattern tilts the extend types / extend functions into extend functions realm, soundly defeating the supposed advantage of being open to extend on types. As such, visitor pattern is precisely the same as a switch statement.
The only difference left is language specific. In Java, adding a new type will make your compiler whine if you have abstract methods in the visitor. That's because javac can't tell when a switch over enums is exhaustive.
Although on emight say that passing functions (closures) or records of functions explicitly might be a more idiomatic pattern.
def method (object)
if object.property
param = object.property
else
param = default_param
end
end
And suggests that this is better: def method (object)
param = object.property || default_param
end
To me, this is an example of an if statement by another name. Sure, you cut out a few lines, but it's still an if-then construct. Both examples are most likely going to be treated the same way by the compiler as well.We need to think about the effect our choices have on the next person who touches the code. Often with a different baseline people make different changes. It's a micro-form of 'No Broken Windows'.
And that next person might be the newbie on the team that doesn't have the business domain knowledge built up over years of working for This Company. So I'll use the if statement to keep intent clear.
Or maybe it'll be someone whose primary programming language doesn't use the || operator. Or maybe, for some unknown reason, it'll be a PHB. So I'll use the if statement to keep intent clear.
"I want to set param to the object property or the default" is much clearer than "I want to check if if I have an object property and if I do then I'll set the param to the object property otherwise I'll set the param to the default"
That is just mixing in a lot of noise about how you do it instead of just what you are doing.
> And suggests that this is better:
It depends in what terms are you thinking. A syntactic improvement is a also a benefit, isn't it?
param = object.property.nil? ? default_value : object.property
Verbose, although less so, but reveals intent almost perfectly. Would be better if the colon were "else" or "otherwise".I'd rather add a method to Object/Nil called something like #or_else so that we could write
param = object.property.or_else(default_param)
which, though still terse, at least describes what's going on.The OR is not an if-then construct. If anything, the if-then construct is a specialised use of OR, given than if-then-else is literally XOR.
Both are an implementation of the interface "coalesce a value with null/0/false".
I only mention this because it points to a lack of precision in our thinking which I see time after time get in the way of using OOP/OOD effectively. Maybe that's a failing of OOP, but I think that if it were, it'd be a failing of programming in general, too.
This helps explain why I teach OOP the way I do: start by following the rules of removing duplication and improving names, which encourages the programmer to ask ever-more-interesting questions about what tools are available to help follow those rules, which encourages the programmer to learn OO theory as needed, rather than having it shoved in their face all at once. This just-in-time learning leads to longer-lasting understanding for many (most?) people.
def method (object)
param = default_param
if object.property
param = object.property
end
end
Here, its clear that param has a default right away.I do agree that people are massively overusing inheritance. In particular its often used as a means for code reuse, not for polymorphism. In most cases composition should be favored over inheritance.
I think the if-less style is great kata material. You learn a lot when you apply it on a toy project rather than a real one. Sometimes you need to overdo things in order to understand what their limitations are.
I am all for Design Patterns and abstraction, but only when it makes sense.
We have a rule about only using patterns if they become apparent rather than selecting a pattern up front.
You can't quantify over-engineering until it is done unfortunately.
It's an interesting article though, especially the conclusion: "Any language construct must be used when it is the more reasonable thing to do. I like the fact that the presence of if-s can indicate a bad OO design."
What they all are getting at I think is that constructs in a language can be "abused" or used less than effectively either in terms of processing efficiency or user/programmer interaction.
So when they say "don't do this" it's more to show cases where doing whatever "this" is can be bad. Not that it's always bad and should never be used.
Also, the "don't do this" mentality can be a useful exercise to teach yourself alternative ways of getting things done.
Here is my experience with architected extension points:
1. Generally the first extension point used set the trend, other developers will follow the "pattern" blindly. Hacking to make it fit rather than use another more appropriate extension. That is both bad and common (everyone has had to work with too little time)
2. There is often a sharp refocus close before or after release 1.0. A lot of the extensions disappear at that stage (demo, experimental feature, cross-platform/framework support, performance targets are set, security infrastructure is decided, server setup, integration test env. available instead of simulated, ...). Structural change (like removing extension point) become very difficult to justify after release 1.0.
3. Technical debt is very often called "selling feature" at management level.
But yeah, real world code sucks.
A few thought though :
- If you really push this to the extreme, you'll end up with all the logic hidden in the classes inheritance hierarchy. I'm not sure this is more readable/extensible than if/else statements.
- Most of the example given by the author to use "language features" are just syntactic sugar. Using collection.select instead of collection.each or || instead of if else is really just a matter of notation. I doesn't reduce the number of test cases required for your code and it might lead to "magical" one-liners that you have to read 20 times to understand.
That said, when it comes to extensibility, pattern matching is more similar to if statements then the OO - its easy to write new functions but hard to extend the original ADT with new cases. If being able to add new functions is important it might be better to use if statements then to do a major rewrite to use OO instead. (You could always use a visitor pattern if you want the extra compiler safety but that can get very complicated, IMO)
Trying not to sound sarcastic, but if people are for if-free programming, pattern matching does not seem to be the answer for me. When I add a new type in haskell, I usually find myself having to look through all my pattern matchings.
Mostly, yes. Pattern matching also provides destructuring, allowing you to bind constructor arguments and pattern match against such arguments as well. For instance:
fun (Just (x:_)) = ...
But some of the downsides are comparable to switch statements, e.g. if you modify: data MyType = Foo | Bar
to data MyType = Foo | Bar | Baz
You will have to (potentially) update all functions or case expressions to account for Baz. One could use parametric polymorphism, comparably to the linked article, to make more extensible code. In such a case, one would define a type class such as: class (Show a) => Printer p where
printIt :: p -> a -> IO ()
And one could make particular printers of this typeclass. You could even throw in existential quantification so that a function does not specialize to a particular Printer. tail2 xs = case xs of { Nil => []; Cons(x,xs) => xs }
tail2' xs = if xs == Nil then [] else tail xs
In the second case, tail2', the compiler won't stop us if we switch the two branches. In the first case, tail2, we only get access to the tail of the list if the list is actually non-empty.In essence, the difference is that if statements throws away any static information about the test result, whereas pattern matching constructs lets that information flow to each branch through variable binding.
But, obviously, don't take this too far. If you find yourself not using an if statement (or ternary operator) when writing the absolute value (for non-optimization reasons)... you've gone too far.
int UnclearAbs(int value) {
return value * (value >> 31);
}Obviously many things are ultimately implemented using if, but it's too low-level a construct to be using for day-to-day work.
result = collection.select(&:condition?)
The "&:proc" methods are (very, very likely) slower and they also "leak".When I say "leak", the VM doesn't Garbage Collect the parameters of the proc until it is used again. Most of the time this is fine, but when it's not, you're wasting considerable amounts of memory. This is known and is considered within the spec.
I know they are semantically equivalent, but the MRI is doing something weird internally. (ps. Learnt this the hard way).
IF:
def method (object)
if object.property
param = object.property
else
param = default_param
end
end
Claimed to be IF-less: def method (object)
param = object.property || default_param
end
It may be easier to read, but in the end you are still writing an IF statement.<code> param = if object.property then object.property else default_param end </code>
Though, actually if you want to do what the description says (use the default if the property is unassigned, represented conventionally in Ruby by the accessor returning nil) rather than what either the good or bad code does, you probably want:
<code> param = if not object.property.nil? then object.property else default_param end </code>
(The difference between this and the other versions shows up if the property is assigned, and its value is false.)
Text after a blank line that is indented by two or more spaces is reproduced verbatim. (This is intended for code.)
http://mike.zwobble.org/2012/12/polymorphism-and-reimplement...
I'm definitely not advocating this as good programming practice, but the point is that if you're used to always using if statements, then it's hard to learn alternatives. By forcing yourself to use the unfamiliar, you might find some situations where polymorphism is better suited to the problem, whereas you would have previously defaulted to using ifs.
(barrkel has already left an excellent comment on when the two styles are useful, so I won't repeat it:
Also, "# I slept during functional classes"? I don't know ruby, but the `each` method seems to be just a variant of map, which is a pretty fundamental functional construct.
I have seen cases of radiation-derived bit-rot which don't manifest in any way until a certain "if"-path is evaluated by the computer - this seriously does happen and can still happen in modern computers today.
Having an abundance of such code switch points in a particularly large codebase can be a degree of complexity that nobody really wants to manage - or in the case of disaster, be responsible for .. so this maxim has been pretty solidly presented in industrial computing for a while. Make the decision-making as minimal as possible to get the job done, and don't over-rely on the ability of the computer to evaluate the expression in order to build robust software.
Now, its sort of amusing that this has propagated into the higher-order realms of general application development by which most Class/Object-oriented developers are employed .. but it is still an equally valid position to take. State changes in an application can be implemented in a number of different ways, "if" being one of the more banal mechanisms - there are of course other mechanisms as well (duffs devices, etc.) which are equally testable, yet more robust - simply because they break sooner, and can thus be tested better.
I take the position, however, that a well-designed class hierarchy won't need much navel-gazing decision-making, which is what the ol' "if (something == SOMETYPE)" statement really is: a kind of internal house-keeping mechanism being done by the computer at runtime, instead of at compile-time.
So there is a balance to this maxim, and the key to it is this: how complex does it need to be, versus how complex can the codebase be before it becomes unmanageable. If you're not doing full code-coverage testing with 100% testing of potential codepaths, then every single if statement represents a potential bug you didn't catch yet.
(reaching into my way back machine, ifs essentially compile down to a few comparison instructions (which are often just subtractions) and a jmp instruction (depending on the platform), it's literally built into the processor! For a simple if statement we might be talking a handful of cycles to eval the if vs an extended call stack pumping and dumping exercise)
to clarify: my question "why is this considered good?" isn't about "if-less programming", but about taking ideas to dumb extremes.
If-then-else ladders tend to evolve to be very difficult to understand, maintain and debug
Don't limit yourself just to blindly comply to some silly idea. Use everything you know to get the job done, and once you get it working, make it beautiful.
If statements are an incredible tool. Just ask any Erlanger and they will either tell how much they miss it, or just lie to your face. ;)
I long ago abandoned else clauses. It was a short time thereafter that I realized that if statements themselves weren't all that necessary, most of the time.
A = 1,
A = 2, % fatal error because 1 != 2
So, yeah, variabless programming FTW!
1) correspond to computer architecture (which excuses the distance from human thinking)
2) correspond to human thinking (which excuses the distance from computer architecture)
So what's the excuse for pulling such strange rules out of nowhere? The sort that have no counterpart outside of the language itself? Is it just for the sake of making programming more of a puzzle, or...?
The reason is simple, with immutability you know what the value is, and you can pattern match on it with confidence.
Variables that can change value are a mini-version of global state with all the reasoning problems that 'globality' gives:
"what is the value at this point in the code?"
"when does the value change"
"what range of values can this have depending on which code path was executed?" ie sometimes the value is changed and sometimes not...
Trust me, once you have gone immutable, you don't want to go back.
Any given Erlang process has meta-information about itself, how many reductions it has, how big its heap is, which flags are set. These are the global state of the process.
The process dictionary allows you to store and manipulate your own global state of the process - and the people (hands up, that includes me) get smart and use it as local state of the programme and then get their bum bitten badly and swear never to dance with the dark side again... :(
Not bitter :)