by Leon Rosenshein

Else, Switch, And Map

Back when I was in 7th grade I entered some kind of scholastic programming competition. No idea what it was called, or what most of the tasks were. I do remember it was 3 hours on a Sunday afternoon, we were using Basic on a TRS-80, and one of the tasks was to come up with a frequency chart of characters in a block of text. The code I submitted looked a lot like

1  DIM A as string
2  DIM I as integer
3  DIM C as string
4  DIM N(26) as integer
6  FOR I = 1 to LEN (A)
7    C = MID(A, I, 1)
8    IF C <> "A" AND C <> "a" THEN GOTO LETTER_B
9    N(0) = N(0) + 1
11   IF C <> "B" AND C <> "b" THEN GOTO LETTER_C
12   N(1) = N(1) + 1
14   IF C <> "C" AND C <> "c" THEN GOTO LETTER_D
15   N(2) = N(2) + 1


83   IF C <> "Z" AND C <> "z" THEN GOTO NEXT_I
84   N(25) = N(25) + 1
85 NEXT_I:
86   NEXT I
87 FOR I = 0 to 25
88   PRINT "There were"; N(I); " "; CHR(65+I); "'s"

Hey, It's ugly, but it worked, or at least it mostly did. Lots of copypasta and lots of copypasta errors. In the comparisons, the indices, and the GOTOs. But that was early in my career. Not long after that I realized I could have just used the ASC() function to get the ascii character and use that (suitably adjusted) as the index. So all those sequential IFs turn into a simple set of assignments. With a more modern language a map of character or string to integer would be even easier. The body of the loop turned into N[ToUpper(C)]++ Much easier to read, much less error-prone to type, and handles things that aren't alphabetic characters to boot. My original attempt didn't crash on numbers or punctuation, but it didn't count them either.

The point of this isn't that my code wasn't very clean 40 years ago (it wasn't), but that while you can take a very procedural approach to coding, a better choice is almost always to let the data and data structures guide you. Cascading if's are rarely a good idea. For smaller numbers of choices consider a switch. That can make things a lot easier to read at least.

For multi-dimensional things, you could do nested switches, methods. One approach I like in such cases is a multi-dimensional map. Let's say you need to calculate something which is a function of color, width, shape, and language. You could have one function with all of the inputs and a bunch of internal logic, some of which might be very different (shape/language differences). You could come up with a class hierarchy that handles it, create the classes, make a factory, and use it. Or, write the functions that are different, use the inputs and a map, and decide which one to call. In pseudo-code something like map[shape][language](func (c color, w linewidth, s shape, l language)), and use

calculator := map[shape][language] {
        {CIRCLE,   ENGLISH,   CalcCircleRomance},
        {TRIANGLE, ENGLISH,   CalcTriangleRomance},
        {CIRCLE,   FRENCH,    CalcCircleRomance},
        {TRIANGLE, FRENCH,    CalcTriangleRomance},
        {CIRCLE,   ITALIAN,   CalcCircleRomance},
        {TRIANGLE, ITALIAN,   CalcTriangleRomance},
        {CIRCLE,   RUSSIAN,   CalcCircleCyrillic},
        {TRIANGLE, RUSSIAN,   CalcTriangleCyrillic},
        {CIRCLE,   BULGARIAN, CalcCircleCyrillic},
        {TRIANGLE, BULGARIAN, CalcTriangleCyrillic},

   func := calculator(shape, language)

   if (func == null) {
     throw Unsupported
   result := func(color, width, shape, language)

With this pattern the decision logic is collected in one place and it's clear what each pair of shape/language is going to do. The list of supported pairs is easy to see, and it's easy to extend. All of which tends to reduce cognitive load. Which, as I've mentioned before, is a good thing.

Obviously you can take this too far, and at some point a polymorphic hierarchy makes sense. But if you don't need that complexity this is a good compromise.