
Special Cases Are a Code Smell - fagnerbrack
https://blog.conjur.org/special-cases-are-a-code-smell/
======
toolslive
One of my more painful anecdotes goes like this: Years ago, we were coding a
piece of government regulation that decides eligibility of some kind. The
rules went something like this "if it's a man and over 60 or over 55 and part
of these classes of occupation but not in .. or ... then .... If otoh, it's a
woman and ... ". The law was written down in the exact same wording. We coded
it in the same way 12 pages of conditionals split out into different functions
to make it more readable. Then I came, armed with my knowledge of algebra,
Cayley tables and optimization of boolean functions. I created a truth table
for the boolean function at hand, optimized that function into a small
equivalent expression and replaced the 12 pages of code by a one pager. A job
well done.

Two months later the law changed adding a new special case somewhere deep
inside. I could start over. Lesson learned: don't outsmart the logic of the
business.

~~~
mrweasel
>Lesson learned: don't outsmart the logic of the business.

Very often when people try to solve problems, like the one you faced, with
software, they fail to recognize that you might have to rethink the business
logic. It's my belief that the majority of software design for specific niche
business and governments fail because there was a lack of willingness to
change and simplify the rules.

Of cause a project create a new system for taxation will fail, if you have
6000 pages of rules, not including the tax law itself.

Often we think we have a software bug/issue, in reality what we have is a
flawed business logic. Until such time the people in charge fixes the logic,
we're not able to do anything clever of efficient in software.

~~~
toolslive
You're right. But when a government is involved, you cannot even start
explaining to `the business` they have flawed logic. (Believe me, I've
tried... and failed)

~~~
Aeolun
When a business with multiple layers of management is involved it also quickly
becomes impossible.

“It was decided!”

“By whom? Why?”

“???, It was decided!”

“...”

~~~
bluGill
It was decided over dozens of meetings with dozens of people, and there are
not good notes. Those people are smart and considered more details than you
did (but not always the same details). There is rightly fear that if we
reconsider this again we will forget one of those other details and some up
with a decision that is better for your case but worse overall because it
breaks some other case.

~~~
toolslive
Isn't it more likely they just keep on adding little exceptions in order to be
able to show they pushed something through for their constituents? That's the
impression I got when reading these laws in the first place (a bone for the
liberals, a bone for the socialists, ....)

~~~
bluGill
Depends. Sometimes things work that way, sometimes they do not. It depends on
how much attention the extras would bring.

------
fenomas
I'm profoundly unconvinced by this. "Handle special cases, then do the thing"
is such a widespread, nigh-universal pattern that I don't think it hides
anything from the programmer. It's the pattern used by any function that
checks its inputs!

Rejiggering an algorithm so as not to have special cases just puts cognitive
load on whoever reads the code to suss out what the original algorithm was.
"Why does this algorithm require its input array to be padded with zeroes? Oh
I see, it doesn't - the developer just did that to remove some if statements."

~~~
dpark
This feels very much to me like architecture astronaut behavior. This
overwhelming drive to pull "elegance" out of everything mundane or tediously
complicated.

Yes, you can sometimes rewrite your algorithm to eliminate "special cases",
but if you do it at the cost of comprehensibility, it's not a net gain. In the
case of these examples, we're not really even talking about "special cases",
but edge cases, where you literally need to deal with an edge in the data. In
the case of the neighbor sums, edge cases were masked by padding. In the case
of the staircase, the edge is just obfuscated by the doubled labels and then
hidden in a modulo operation. These are totally fine except that they seem to
provide no value except "elegance", and they come at a high cost in terms of
understandability. (The first step is literally "transform to a different
problem", so now you need to understand the original problem as well as the
new problem, and how they relate.)

I'm all for elegance when it comes bundled with understandability or
legibility or performance or even simplicity. But often it actually means
"clever" and it's an extremely subjective measure that delivers little to no
value and is prone to causing problems later.

~~~
arethuza
That first example in the article was interesting because it made me realise
that I have a strong aversion to pushing the complexity into the data rather
than into the code. Modifying the list seems to me that it could be a cause of
other bugs (what if the list is re-used and something depends on it?).

~~~
DarkWiiPlayer
Never mind bugs, imagine that array has 5 million elements. Good luck re-
allocating and copying that to add an element at the start and another at the
end.

~~~
dpark
I don't like the approach of pointlessly reallocating the array this way, but
5 million elements is not a problem on any machine built in the last 20 years.
That's just 20 megs for 4-byte integers. With modern GC, this is cheap. It's
not something you'd want to do in a tight loop (the copy time will start to
matter), but for something like the example, the cost (in machine terms) is
negligible.

