top | item 20067693

Perl 6's given: switch on steroids

109 points| lizmat | 6 years ago |tenesianu.blogspot.com | reply

64 comments

order
[+] rwmj|6 years ago|reply
They've reinvented OCaml / SML pattern matching. I wonder if they also compile it to efficient machine code? Xavier Leroy's famous ZINC paper goes into how you do that, and it's pretty complicated (page 64 of http://caml.inria.fr/pub/papers/xleroy-zinc.pdf)
[+] rurban|6 years ago|reply
Machine code? Lol. They don't even seperate out compile-time branches, like switching on types. If types are known at compile-time, the switch can be collapsed to the rest. Better languages do that and provide proper syntax to match on types or values or both.

Perl6 (and perl5 given/when) is famously over-designed and under-implemented, with both lsh and rhs needed to be evaluated at runtime, and many inconsistent edgecases, eg matching on arrays vs lists. destructuring (matching nested structures) is also confusing.

[+] dan-robertson|6 years ago|reply
Not really.

Let’s first consider how this code would be translated to ocaml. One would expect to use variants instead of strings, so e.g. gender should be:

  type t = | Masculine | Feminine | Mixed
And then one can match on them and match on tuples and so on. But what about countries? In this case you could write out all the cases separately (difficult to maintain), or you could use polymorphic variants to achieve the @foo.any behaviour:

  type vosotros = [ `Spain | ... ]
  type dialect = [ vosotros | ... ]
And then do e.g.

  match dialect with
  | #vosotros -> ...
But this has the disadvantages that polymorphic variants usually bring (difficulties with type inference/exhaustiveness checks). So instead one would probably have some function to decide if a dialect Is vosotros, and then match on the result of that, a slightly annoying to read boolean.

And one would also need to match on the (perhaps) nonsense case of singular mixed-gender. One can get around this with types in one of two annoying ways. Either combine gender and number:

  type gender = Masculine | Feminine
  type genders = Masculine | Mixed | Feminine
  type number = | Singular of gender | Plural of genders
Which is not great because it is now hard to extract the gender if that is all one wants (but it is perhaps not grammatically useful); or one can use GADTs:

  type singular = [ `singular ]
  type plural = [ `plural ]
  type 'number gender =
    | Masculine : 'a gender
    | Feminine : 'a gender
    | Mixed : plural gender
  type 'number number =
    | Singular : singular number
    | Plural : plural number
  type t =
    T : { number : 'num number; gender : 'num gender } -> t
But this is also unpleasant.

And how does one do other smart-match predicates? For example * > 0. This could either be:

  match number, ... with
  | num, ... when num > 1 -> ...
Which separates the important part (num > 1) from the unimportant part (giving it the name when you don’t care), and you still need a catch all because of the exhaustiveness checks. So maybe instead you would have a helper function to change the number into a variant like One | Zero | Many.

This also ignores the case when one might want to write some more traditional style Perl and put regular expressions as the predicates of those when blocks. This isn’t really an ocaml/SML thing to do but that doesn’t mean it should be disregarded as something bad/useless.

[+] raiph|6 years ago|reply
> They've reinvented OCaml / SML pattern matching.

I don't think so. ML pattern matching is statically machine checkable for exhaustiveness. P6 matching is dynamic.

Aiui what they've done is generalized matching.

This includes features traditionally associated with pattern matching. But also any other match/destructure/bind operation.

A field of a match might build a parse tree or AST.

(Perl 5 did a simple small scale flat assignment approach to this; a statement could assign variables to selected elements of an array of captures extracted from a string by a regex. Perl 6 scales this up to turing complete parsing and AST generation involving huge numbers of nodes.)

One can match an arbitrary predicate. That's what the `* > 1` is. It's not a pattern.

Any value or variable can be matched against any value or variable. (Not just literals.)

Matching is "smart". For example:

  say DateTime.new('2020-01-01T01:01:01Z') ~~ Date.new('2020-01-01')
displays True.

And it can all be destructured (with a fairly sophisticated range of options) and bound as part of the match.

[+] adultSwim|6 years ago|reply
Sigh. ML is such a sweet spot in language design.

OCaml was so far ahead of Java and C# 20 years ago. It's a shame us regular industrial programmers ended up with Java (which I like fine) and PHP, later Python and JS.

[+] combatentropy|6 years ago|reply
This is nice.

In other languages, I would settle for a switch-statement like Go's, which by default breaks instead of falls through. The common switch-statement seems like a prank: If you omit the break, the compiler throws no error, like when you forget a closing brace. Instead the program silently does the opposite of what you meant.

I am less afraid of forgetting one, more of one day accidentally deleting one. Every once in a while in Vim I order my keystrokes wrong and a random line disappears. Usually it catches my eye, but it is unnerving. The risk is eternal.

You would want to leave the switch-statement as is, for backward compatibility, and come up with a new key word to set off the improved version, like Perl did with "given."

With all of the ongoing improvements to JavaScript, I'm surprised this has not been addressed. And in general with most languages I am surprised that their flow control is so stunted.

[+] codr7|6 years ago|reply
Falling through sort of makes sense in C, which is underdefined by design to allow pulling whatever dirty tricks needed to get where you're going.

Copying the idea to higher level languages makes little sense to me. Blindly copying any feature into a language design is a bad idea from my experience.

[+] pantalaimon|6 years ago|reply
Nowadays GCC warns about implicit fall-through.

If you want to have a fall-through, you have to annotate it with, e.g /* fall-through */

This is of course not ideal, suddenly comments have to be considered by the compiler. But I guess it's the only way to fix this in a backwards compatible way.

[+] cracauer|6 years ago|reply
Of course if you'd use Lisp in the first place you could make a new syntax to input those things in the visually safest manner without waiting for a language revision.

Compile-time computing is precisely about this, e.g. https://medium.com/@MartinCracauer/a-gentle-introduction-to-...

[+] dan-robertson|6 years ago|reply
Perl 6 includes the ability to modify the grammar and parser on the fly in a way which is much more convenient than reader macros in Common Lisp (and different to macros in CL). Saying “new syntax” is also slightly disingenuous because the only convenient new syntax available is made of parens.

That said, this isn’t really an article about syntax but how several language features fit together.

Making something slightly similar in CL could be something like:

  (defmacro with (it &body body)
    `(let ((it ,it)) (block nil ,@body)))
  (defmacro having (condition &body body)
    `(if (smart-match ,condition it)
         (return-from nil (progn ,@body))
         (values)))
  (defgeneric smart-match (matcher thing))
  ...
But this makes the behaviour of `having` a bit weird.

I feel like this article is much more about how certain perl6 features compose nicely together (in particular smart matching, $_, expressions returning values) in a way that the existing features of given and when compose to give a useful switch mechanism for free.

[+] sametmax|6 years ago|reply
And of course, your code base will end up riddled with those untested, undocumented customizations. People getting into it will hate you for it, and you will look at them with a smug face, stating how they don't get the power of a true language.

There is a reason Lisp based languages are not more popular: most people don't want to learn a new language every time they work on a new project.

[+] riffraff|6 years ago|reply
perl6 also has syntax macros (sort of, I think rakudo does not implement the full perl6 macro stuff).
[+] xisukar|6 years ago|reply
Do you mind explaining how is it related to the article? I'm genuinely interested.
[+] otabdeveloper1|6 years ago|reply
> you could make a new syntax

Thanks, but I'd rather not.

[+] xisukar|6 years ago|reply
The `when` block seems to exhibit a different behavior when used as a block vs as a statement modifier. The latter lets things falls through until it matches the most general case. For instance, given

    class Audience {
        has $.number;
        has $.gender;
        has $.formality;
        has $.country;
    }
    
    my @vosotros = <Spain EquitorialGuinea WesternSahara>;
    
    my $audience = Audience.new:
     :number(100), :gender('feminine'),
     :formality('informal'), :country('Spain')
    ;
Contrast:

    my $message = do given (.number, .gender, .formality, .country given $audience) {
      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?'     }
    }

    say $message; #=> «¿Cómo estáis mis amigas?␤»
with this:

    my $message = do given (.number, .gender, .formality, .country given $audience) {
      '¿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,  *,                  * , *;
    }
    
    say $message; #=> «¿Cómo están ustedes?␤»
[+] raiph|6 years ago|reply
Yes, that's deliberate.

If you use an ordinary `when` (with its statement(s) on the right) then P6 assumes it should succeed with the value of the last statement of the matched `when`. To succeed means to leave the block and return that last value if the block's value is being kept (eg via a `do`). If you wish instead to proceed to the next statement rather than succeed, you must write `proceed`.

Conversely, if you use a modifier `when` (with its statement(s) on the left) then P6 assumes it should proceed as explained above after matching. If you wish instead to succeed as explained above you must write `succeed` (with a value unless you wish to succeed with a value of `Nil`).

This is great bit of language design work. Thank you Larry.

[+] nxrabl|6 years ago|reply
Fortunately, the second example does throw warnings for each line of the block but the last:

    Useless use of constant string "¿Cómo estás mi amigo?" in sink context
[+] harryf|6 years ago|reply
Every time I see something about Perl 6 I think this is awesome. Shame that a younger generation of programmers will never use it, because it’s not the new cool.

If they renamed Perl 6 into something cool like “Liquid” or some think you find in the kitchen, like Chilli. Then hide away it’s origins a bit, they’d have a language kids would be raving about.

I know that’s not the point but I can’t think of any programmer I know today that doesn’t talk about Perl with a certain contempt. “It’s Perrrrl” eyes up

[+] Sharlin|6 years ago|reply
There's a concept in programming language design called "weirdness budget". There's only a finite amount of idiosyncratic syntax and semantics your language is allowed to have before it starts to turn off potential adapters because it looks too unfamiliar, too unintuitive, or too opinionated compared to what they already know.

Perl has never cared what the mainstream programmer community thinks. It is willfully and deliberately weird; when it adopts or reinvents concepts from other languages it does it in a way that seems purposively quirky; it invents bespoke vocabulary for things that already have well-established names. The Perl community appears cultish, lacking a certain self-awareness and cognizance of the programming world at large. It's like an alternate timeline where everything seems eerily off in some way.

Perl blew its weirdness budget a long time ago and never cared. And given its well-deserved reputation as a write-only language, it's no wonder at all that people don't take it entirely seriously.

[+] noir_lord|6 years ago|reply
If you want to make Perl 6 'cool' just create an alternate syntax that maps to Perl 6.

cough Elixir cough

[+] Retra|6 years ago|reply
If you think merely rebranding something will make it more acceptable, then perhaps you've found the real reason people don't like Perl: it is a language that appears to be built around the superficial. Like someone who takes 30 mins crafting their bed hair in an effort to prove they don't care about appearances.
[+] jacoblambda|6 years ago|reply
I can say as a part of the younger generation of programmers that I don't have any problem with Perl and that it has its uses but I don't think Perl should necessarily be used as much as it was in the past.
[+] CDSlice|6 years ago|reply
This looks a lot like Rust's match blocks (which were inspired by pattern matching in ML languages). It's nice to see pattern matching constructs come to more languages.
[+] b2gills|6 years ago|reply
Rust's match blocks appear to be less powerful than the smart-match feature in Perl6.

Rust doesn't appear to allow arbitrary code in the condition.

---

    given 1 {
      when (0 ≤ * ≤ 9) { 'digit' }
    }
Expanding out the definition of the `when` above:

    given 1 {
      {
        my $initial-result = (0 ≤ * ≤ 9); # get the result of the expression
        my $result = $initial-result.ACCEPTS($_); # ask it if it accepts the topic
        if $result {
          succeed 'digit'
        }
      }
    }
The condition part a `when` is just like the right side of the `smart-match` feature, and the `where` clause.

It does double code execution.

First as just a statement. (In this case the result is a lambda that will compare against `0` and `9`.)

Then it asks the result of the statement if it `ACCEPTS` the current topic value. (In this case it runs the lambda with topic as its argument.)

If the second level result is truish then `when` runs its block and calls `succeed` with its last value.

`succeed` throws a `CX::Succeed` control exception which the `given` catches.

---

This double code execution allows for a lot of flexibility. Also since smart-match asks the initial result if it `ACCEPTS` the topic, it gets to decide how it matches.

    class Match-Always {
      method ACCEPTS ( $ --> True ){}
    }
    class Match-Never {
      method ACCEPTS ( $ --> False ){}
    }

    # match 50% of the time arbitrarily
    class Match-Half {
      method ACCEPTS ( $ --> Bool ){ Bool.pick }
    }

    given 1 {
      when Match-Half { … }

      when ( $_ == 1 ?? Match-Always !! Match-Never ) { … }
    }
(Note that `True` works like `Match-Always` and `False` like `Match-Never`, so there is not a good reason to add those two types like I did.)
[+] makecheck|6 years ago|reply
I find it odd that no language seems to have a syntax short-cut (like operators) for something as common as switches; the closest thing might be a shell script!

This is a very common thing to write. Why couldn’t a switch avoid all keywords and look more like a list...such as:

    ?(value):
    - 0: { doThing(); }
    - 1: { otherThing(); }
    - *: { doDefault(); }
[+] rbonvall|6 years ago|reply
Haskell's syntax is pretty minimal:

  f 0 = doThing
  f 1 = otherThing
  f _ = doDefault
[+] ohithereyou|6 years ago|reply
given/when is also available in Perl 5 from 5.10.1 onward[1]. There are times when it is very helpful.

[1] https://perldoc.perl.org/perlsyn.html#Switch-Statements

[+] Grinnz|6 years ago|reply
Don't use it in Perl 5. You have to first read the whole documentation on what when does[1] and then the whole documentation on what smartmatch does[2] and then you probably still won't know what will happen if you compare two values that might or might not be numbers, because Perl does not make this distinction for you, so smartmatch guesses. There is a reason every other operator has explicit stringy and numeric comparison versions, and optional new stringy bitwise operators were added because they had the same ambiguity[3].

[1] https://perldoc.pl/perlsyn#Experimental-Details-on-given-and...

[2] https://perldoc.pl/perlop#Smartmatch-Operator

[3] https://perldoc.pl/feature#The-'bitwise'-feature

[+] skywhopper|6 years ago|reply
I feel like you can approach most of this with Ruby’s `case`. Not sure about the `when @ary.any` but it wouldn’t be hard to monkeypatch a fix (not that it would be a good idea).
[+] codr7|6 years ago|reply
I'm usually looking for two kinds of switching, most implementations support one of them. Switching on a list of conditions in priority order like CL's COND, and switching on a value.

Simply making the value optional and injecting it in specified conditions if present would allow using the same construct for both.

Note that this may well be possible in P6, most things are; but I have yet to come across and example of COND-behavior, they're all switching on values from what I can see.

[+] aasasd|6 years ago|reply
Behold:

    switch (true) {
        case a > b: ...
        case isFullMoon(): ...
    }