aardappel / lobster

The Lobster Programming Language

Home Page:http://strlen.com/lobster

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Add Pattern Matching

MavenRain opened this issue · comments

Self-explanatory . . . from the looks of it, this feature seems like the only one keeping Lobster from being the best programming language in the world!

Well, thanks!

We could make it part of the switch syntax (similar to Rust/ML match):

switch obj:
    case Monster { hp, _ }: return hp
    case Wall {}: return 10
    case Pickup { amount }: return amount

Or it could be made part of function overloading (similar to Haskell etc):

def health(Monster { hp, _ }): return hp
def health(Wall {}): return 10
def health(Pickup { amount }): return amount

The latter seems somewhat pointless though, since you can already write:

def health(m::Monster): return hp
def health(w::Wall): return 10
def health(p::Pickup): return amount

i.e. Lobster already has good support for writing methods together like this (and directly accesssing members), rather than as part of the class definition.

An advantage of pattern matching would be more complex patterns, like:

def health(Monster { hp, Armor { a }}): return hp + a * 10
def health(Monster { hp, SwimSuit { _ }}): return hp - 1

But even that is not that much longer in the current syntax, or can use a helper function that computes armor.

While I generally like pattern matching (I used to absolutely love it when I programmed in Haskell, and have implemented it for several of my past languages, e.g. Bla and Aardappel), I think the "cases are next to eachother" is a much bigger benefit than "you can select on multiple objects at once". The latter is useful very infrequently (optimizers for programming languages are a great example), but not so much in games, Lobster's #1 target field.

Can anyone find a compelling example for games that would be a great improvement on what is currently possible?

If anything, extending the switch with objects would be my choice, since it add the most new functionality to what we already have. Though rather than full on structural pattern matching, I think just object types would maybe also be sufficient:

switch obj:
    case Monster: return hp +
        switch armor:
            case Armor: a
            case SwimSuit: -1
    case Wall: return 10
    case Pickup: return amount

i.e. this would add an implicit variable to each case body where members can be accessed just like a method. But again, that ends up just like alternative syntax for the method code above.

Opinions?

commented

But even that is not that much longer in the current syntax

My main draw for pattern matching is readability; ime pattern matched code isn't often that much shorter than alternative anyway

Can anyone find a compelling example for games that would be a great improvement on what is currently possible?

Depends on what flexibility you're willing to go for. Would this include predicates (ie instead of variable, put in function reference, and pattern matches if for that slot the function returns true) and equality checks on reoccurring variables (ie if I use a for a slot in one place, and a for a slot in another, that it'd only match if both a are equal)?

@arvyy yes, the possible list of pattern matching features is long. One of my past languages did implement the "a second occurrence of the variable means the value must be equal to the first", and that is certainly neat.. question is how useful is it, and how hard or easy to read does it make the resulting code.

Some possible things you can match in a pattern:

  • sub objects
  • constants
  • wildcards
  • remainders (of a vector, or arbitrary unmatched fields)
  • ranges
  • predicate functions
  • previously bound variables
  • inline conditional expressions

That becomes a whole programming language by itself :)

That all would translate underneath to a "web of if-then-elses and variable definitions"

Pattern matching is great when working with data structures. Compare Haskell and Lobster versions of a btree double rotation, for example. And even this is only possible because I defined a BOA constructor newNode that takes arguments without labels. It's so much clearer what doubleL is doing in the pattern match code.

That said, I understand that data structure implementation is not the main focus of Lobster.

@dcurrie actually, I find those both hard to read.. the pattern makes the incoming data structure easier to read, but relating that to the output data structure being constructed is almost harder in the Haskell version (since you need to relate the individual variables, rather than having a . reference).

That said, I understand that data structure implementation is not the main focus of Lobster.

It is something that ideally every language should be good at, and I certainly would Lobster is, or will be. Games is my example use case, but I certainly want it to be a powerful and fast general purpose language by extension.

Reading the above discussion again, I'd actually be most interested in this form:

switch obj:
    case Monster:
        return hp +
            switch armor:
                case Armor: a
                case SwimSuit: -1
    case Wall:
        return 10
    case Pickup:
        return amount

Which would replace:

if obj is Monster:
    return obj.hp +
        if obj.armor is Armor:
            return obj.armor.a
        elif obj.armor is SwimSuit:
            return -1
elif obj is Wall:
    return 10
elif obj is Pickup:
    return obj.amount

Not only is it a big improvement on cluttered syntax, it is simple (the switch selects object type, and brings the type into scope like :: would), it could also actually be a speed improvement when testing many types, though that would require sticking a sub-class index in the type structure since that is common case.

For now, we have exhaustive switch on types, which goes pretty far towards this functionality, is simple and fast!

Example:

abstract class A:
a = 0
class B : A
b = 1
abstract class C : A
c = 2
class D : C
d = 3
class E : C
e = 4
class F : E
f = 5
// 6 classes, but 2 are abstract, so this switch will demand that exactly 4 cases
// must always be covered!
/*
(A)
|\
B (C)
|\
D E
|
F
*/
let tests = [ B {}, D {}, E {}, F {} ]
// Exactly 1 case per class.
let results1 = map(tests) t:
switch t:
case B: t.b
case D: t.d
case E: t.e
case F: t.f
// No default needed / allowed!
assert equal(results1, [ 1, 3, 4, 5 ])
// Subclasses can be done by superclasses.
let results2 = map(tests) t:
switch t:
case B: t.b
case D: t.d
case E: t.e
// No default needed / allowed!
assert equal(results2, [ 1, 3, 4, 4 ])
// Abstract base class may implement for all subclasses.
let results3 = map(tests) t:
switch t:
case B: t.b
case C: t.c
// No default needed / allowed!
assert equal(results3, [ 1, 2, 2, 2 ])
// Probably bad practice, but defaults are still allowed.
let results4 = map(tests) t:
switch t:
default: t.a // Can't access anything else!
case F: t.f
assert equal(results4, [ 0, 0, 0, 5 ])

Docs:
You can also "dynamic dispatch" with `switch` ! You can use class names as
switch cases:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
switch x:
case A: print x.field_in_a
case B: print x.field_in_b
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As you can see, the type of variable switched on will be "upgraded" to the type
matched, so you can access its fields.
switches on types are "exhaustive", meaning if you don't use a `default` case
(and its a good habit to not use those) you will get a compile-time error if
a type is not covered by a switch (all possible subclasses of the type of the
switched on value).
Superclass cases can apply to subclass cases, and if both are present, the
most specific case will always be used. It is a good idea to make superclasses
`abstract` for use with `switch`, that way you may omit a case for them,
causing all their subclasses to need their own case.
The actual implementation use vtables much like the above dynamic dispatch,
so is similar in speed too.