------
chrismorgan
The first example is not at all good. It does not, in fact, eliminate the
special cases, and becomes almost certainly less efficient—demonstrating why I
would say that for this _particular_ case a Good Programmer would write the
code in the _first_ form and not the second.

The special cases: they are not removed, merely transformed from `if`
statements to boundary conditions on the iterator. The `i == 0` and `i ==
input.size - 1` checks effectively move into the iterator, with how the nice
and clear `input.map.with_index do |_, i|` becomes the magic
`(1...padded.size-1).map do |i|`. The 0 added to the start and end are also
easily arguably special cases.

The second form is almost certainly less efficient because you’re creating at
least one new array, unnecessarily.

> _The special cases are gone, and the simple algorithm shines through._

The algorithm does shine through more, but the special cases are _not_ gone,
merely transformed (and into an arguably _less_ clear form), and the code is
less efficient.

Simplifying things is a worthy goal, and a lot of special cases _are_ code
smells, but be careful in seeking to remove them, and remember to at least
_consider_ performance, even if you end up deciding to prefer succinctness
instead.

~~~
ballenf
The repeated if checks inside a loop could easily be less performant than the
O(1) operation of adding 2 items to a array.

I think you’re too quick to assume that your preferred solution is faster.

Personally, I’d use the more clear code and only if the code proved to be a
bottleneck even consider refactoring for performance. Whether the author’s
preferred solutions are clearer, I’m not convinced however.

Finally, the author doesn’t claim that the special cases are _gone_ , but more
so that the input is transformed in a way to allow processing that is blind to
them.

~~~
drdrey
Appending to an array is O(1). Prepending is going to be O(n).

~~~
smitherfield
Prepending would be O(1) because it's creating a new array instead of
prepending in place. Still bugs me (seems inelegant, even if not necessarily
inefficient) so I wrote my own version:
[https://news.ycombinator.com/item?id=18988075#18998572](https://news.ycombinator.com/item?id=18988075#18998572)

~~~
boomlinde
_> Prepending would be O(1) because it's creating a new array instead of
prepending in place._

No, you still need to copy the old array to the new array.

FWIW, Ruby may already be allocating some space before and after the array to
accommodate a few pre-/appendages.

~~~
smitherfield
_> No, you still need to copy the old array to the new array._

That's just a lock (nontrivial but O(1)) and a memcpy (technically O(n) but
trivial, and O(1) for the common case if it's implemented with vector
instructions), plus in any event the sums-of-neighbors method has to be at
least O(n) on an idealized Von Neumann machine because it must read every
element of the source array and also write every element of the destination.

~~~
boomlinde
In other words, O(n), not O(1).

 _> technically O(n) but trivial_

"Technically" O(n) is the only O(n). There isn't some widespread colloquial
use of Big O notation where O(n) means something else. Whether it's trivial is
beside the point, but for a large n, O(n) in both time and space can be
prohibitive, and it may become important that I don't use such an algorithm.
For example, if I have 8 GB of data and 12 GB of working memory I can't
satisfy those space requirements.

 _> and O(1) for the common case if it's implemented with vector
instructions)_

What is the common case in your view? memcpy in the general case is O(n). That
you can perform multiple copies in parallel might affect the real time, but it
doesn't affect the time complexity because O(kn) = O(n) for a constant k even
if that k = 1/16 or however many copies you can perform at once.

 _> plus in any event the sums-of-neighbors method has to be at least O(n) on
an idealized Von Neumann machine because it must read every element of the
source array and also write every element of the destination._

O(3n) = O(2n) = O(n)

~~~
smitherfield
_> "Technically" O(n) is the only O(n)._

In idealized algorithmic analysis, but not necessarily real life. "Amortized
O(1)," which I assume you concede is a commonly-used, meaningful and
legitimate term, means "technically" an idealized O(>1) but O(1) in practice.

Calling memcpy inside a Ruby method call is amortized O(1) because for any "n"
that fits within available memory, it will always be _much_ faster than the
other things in a Ruby method call, which involve dozens of locks, hash table
lookups with string keys, dynamic type checks, additional Ruby method calls
and so forth.

Likewise, computational complexity on an idealized Von Neumann machine isn't
always the same on a real computer, in both directions. Dynamic allocations
are theoretically O(n) but may be O(1) if the program never exceeds the
preallocated space. Or suppose there were a loop over an array of pointers
which dereferenced each pointer; the dereferences are theoretically O(1) but
may be O(n) if they evict the parent array from the cache.

 _> What is the common case in your view?_

