1. 31
  1.  

  2. 24

    Maybe they’re a code smell, but you’d better get used to them because getting rid of them is often worse than the cure.

    The examples presented are toys. The author is able to eliminate them because the domain they are working in is well defined and there are enough examples in order to work out a model for it. It also seems that the models are resiliant to new information or cases. That’s a luxury.

    I doubt this happens a lot, especially when dealing with business logic. It certainly hasn’t happened much in my work. It’s tempting to see a special case then then try to get rid of it by abstracting things in some manner (I like the phrase “extended domain” used in another comment) but that abstraction tends to do at least one of two things: it makes expressing that special case (or even most cases) less concise or incur a noticable performance penalty. It may be worth it. It might not. It’s hard to know before you do it sometimes.

    And then there’s the problem of thinking that the abstraction can adequately describe all the cases that you think could happen, only to find out something much different comes along. You might end up bending that abstraction anyway, making things uglier than just accepting that you’ve got some special cases to deal with.

    1. 2

      Hey Geoff,

      I think all these caveats are fair and I should have made them clearer in the article (see my response to Michael below). I especially agree that the following is a risk of using this technique unwisely:

      You might end up bending that abstraction anyway, making things uglier than just accepting that you’ve got some special cases to deal with.

      When new information arises, you need to be open to re-evaluating or scrapping your extended domain.

      The only thing I’d challenge is this:

      I doubt this happens a lot, especially when dealing with business logic.

      In my experience it happens a decent amount, especially when you view “removing special cases” on a spectrum. You might not get your pristine palace of an abstraction, but you might find one that improves your code, or part of your code, and doesn’t have any downsides. Or you might not. It’s just a code smell, something to be alert to. It doesn’t necessarily mean things are rotten.

    2. 10

      I used to believe this but I don’t anymore. All functional code by necessity must have special cases otherwise it doesn’t encode any real world domain semantics. An example I’m familiar with is cloud infrastructure and auto scaling.

      Auto scaling by necessity is full of special cases related to which piece of the infrastructure is being scaled and in which direction (up or down). When bringing up new machines their state must be reconciled with the rest of the existing infrastructure in domain specific ways and when removing machines they must be gracefully disconnected from everything else. For some components you can just yank the plug and call it done. For other pieces you will need shutdown and clean up procedures if you want your data to remain consistent and this procedure will depend on the component. Moving to serverless/containers doesn’t solve the issue either. Now the domain logic is encoded in some other way using the serverless/container primitives instead of whatever was being used previously.

      You could try to abstract everything as far as possible but now you’re paying an abstraction cost and this cost translates to real money and performance trade-offs. You could just say everything works with queues and we don’t care how things are coming and going to handle the work in the queue but this has a real cost. You have added extra hops to everything and along with servers are paying for the queues.

      1. 6

        The thing I’ve arriving at is the concept of the ‘domain’ and the ‘extended domain.’ The latter is generalized to get rid of edge cases in the former. There is an abstraction cost to doing this, but I don’t think there is a single answer. Sometimes it’s worth it, and sometimes it’s not. The amount of abstraction leakage in the extended domain is key.

        As an example: in many languages we can use negative indices to index from the right of an array. The leak is the awareness that we should have that an index could overflow and produce an inadvertently valid, but wrong reference.

        1. 3

          Right, and it’s not just leaking abstractions becase you are actually paying a performance cost as well for the more uniform abstractions. Your domain might have restrictions that can be used to optimize things but those restrictions are not expressible in the extended domain. For example, if you knew that in some function the only inputs were ever going to be in the range 0 to 10 then this could enable optimizations that wouldn’t be possible if you assumed arbitrary input.

          In my mind that’s the trade-off we are making with special cases. We make things more performant at the cost of having more edge cases and more rigid implementations that are less composable. Ideally we’d have tools that would allow expressing the logic without edge cases and then just-in-time optimizing it for the actual real world runtime use cases. If I could specify with first-order logic and then compile that specification down to real world implementations then that would be ideal but the current gap in tools and general software engineer education is too wide to make that a reality.

          1. 3

            I agree. But I’ll just add that domain extensions don’t always incur significant performance cost and, when they do, it’s acceptable sometimes.

          2. 3

            Michael,

            I’m a big fan of your work and was happily surprised to find this posted here and see your response.

            There is an abstraction cost to doing this, but I don’t think there is a single answer. Sometimes it’s worth it, and sometimes it’s not

            This gets the heart of the matter, and I only hint at this in the last paragraph when I say “It’s not always possible to simplify a problem by recasting it. But often it is, if only partially.” Not delving into it more deeply may have been a mistake on my part, but was at least well intentioned: I thought it would be better to illustrate my points unambigiously with over-simplified examples than grapple with all the nuances and gray areas and risk losing the plot.

            That said, I’ll be the first to admit that applying this technique over-zealously risks a cure that’s worse than the disease, in the form over-engineering or premature abstractions. And while over-engineering is never in short supply, in the wild I see, just as often if not more often, special case code that could be removed with no penalty – indeed, where the “extended domain” solution is conceptually simpler, the code shorter, and the performance unaffected. Sometimes we just don’t see the better solution.

            Related: As you mentioned, we usually view layers of abstraction as a cost – which may or may not be worth it, depending on the benefit. But it’s possible – you see this in mathematics – that “going up a level” in abstraction actually makes everything simpler. I’d argue the invention of negative numbers falls in this camp.

            And then there’s yet another category, where there might be a significant learning cost to some abstraction, but once you’ve acquired it – once it becomes muscle memory, so to speak – it makes many problems much easier to solve. You pay to expand your set of primitives to higher-level ones, and then reap the benefits. Here I’m thinking of learning a library of functional utility methods, or becoming comfortable with a new programming paradigm. But then you have to weigh the benefit of your simpler code (but simpler only within this larger knowledge context) against the barrier to entry it might create to future maintainers, and that intersects with the culture of the company where you’re working, and what kinds of assumptions it’s safe to make.

            I considered exploring all of this, but opted for the more straightforward discussion in the article.

            I’d love to hear more about your upcoming book.

            Jonah

          3. 1

            For some components you can just yank the plug and call it done. For other pieces you will need shutdown and clean up procedures if you want your data to remain consistent and this procedure will depend on the component.

            Maybe a bad example, Just being able to yank the power is not a special case, it is a noop cleanup function.

            1. 1

              That’s what I mean. That’s the default and it often won’t work.

          4. 6

            I’m currently working on a book about this subject. The working title is ‘Unconditional Code.’

            I’ve also made an instagram account containing random pithy statements related to the topic: https://www.instagram.com/unconditional.code/

            1. 4

              The funny thing about this discussion is that it completely ignores polymorphism as the much more common way to achieve code that is generic and avoids embedded special casing for many special cases.

              In a business domain where fairly arbitrary rules will need to be developed, a classic object oriented sum type with a fixed interface works well (and probably accounts in part for the success of OOP).

              The use of lawful compositional types (eg monads) can also be very helpful where the need is to abstract away parts of the program structure. The disadvantage at times is that it makes it easy to embed the special casing logic at the site of processing. This may or may not be a good thing for readability and navigability.

              1. 0

                Don’t you know “good” and “OOP” can’t be used in the same sentence on lobsters? FP is the One True Way.

              2. 3

                Interesting article, but even with author’s examples simpler code is not always more easily understandable. So I am not sure it is necessary better. My experience also seems to be different since special cases I deal with often if not usually come from matching user’s model instead of mathematical one. People somehow tend not to have consistent behaviour or expectations.

                Somewhat off-topic, but based on proliferation of “code smell” articles one might assume that programming is one putrid mess.

                1. 2

                  It’s the new “technical debt”. A neat sounding, subjective assessment of a complex system often based on gut feeling rather than any objective metric.

                2. 2

                  tl;dr:

                  When you can’t make the special case look general, see if you can make the general case look special.