Friday, May 31, 2019

Perl 6's given (switch on steroids)

Sometimes the best features of Perl — and even more so of Perl 6 — are found by asking yourself, “I wonder if this’ll work?” and then finding that yes, yes it does.

Many languages have a type of switch statement that lets us avoid having a long list of if-then-else.  In JavaScript, we can tidy up things from this:
if (partOfSpeech == 'noun') {
  doNounStuff();
} else if (partOfSpeech == 'verb') { 
  doVerbStuff();
} else if (partOfSpeech == 'adjective') { 
  doAdjectiveStuff();
} else if (partOfSpeech == 'adverb') {
  doAdverbStuff;
}
To this:
switch (partOfSpeech) {
  case 'noun':
    doNounStuff();
    break;
  case 'verb':
    doVerbStuff();
    break;
  case 'adjective':
    doAdjectiveStuff();
    break;
  case 'adverb':
    doAdjectiveStuff();
    break;
}
Now, because of the way that switches work in many other languages, we aren't necessarily less wordy, because we have to include break (unless we want to flow through, which I generally find to be the exception and not the rule, but YMMV). In Perl 6, the switch statement doesn’t exist and instead there is a much more powerful given statement that has all the functionality of other language's switches, plus more. I'm going to focus, however, on its use as a switcher. To write the above Javascript code in Perl 6, we get:
given $part-of-speech {
  when 'noun'      { do-noun-stuff      }
  when 'verb'      { do-verb-stuff      }
  when 'adjective' { do-adjective-stuff }
  when 'adverb'    { do-adverb-stuff    }
}
Now let’s imagine a situation where we might want to use a switch statement inside of another switch statement. For this, we’ll consider needing to determine a greeting message in Spanish oriented towards a given audience. The catch is, our audience could be one person or multiple, we may need to treat them informally or formally, and they may be guys, girls, or a mix thereof. All of these factor into the message we need to give them. Let’s see how we could set our greeting message in JavaScript:
switch (audience.number) {
  case 'singular':
    switch (audience.gender) {
      case 'masculine':
        switch (audience.formality) {
          case 'informal':
            message = '¿Cómo estás mi amigo?';
            break;
          case 'formal':
            message = '¿Cómo está el señor?';
            break;
        }
        break;
      case 'feminine':
        switch (audience.formality) {
          case 'informal':
            message = '¿Cómo estás mi amiga?';
            break;
          case 'formal':
            message = '¿Cómo está la señora?';
            break;
        }
        break;
    break;
  case 'plural':
    switch (audience.gender) {
      case 'masculine':
      case 'mixed':
        switch (audience.formality) {
          case 'informal':
            message = '¿Cómo estáis mis amigos?';
            break;
          case 'formal':
            message = '¿Cómo están los señores?';
            break;
        }
        break;
      case 'feminine':
        switch (audience.formality) {
          case 'informal':
            message = '¿Cómo estáis mis amigas?';
            break;
          case 'formal':
            message = '¿Cómo están las señoras?';
            break;
        }
        break;
    break;
YIKES! Now in the case of Spanish, because many of the oppositions are binary, one could argue that an if-then-else might be cleaner or even with some nested ternary operators, but it still doesn’t generate anything remotely as clean as the Perl 6 code:
given $audience.number, $audience.gender, $audience.formality {
  when 'singular', 'masculine', 'informal' { my $message = '¿Cómo estás mi amigo?'    }
  when 'singular', 'masculine',   'formal' { my $message = '¿Cómo está el señor?'     }
  when 'singular', 'feminine',  'informal' { my $message = '¿Cómo estás mi amiga?'    }
  when 'singular', 'feminine',    'formal' { my $message = '¿Cómo estás la señora?'   }
  when 'plural',   'masculine', 'informal' { my $message = '¿Cómo estáis mis amigos?' }
  when 'plural',   'masculine',   'formal' { my $message = '¿Cómo están los señores?' }
  when 'plural',   'feminine',  'informal' { my $message = '¿Cómo estáis mis amigas?' }
  when 'plural',   'feminine',    'formal' { my $message = '¿Cómo están las señores?' }
}
I don’t think there’s much question that the Perl 6 version is much more readable and — importantly — maintainable. You can very quickly see which attributes apply to which message. Even more nicely (one of the powerful things of given) is that the given block can return data, so we can greatly simplify the assignment to:
my $message = do given $audience.number, $audience.gender, $audience.formality {
  when 'singular', 'masculine', 'informal' { '¿Cómo estás mi amigo?'    }
  when 'singular', 'masculine',   'formal' { '¿Cómo está el señor?'     }
  when 'singular', 'feminine',  'informal' { '¿Cómo estás mi amiga?'    }
  when 'singular', 'feminine',    'formal' { '¿Cómo estás la señora?'   }
  when 'plural',   'masculine', 'informal' { '¿Cómo estáis mis amigos?' }
  when 'plural',   'masculine',   'formal' { '¿Cómo están los señores?' }
  when 'plural',   'mixed',     'informal' { '¿Cómo estáis mis amigos?' }
  when 'plural',   'mixed',       'formal' { '¿Cómo están los señores?' }
  when 'plural',   'feminine',  'informal' { '¿Cómo estáis mis amigas?' }
  when 'plural',   'feminine',    'formal' { '¿Cómo están las señores?' }
}
So, what‘s happening here? In Perl 6, given can take one or more arguments and will try to match each of them to the values for each when block. But we can take advantage of some of some other features of Perl 6 to do some cooler stuff. Both the masculine and mixed messages are the same when plural, so we can use a junction to simplify it:
my $message = do given $audience.number, $audience.gender, $audience.formality {
  ...
  when 'plural',   'masculine'|'mixed', 'informal' { '¿Cómo estáis mis amigos?' }
  when 'plural',   'masculine'|'mixed',   'formal' { '¿Cómo están los señores'  }
  ...
}
Now those two messages are given when the gender is either masculine or mixed. This can be particularly powerful if you have a large number of values in an array which might not even be possible to mimic in other languages in a switch:
given $baby-name {
  when @cool-names.any   { say 'That’s a cool name for a kid!'    }
  when @wtf-names.any    { say 'They’re going to hate that name!' }
  default                { say 'Eh, I guess it’s alright.'        }
}
One thing I mentioned before is maintainability. Let's imagine for a second that you had written the message select code. Your boss comes to you and says “Hey, actually, we really don’t like using señores/señoras for the formal plural. How about just using ustedes since it's nice and neutral?”. You might think that we need to use a large junction, but if for any when block we don’t care about a value, we can use a Whatever star:
my $message = do given $audience.number, $audience.gender, $audience.formality {
  when 'singular', 'masculine',         'informal' { '¿Cómo estás mi amigo?'    }
  when 'singular', 'masculine',           'formal' { '¿Cómo está el señor?'     }
  when 'singular', 'feminine',          'informal' { '¿Cómo estás mi amiga?'    }
  when 'singular', 'feminine',            'formal' { '¿Cómo está la señora?'   }
  when 'plural',   'masculine'|'mixed', 'informal' { '¿Cómo estáis mis amigos?' }
  when 'plural',   'feminine',          'informal' { '¿Cómo estáis mis amigas?' }
  when 'plural',    *,                    'formal' { '¿Cómo están ustedes?'     }
}
Nice and simple, and the star really emphasizes that we don’t care what the value is there. The star can actually be even more powerful. What if $audience.number gave us an actual number, rather than the grammatical number? That's no problem at all!
my $message = do given $audience.number, $audience.gender, $audience.formality {
  when     1, 'masculine',         'informal' { '¿Cómo estás mi amigo?'    }
  when     1, 'masculine',           'formal' { '¿Cómo está el señor?'     }
  when     1, 'feminine',          'informal' { '¿Cómo estás mi amiga?'    }
  when     1, 'feminine',            'formal' { '¿Cómo está la señora?'   }
  when * > 1, 'masculine'|'mixed', 'informal' { '¿Cómo estáis mis amigos?' }
  when * > 1, 'feminine',          'informal' { '¿Cómo estáis mis amigas?' }
  when * > 1,  *,                    'formal' { '¿Cómo están ustedes?'     }
}
For the coup de grâce, what if your boss came to you and said, “Now some of our users have complained that they’re getting messages using vosotros, but they don’t use that in Latin American.  Can you give a different message depending on the country?”.  In many other languages, that could mean adding a lot of extra blocks!  Instead, we can just add a new attribute to $audience for their country of origin and in just a few seconds, we have it up and running:
my @vosotros = <Spain EquitorialGuinea WesternSahara>;
my $message = do given $audience.number, $audience.gender, $audience.formality, $audience.country {
  when     1, 'masculine',         'informal', *              { '¿Cómo estás mi amigo?'    }
  when     1, 'masculine',           'formal', *              { '¿Cómo está el señor?'     }
  when     1, 'feminine',          'informal', *              { '¿Cómo estás mi amiga?'    }
  when     1, 'feminine',            'formal', *              { '¿Cómo está la señora?'    }
  when * > 1, 'masculine'|'mixed', 'informal', @vosotros.any  { '¿Cómo estáis mis amigos?' }
  when * > 1, 'feminine',          'informal', @vosotros.any  { '¿Cómo estáis mis amigas?' }
  when * > 1,  *,                          * , *              { '¿Cómo están ustedes?'     }
}
You might think we need to use @vosotros.none to catch the countries that don’t use that (though it’s easy to see when you might want to do something like that), but just like with switch blocks in other languages, the whens are evaluated in order, so if the way the masculine / mixed junction adds a bunch of space bothers you as much as it does me, we could actually simply this even further to:
my @vosotros = <Spain EquitorialGuinea WesternSahara>;
my $message = do given $audience.number, $audience.gender, $audience.formality, $audience.country {
  when     1, 'masculine', 'informal', *              { '¿Cómo estás mi amigo?'    }
  when     1, 'masculine',   'formal', *              { '¿Cómo está el señor?'     }
  when     1, 'feminine',  'informal', *              { '¿Cómo estás mi amiga?'    }
  when     1, 'feminine',    'formal', *              { '¿Cómo está la señora?'    }
  when * > 1, 'feminine',  'informal', @vosotros.any  { '¿Cómo estáis mis amigas?' }
  when * > 1,  *,          'informal', @vosotros.any  { '¿Cómo estáis mis amigos?' }
  when * > 1,  *,                  * , *              { '¿Cómo están ustedes?'     }
}
Imagine the monstrosity of a codeblock that the above code would be in many other languages, which would furthermore obscure the purpose of the code. And yet here the conditions for each message can be very cleanly and clearly spelled out making maintenance a breeze.

So what's the lesson here? If you're thinking about having a lot of embedded given or even if-then-else blocks where the same values are being checked, using multiple argument given statements in Perl 6 can save you a LOT of hassle when refactoring and can also make your code be infinitely more readable.

Edit

I also forgot that the when can be postfixed, meaning we can get rid of our brackets (and some of that unruly space for the @vosotros.any):
my @vosotros = <Spain EquitorialGuinea WesternSahara>;
my $message = do given $audience.number, $audience.gender, $audience.formality, $audience.country {
  '¿Cómo estás mi amigo?'     when     1, 'masculine', 'informal', *;
  '¿Cómo está el señor?'      when     1, 'masculine',   'formal', *;
  '¿Cómo estás mi amiga?'     when     1, 'feminine',  'informal', *;
  '¿Cómo está la señora?'     when     1, 'feminine',    'formal', *;
  '¿Cómo estáis mis amigas?'  when * > 1, 'feminine',  'informal', @vosotros.any;
  '¿Cómo estáis mis amigos?'  when * > 1,  *,          'informal', @vosotros.any;
  '¿Cómo están ustedes?'      when * > 1,  *,                  * , *;
}
This to me is the best because it visually puts the actually messages right next to the variable $message. Clear purpose, and devilishly easy to add new messages with different conditions.

4 comments:

  1. I think I'd write

    given $audience.number, $audience.gender, $audience.formality, $audience.country {

    as

    given (.number, .gender, .formality, .country given $audience) {

    But that's a tiny thing. I love this post. :)

    ReplyDelete
    Replies
    1. I would too! But I wanted to focus on using given as a switcher, rather than as a topicalizer.

      Delete
  2. You can do 'give' with JS switch that way:

    var var1 = "something";
    var var2 = "something_else";
    switch(var1 + "|" + var2) {
    case "something|something_else":
    ...
    break;
    case "something|...":
    break;
    case "...|...":
    break;
    }

    ReplyDelete
    Replies
    1. What you have will certainly allow for testing two+ variables simultaneously, but you can't check that var1 or var2 match conditions — you can only match them to exact values. If one of my conditions is for var1 to be anything from 1-10, and var2 to be 1-3, I'd need 30 case statements, and of course a wildcard would be impossible. So while it does work for the small JS example I've given, there're two important caveats: (1) the alignment of the values which aids readability will be lost, and —more importantly— (2) the examples using arrays and wildcards couldn't be done at all.

      Delete