Such as an array small enough that it can be copied with 10 or fewer vector
load/stores.

 _> O(3n) = O(2n) = O(n)_

Yes, that's my point. It's impossible to implement the example in less than
idealized O(n) time, so O(n) and O(1) operations are equivalent complexity-
wise WRT the entire method.

~~~
boomlinde
_> In idealized algorithmic analysis, but not necessarily real life._

Big O notation is used for idealized algorithmic analysis. If you want to talk
about real life, you can count cycles, seconds, watts etc.

 _> "Amortized O(1)," which I assume you concede is a commonly-used,
meaningful and legitimate term, means "technically" an idealized O(>1) but
O(1) in practice._

Yes, but I wouldn't take O(1) on its own to imply amortized complexity. Not
that pretending that an array copy is O(1) in practice is particularly useful
here since if you measure a copy operation in practice, you'll find that the
time it takes scales roughly linearly with the size of the array. Not to
mention that the space complexity is O(n) no matter how you put it.

 _> Such as an array small enough that it can be copied with 10 or fewer
vector load/stores._

Are other cases conversely "uncommon"? My point here is that this is entirely
your opinion and doesn't pertain to whether an array copy is O(1) or O(n)
complex.

 _> Yes, that's my point. It's impossible to implement the example in less
than idealized O(n) time, so O(n) and O(1) operations are equivalent
complexity-wise WRT the entire method._

Not in terms of space.

------
gumby
These suggested fixes look worse to me. In the first one, the "wrong" function
was quite clear (special case 0, special case 1, general case) while the
second one _allocated memory_. I'd call that a code smell!

~~~
eridius
Agreed. A better solution here is to have a generic accessor on the Array that
can return a default value for out-of-bounds accesses. There almost is one
already, the two-argument version of fetch(), except that interprets negative
indices as counting from the end. But if we create a variant that doesn't
treat negative indices specially then the code becomes very simple

    
    
      result = input.map.with_index do |_, i|
        input.better_fetch(i-1, 0) + input.better_fetch(i+1, 0)
      end.to_a

~~~
chrismorgan
Yeah, in Rust it’d fall very neatly out of the type system, as `input.get(i -
1).unwrap_or(0) + input.get(i + 1).unwrap_or(0)`.

~~~
eridius
Funnily enough, that Rust code is actually buggy, because i is a usize and if
i == 0 then i-1 will panic due to underflow. You'd have to write

    
    
      input.get(i.wrapping_sub(1)).unwrap_or(0) + input.get(i+1).unwrap_or(0)
    

This also technically won't work right if the slice has usize::MAX elements,
but that's rather unlikely.

This is one area where Swift's choice of using signed Int for most things
actually works quite well. The equivalent Swift code would be something like

    
    
      (input[safe: i-1] ?? 0) + (input[safe: i+1] ?? 0)
    

though unfortunately Swift doesn't ship with Array.subscript(safe:) so you
have to write that yourself.

~~~
GlitchMr
A slice of a non-ZST element cannot have `usize::MAX` elements, by definition
of `usize`.

~~~
eridius
Slices of ZST elements is indeed what I was thinking of, but is there anything
actually stopping me from constructing a `std::slice::from_raw_parts(0 as
*const u8, usize::MAX)`? What if I'm working on an architecture where the heap
and stack are separate address spaces, and every single address in the heap is
reachable? In fact, doesn't WebAssembly work this way? If I can declare that
my linear memory is usize::MAX bytes, then constructing a slice that covers
the entire memory address seems like a potentially-valid thing to do.

------
clusmore
I'm reminded of Linus Torvald's interview at Ted [1] where he talks about
programming "taste", and compares the following two code snippets:

    
    
      remove_list_entry(entry)
      {
          prev = NULL;
          walk = head;
    
          while (walk != entry) {
              prev = walk;
              walk = walk->next;
          }
    
          if (!prev)
              head = entry->next;
          else
              prev->next = entry->next;
      }
    
      remove_list_entry(entry)
      {
          indirect = &head;
    
          while ((*indirect) != entry)
              indirect = &(*indirect)->next;
    
          *indirect = entry->next;
      }
    

[1]:
[https://www.youtube.com/watch?v=o8NPllzkFhE](https://www.youtube.com/watch?v=o8NPllzkFhE)

~~~
userbinator
There's a third alternative, which I've heard of as "virtual head", that
removes the extra indirections in the loop and removal:

    
    
        while(p->next != entry)
         p = p->next;
        p->next = entry->next;
    

I've deliberately not shown the initialisation of p, because it's a bit tricky
in C (but trivial in Asm); p is not initialised to the head, nor the address
of the head, but to a "virtual" node centered around the head, such that
p->next will initially access the head. If the next field is at the beginning
of the structure, p does point to the head; else p points to a location
_before_ the head. It would be something like (char*)&head - offsetof(Node,
next);

~~~
rovolo
For those curious how to create the "virtual head" in C:

    
    
        struct entry { int value; entry * next; };
    
        entry * head = NULL;
        void remove_entry(entry *target) {
            long offset = (long)&((entry*)0)->next;
            entry * curr = (entry*)((long)&head - offset);
    
            while (curr->next != target)
                curr = curr->next;
    
            curr->next = target->next;
        }
    

But I'm not sure how portable that is. It is portable if the next pointer is
the first member of the struct though. From 6.7.2.1.13 in the C99 standard:

>A pointer to a structure object, suitably converted, points to its initial
member

    
    
        struct entry { entry * next; int value; };
        ...
            // virtual entry where &(curr->next) = &head
            entry * curr = (entry*)&head;
    

Note that it's exactly the same logic as the grandparent since

    
    
        *curr = curr->next
    
        indirect = curr
        *indirect = *curr
        *indirect = curr->next
        indirect = &(curr->next)
    

But, I think that the virtual head entry is an easier mental model.

C99 standard: [http://www.open-
std.org/jtc1/sc22/WG14/www/docs/n1256.pdf](http://www.open-
std.org/jtc1/sc22/WG14/www/docs/n1256.pdf)

------
khendron
This is the coding equivalent of a designer putting appearance over usability.

In this case, it is putting code elegance over readability/maintainability.
Sure, you've reduced the number of if statements, but those if statements
explicitly represent the logic behind the calculations. Come back 6 months
later, and you'll be scratching your head about what's going on. Unless you
add a comment, but comments are also a code smell.

~~~
riskable
Comments are a code smell? WTF? No. Just... No.

 _Not_ having comments is a code smell. _Everything_ has context and it is
wrong to assume that the next developer to come along will know/understand it
well (or at all). Even _you_ as the original coder might not remember why you
did something a certain way.

I personally comment like I'm going to start suffering from a massive
cognitive decline any day now and will need most things re-explained to me.

Example: I was looking at some old code this morning...

    
    
         temp.write('\n')
         temp.close()
    

Why's that newline being written there? From the perspective of the code it
serves no propose. Good thing I had a comment right above it...

    
    
        # Add a trailing newline so 'cat' doesn't leave an ugly mess
    

"Ah, yes. That's a good reason to add a newline."

~~~
Retric
You misunderstood.

The _need_ for comments means the code is not clear. That need is a code
smell.

~~~
magicalhippo
I think that's too broad.

Needing comments to explain _what_ the code does is a smell.

Needing comments to explain _why_ the code needs to do what it does is not a
smell.

~~~
Retric
I don’t think zero comments should be the goal, just that minimizing the need
for them is often a good idea. _Why_ can generally be included in the code.
Total = SubTotal + SalesTax; Needs no real explanation.

Code smell is just another way to compare two possible approaches. If one
version can include significantly fewer comments without issue then that’s
good sign.

~~~
EpicEng
>Why can generally be included in the code. Total = SubTotal + SalesTax; Needs
no real explanation.

Sure, and if all the code you ever write is implementing some trivial school
assignment then your probably fine. No one with half a brain thinks your
example requires a comment, but it's a bad example.

~~~
Retric
No offense, but functionally like that shows up in school assignments and
billion dollar companies back ends. However, it’s easy for even that simple
idea to end up being something like: Item.FinalPrice = Item.BasePrice *
CalculateModifier(Item.ItemCode, Item.OrderContext); Which sacrificed
understanding for elegance.

Sure, the functionality to find that SalesTax number might take tens of
thousands of hours to create and cover a multitude of egde cases. Still with
proper context, structure, naming conventions, etc the why’s should be clear.
If your thinking “The exceptions function adjusts for tax holidays. So of
course it needs to get exceptions based on location and the order date, and
then it needs each items metadata etc” then that’s a great sign.

Elegant code is elegant when it encapsulates the why’s not just the how’s.

~~~
EpicEng
>However, it’s easy for even that simple idea to end up being something like:
Item.FinalPrice = Item.BasePrice * CalculateModifier(Item.ItemCode,
Item.OrderContext); Which sacrificed understanding for elegance.

Sure, and I'd call that bad code unless it exists because there are far more
considerations than sales tax. Either way I don't see how that is an example
of when to or not to leave a comment.

>Sure, the functionality to find that SalesTax number might take tens of
thousands of hours to create and cover a multitude of egde cases. Still with
proper context, structure, naming conventions, etc the why’s should be clear.

Again, that's just not true. I have a hard time imaging that you're a
professional engineer with real world experience if you've never found
yourself in a situation where variable names alone could not express the _why_
behind a piece of code.

>Elegant code is elegant when it encapsulates the why’s not just the how’s

Great, not always possible. For example:

    
    
      // We have a longer than normal backoff period on
      // timeouts here because device XYZ is a piece of junk
      // and randonly stops responding for minutes at a time
    

or

    
    
      // version 2 of the spec switched to an XML format and
      // allows the header to be anywhere above the root
      // element of the document (as a processing
      // instruction). We cannot define a reasonable min
      // header position/length. Just read the whole file.
      const size_t MinHeaderLength = std::numeric_limits<size_t>::max();
    

or

    
    
      // Workaround issue caused by .NET 4.6.1 upgrade which
      // has more restrictive certificate checks for secure
      // connections. This is currently affecting SignalR
      // Scaleout connections to Azure Service Bus.
      AppContext.SetSwitch("Switch.System.IdentityModel.DisableMultipleDNSEntriesInSANCertificate", true);
    

Of course you could suss out the reasoning on your own eventually, but why
force people to do that? What variable naming scheme would you use to convey
those reasons?

~~~
Retric
You’re missing my point of course we sometimes need comments. It’s a question
of design tradeoffs. CalculateModifier could be part of a great design or a
terrible one, but the need for comments based on the near meaningless name is
an obvious issue that would need to be balanced by something else. Consider
these projects:

Several years ago I was updating this ancient Object Pascal program. OS X had
just showed up and they finally decided to do a full rewrite, but they wanted
to do this is stages. Anyway, this thing was still using Apple Talk networking
not TCP/IP and I was rewriting the network stack so we could replace each
system individually. Surprisingly, the code was very easy to read, but was
also filled with a long legacy of past issues. Comments on 68000 processor
issues could safely be ignored for example. So yes lots of comments, but most
of them had become useless.

More recently, I was redeveloping a .NET website that had been built by
someone in love with XML. Unfortunately, the mismatch between the way the code
operated and the way the system operated meant that you needed to carefully
read each comment. It had slightly fewer, but far more nessisary comments
which was one sign among many that it was a horrible design.

Which is why I am talking about nessisary comments. Many comments can safely
sit in source control or automated tests. Their context quickly becoming
outdated. However, when a systems design nessitates a great many important
comments that’s a bad sign.

------
Jorge1o1
I disagree with the article. The reasoning is much clearer when you
specifically spell out how you’re handling special cases than trying to
finagle your input and padding with zeros and whatnot.

It’s literally hiding your intentions and reasoning for doing things, which I
think is anathema to good code maintenance.

------
ridiculous_fish
For neighbor-sum, instead of saying "each output value is the sum of its input
neighbors", we can say "each input value contributes to the output values of
its neighbors."

Then it is natural to iterate over the input and not the output:

    
    
        for (int i=1; i < length; i++)
           output[i-1] += input[i];
        for (int i=0; i + 1 < length; i++)
           output[i+1] += input[i];
    

"Invert the problem" is a really powerful general strategy.

~~~
pure-awesome
That's actually the kind of solution I thought the author of the article was
going to suggest, before I read their solution.

On an unrelated note, why do you use i+1<length rather than i<length-1 ?

------
jimmaswell
Certainly subjective. One could in many instances easily find it clearer
making the special cases explicit instead of messing with the data beforehand
in order to force it to fit into a more general algorithm, these instances
included.

------
ridiculous_fish
Hey! Here's a fun trick I learned from Knuth's TAOCP, the one with the tape
drive algorithms.

You want to search an array for an element:

    
    
        int i;
        for (i=0; i < array.length; i++) {
            if (array[i] == value) break;
        }
        return i;
    

This is bad because we have _two_ comparisons every loop iteration. But simply
append our search term:

    
    
        array.push(value);
        int i;
        for (i=0; ; i++) {
            if (array[i] == value) break;
        }
        array.pop();
        return i;
    

and we've cut our comparisons in half!

~~~
pvorb
I'd prefer the first version in almost all situations except for a major
performance bottle neck. The second version is less clear to the reader and
thus likely done wrong.

~~~
ridiculous_fish
Yeah tape-based algorithms are an anachronism, but the idea of eliminating
bounds checking via a terminal value is very powerful.

An example of this in action is LLVM's MemoryBuffer [1], which is input to a
parser. How to write a parser? Perhaps it performs bounds checking at each
production. However LLVM guarantees that the MemoryBuffer is NUL (0)
terminated. A parser can then arrange for NUL to be a terminating character,
and handle it uniformly.

The resulting parsers are faster, and also significantly clearer and easier to
write, because there's no need for bounds checking at all.

[http://llvm.org/doxygen/classllvm_1_1MemoryBuffer.html](http://llvm.org/doxygen/classllvm_1_1MemoryBuffer.html)

~~~
magicalhippo
However it requires that the terminal value does not occur in the input.

For a parser that's probably not a problem (it can transform NULs in the input
to a different token), in other situations it's not so easy.

------
overgard
If you were to translate these examples into a systems programming language
like C, I think you’d find these “simpler” cases are anything but. They’d look
much more complicated. They’re filled with new allocations and memory copying.
They’re essentially new algorithms at that point.

------
bfung
Arrays are not special cases. They have a beginning and an end.

Trying to expand the Array by prepending and appending to it shows lack of
knowledge of memory, performance, and maintainability. If loop is refactored
away from 'padded', all hell breaks loose 3 months down the line.

------
hasahmed
Got an easy problem to solve? Let's make it difficult to reason about and hard
to maintain. But no if statements!

------
SatvikBeri
I don't think this article captures the main advantage – when you have layers
of logic, eliminating special cases lets you have O(n) pieces of code instead
of O(n^2). And even if each portion is less readable, 100 functions beats
10,000 functions any day.

A good example of this is making values non-nullable by default, with Optional
types, and eliminating a huge swath of special case handling.

But the benefit doesn't show up without significant scale, and isn't worth it
for functions that are just used once or twice.

------
JoeSmithson
This is something I've definitely changed my mind on several times in my life.

I studied Maths at uni so I definitely have a bias to the author's position of
transforming the problem into a form where an elegant solution "falls out"
naturally. However, I think this is because I am comfortable (from training)
with transformation steps and can easily see whether they matter or not in
terms of affecting the answer.

The example the author uses seems nice because their entire audience is
experienced/intelligence enough to see why padding zeroes works.

There's a puzzle where you are given an array of ints and told every term
appears twice except one. You have to find this "lonely element". This can be
solved naively by looping through the list and recording how many times you
see each number.

It can also be solved "elegantly" by XORing the whole list - but it is not
immediately obvious why this works. In this case I would definitely say the
elegance is not worth the obfuscation cost.

The personal compromise I've come to is to try and refactor the code so that
specifical cases are extracted and quarantined as much as possible, leaving
the main algorithm hopefully clear and elegant.

For example, in the article's first example, I would not pad the array but
would split out a separate function neighbours(i) which returns [1], [n-1] or
[n-1, n+1].

This approach makes more sense in more complicated examples, (say a 3D grid)
or in particular if padding is impossible, like you have to calculate (sum of
neighbours) + (product of neighbours).

------
i_phish_cats
Special cases are a sign you have a business problem that is worth solving.

~~~
swagasaurus-rex
What if the business is a special case among general solution businesses?

------
dkarl
It's way too strong to say special cases are a code smell. A code smell is
something that means most likely something is off. Special cases are just one
way of achieving a result and often the most readable and maintainable one.
The alternatives can be just as "smelly" in their own right.

I'd say this is a special case (ha!) of the principle that each person and
organization has their own personal tendencies. If you say "X is a code smell"
and the internet disagrees, it probably means you or your organization have a
tendency to choose X when it's not the right solution.

------
gerbilly
I used to have a math teacher in high school who would say: "When a
mathematician doesn't have what he wants, he re-arranges things so as to get
it."

Basically he meant, if the problem isn't presented in a way that makes it
convenient to solve, rearrange the problem fist, then solve it.

A classic example of this from high school math would be:
[https://en.wikipedia.org/wiki/Completing_the_square](https://en.wikipedia.org/wiki/Completing_the_square)

~~~
rahimnathwani
"If I need to add a new function and the design does not suit the change, I
find it’s quicker to refactor first and then add the function."

[https://martinfowler.com/books/refactoring.html](https://martinfowler.com/books/refactoring.html)

------
austincheney
One-offs and edge case requirements are everywhere. The article entertains the
idea of eliminating edge cases. Edge case requirements aren't the problem. The
problem is how you solve for them. This is a form of complexity management and
fortunately there are a few good approaches that solve for most forms of
complexity.

The way I commonly approach edge cases is that they are dealt with by an
additional rule (and possibly accompanying data structure) in otherwise
existing similar common functionality. If current functionality is not
sufficient then write new functionality to solve for and replace other
existing insufficient functionality. This allows for consideration of edge
cases as formal requirements while minimizing expansion of complexity. It is
refactoring and not innovation.

If there is a need for innovation and new functionality then the requirement
is a dedicated feature instead of an edge case. The difference between a
feature and an edge case is the different level of intentional overhead
required for documentation, integration, testing, and maintenance.

------
scarejunba
Funny. Linus Torvalds somewhat feels this way judging by this
[https://m.slashdot.org/story/176163](https://m.slashdot.org/story/176163)

Repeated below:

At the opposite end of the spectrum, I actually wish more people understood
the really core low-level kind of coding. Not big, complex stuff like the
lockless name lookup, but simply good use of pointers-to-pointers etc. For
example, I've seen too many people who delete a singly-linked list entry by
keeping track of the "prev" entry, and then to delete the entry, doing
something like

if (prev) prev->next = entry->next; else list_head = entry->next;

and whenever I see code like that, I just go "This person doesn't understand
pointers". And it's sadly quite common.

People who understand pointers just use a "pointer to the entry pointer", and
initialize that with the address of the list_head. And then as they traverse
the list, they can remove the entry without using any conditionals, by just
doing a "*pp = entry->next".

------
0xBA5ED
Yes, it would be nice if your inputs could take any form at any time to
accommodate your "ideal algorithms". Sadly, inputs have definite form and
changing them may require unnecessary work, such as allocating memory!

------
kurtisc
12am/12pm are ambiguous and should be avoided when possible. Try asking people
what time 12pm means and you'll see that some will struggle and some will get
it wrong, especially if they're from a place that uses a 24 hour clock. But
even a 24 hour clock is ambiguous at 00:00. What date should it be? This can
be solved by using 11:59 and 23:59 instead.

12pm is usually used for noon. Ironically, of the two choices, it makes the
least sense. It makes the PM hours become:

12, 1, 2, 3...

When one would expect:

...9, 10, 11, 12

Unfortunately, people weren't quite as into zero-indexing in ancient history.

~~~
scrollaway
Right, 12 is 0 on AM/PM clocks. Once you get to understand that, it's easy to
reason with (though 24-hour clocks are the far superior siege weapon).

00:00 is not ambiguous, though. There is no such date on 12-hour clocks
(there's no zero! Just 12…), and on 24-hour clocks it's unambiguously the
beginning of the day (24-hour clocks are zero-indexed).

In fact, 00:* dates are consistently unambiguous, because they don't require
you to define whether you're using a 12 or 24 hour clock. I think you're
thinking about 12:00 on 12 hour clocks.

------
im_down_w_otp
I can certainly imagine cases for which this is true, but I more often than
not would expect this to be true mostly at the end of a long tail of
requirements smell and model smell.

------
taurath
I'd say if you're working for a real business making money and don't have
special cases in the code, you should watch out because some competitor is
going to be moving faster than you, and you probably don't bring much value.

In any significantly complex system of business logic, special cases abound. A
system of only special cases is of course bad, but a system with a few here
and there are expected.

------
oneeyedpigeon
I note that some languages effectively do the array-padding by default. In
PHP, accessing an out-of-bounds element results in a Notice (not even a
Warning, let alone an Error) and a NULL return value. Of course, in an integer
context, NULL gets coerced to 0, so the following will work just fine:

    
    
        $input = array(1, 3, 4, 5, 0, 1, 8);
        $i = 0;
        echo $input[$i - 1] + $input[$i + 1];
    
        // outputs 3

------
freetime2
I agree with the principle. Often when you run into a lot of special cases it
can be a sign you have modeled something incorrectly. But I don't really agree
with these examples. In these cases the "naive" approaches all feel perfectly
adequate.

Write it out as straightforwardly as possible. Add some unit tests for the
special cases. And then move on to something more worthy of your time than
trying eliminate a few if statements.

------
roywiggins
I suppose a side benefit of eliminating special cases is that you also
eliminate branches, which may make things faster. If your preprocessing step
is quick and branches are very expensive it might make a real difference.

It reminds me of programming assignments where you're tasked to solve a maze.
Exploring the maze is really annoying if you have to special case the edges;
much easier if you just build an edge-of-the-world wall first.

------
taco_emoji
This article is unconvincing--the author doesn't even _attempt_ to justify the
claims made throughout. I.e.:

> Notice how we treat the endpoints as special cases, complicating the simple
> rule “sum both neighbors of every element.” A better approach is to first
> transform the input so the special cases vanish, leaving only the general
> case.

Why is that a better approach? The author never deigns to share their
reasoning.

------
koliber
Be smart, write DRY code, but leave room for hooks to implement special cases.
Make code readable, maintainable, and extensible.

Write for maintainability and extensibility. I know you can solve the given
problem better, with more succinct syntax and clever shortcuts. Please don't
though. The code you write today is here to stay. Plan for the war, and don't
overoptimize the individual battles.

------
LudwigNagasena
Ugly structures that don't map to clear concepts are worse than any special
case.

Padding array makes you go "wtf is that?"

I would rather see something like this: (i > 0 ? input[i-1] : 0) + (i <
input.size - 1 ? input[i+1] : 0) which clearly maps to "if there is a left
neighbor, add it; if there is a right neighbor, add it; return the sum."

------
nikanj
Where there's muck, there's brass.

Business is smelly, messy, and just filled with exceptions, special cases, and
weird deals with an diabolical structures.

Most startups start with a beautiful, elegant system with zero real users,
then they hit traction, and with added business the business logic parts of
the code base turn into eldritch monstrosities.

------
uberman
Forcing others to alter their inputs because you want to simplify your method
(because code smell??) is nuts.

If you say:

 _Oh by the way, if you want to call "final_step" be sure to remember to
mirror your input array otherwise my method will break._

I would say:

 _Oh by the way, You are fired._

------
csours
Sometimes it feels like all I do is move problems around like boiled broccoli
on a plate.

------
mokkaweli
Life is made of special cases and it isn't an ideal mathematical model that
you can describe with a beautiful one liner. Write solid tests, write
meaningful comments, don't over engineer things and you should be good.

------
lucio
Isn't the "better" code wrong in the first example?

instead of "result = (1...padded.size-1).map do |i|" shouldn't be "result =
(1...padded.size-2).map do |i|"?

~~~
bhrgunatha
There are 2 forms of range in ruby: inclusive 2 dots - 1..5 => 1, 2, 3, 4, 5
and exclusive 3 dots 1...5 => 1, 2, 3, 4

It's always felt deeply counter-intuitive to me because .. feels like it
represents less than ... but that's a separate discussion.

------
PaulHoule
The structure of common sense knowledge is "A unless B unless C unless D..."

Thus the ultimate way to organize a program is to have a clear separation
between the specific and the general.

------
_pmf_
You just change the way they are handled; the knowledge about the special case
is now hidden in the setup code, which might be better in some cases and worse
in other cases.

------
DarkWiiPlayer
I always wondered how on modern day computers software can still run as slow
as it often does. This article has really opened my eyes.

------
nadam
>Before going on, consider how you’d solve this yourself.

    
    
        var sum = 0;
        for(v : vals) {sum += 2*v;}
        sum -= (vals.first + vals.last);
    

simpler and much faster than any of the mentioned methods in the article.

(Proof: when you sum the neighbours of all the elements then you add each
element twice except the first and the last one.)

Even simpler and even faster:

    
    
        var s = sum(vals) * 2 - vals.first - vals.last;

~~~
jmmcd
Even though the wording is a bit ambiguous, the problem is to output an array
of neighbour-sums, not a single sum-of-neighbour-sums.

~~~
nadam
oh, ok, thanks.

------
rlv-dan
Special cases exists because reality is not perfect.

------
AstralStorm
Can we get the months to be evenly 30 days? ;)

------
edflsafoiewq

        result = [0] * len(xs)
        for i in range(0, len(xs) - 1):
            result[i] += xs[i + 1]
            result[i + 1] += xs[i]
        return result

------
bitwize
No, special cases are what happens when nice neat abstractions meet dirty,
messy reality.

Get used to it, kid.

------
skookumchuck
The only hardcoded numbers in code should be 0 or 1. Anything else, like 5, is
a smell.

~~~
quickthrower2
It depends, hard coding 12 as in months in a year is fine. Best to do it
through a named constant though.

