top | item 18692227

Pampy: Pattern Matching for Python

126 points| fagnerbrack | 7 years ago |github.com

44 comments

order
[+] lou1306|7 years ago|reply
Just a quick FYI, but in vanilla Python 3 [1] you can already do a head/tail split on any iterable:

    >>> hd, *tl = range(5)
    >>> hd
    0
    >>> tl
    [1, 2, 3, 4]
[1] Tested on 3.6.5, don't know about older versions
[+] quietbritishjim|7 years ago|reply
This is due to PEP 448 "Additional Unpacking Generalisations" [1], which was approved with effect from Python 3.5. Other examples:

    x, y, *r, z = range(7)
    print("x: {}, y: {}, r: {}, z: {}".format(x, y, r, z))
    # prints: x: 0, y: 1, r: [2, 3, 4, 5], z: 6
     
    x, (qa, *qr), *r = [1, [2, 3, 4], 5, 6]
    print("x: {}, qa: {}, qr: {}, r: {}".format(x, qa, qr, r))
    # prints: x: 1, qa: 2, qr: [3, 4], r: [5, 6]
The same PEP allows expanding lists and dictionaries in literals:

    r = [*r1, *r2]    # Same as r = r1 + r2 if they are lists
    d = {**d1, **d2}  # Merges d1 and d2 into d
[1] https://www.python.org/dev/peps/pep-0448/
[+] crimsonalucard|7 years ago|reply
One of the very reasons why Haskell and Rust are so safe is because pattern match checking in these languages is exhaustive. If you don't cover every possible type constructor for an enum or pattern the compiler will throw an error.

For example, the Maybe monad used with match must have Nothing and Just handled during pattern matching. Precompile time logic checking.

The below will throw a precompile time error:

  handleMaybeNum :: Maybe int -> int
  handleMaybeNum Just a = a
The below will not:

  handleMaybeNum :: Maybe int -> int
  handleMaybeNum Just a = a
  handleMaybeNUm Nothing = -1
Could the same be said for this library? If so when combined with mypy type checking and functional programming this can transform python into a language with haskell level saftey.
[+] tom_mellior|7 years ago|reply

    $ cat maybe.hs 
    handleMaybeNum :: Maybe int -> int
    handleMaybeNum Just a = a
    $ ghci maybe.hs 
    GHCi, version 7.10.3: http://www.haskell.org/ghc/  :? for help
    [1 of 1] Compiling Main             ( maybe.hs, interpreted )
    
    maybe.hs:2:16:
        Constructor ‘Just’ should have 1 argument, but has been given none
        In the pattern: Just
        In an equation for ‘handleMaybeNum’: handleMaybeNum Just a = a
    Failed, modules loaded: none.
You're right about the "precompile time error", although it's not what you claim...

Let's fix the syntax error:

    $ cat maybe_fixed.hs 
    handleMaybeNum :: Maybe int -> int
    handleMaybeNum (Just a) = a
    $ ghci maybe_fixed.hs 
    GHCi, version 7.10.3: http://www.haskell.org/ghc/  :? for help
    [1 of 1] Compiling Main             ( maybe_fixed.hs, interpreted )
    Ok, modules loaded: Main.
    *Main>
No error now. You do get a warning with -Wall, but that's not quite what you claimed. (Newer versions may be more strict, I don't know.)
[+] csantini|7 years ago|reply
Author here. Pampy and Pampy.js (https://github.com/santinic/pampy.js) are more similar to Lisp-style Pattern Matching. Of course they cannot provide static analysis, but they can improve understandability, and they are useful with nested structures which are typical of how you solve problems in dynamic languages (less types, more nested dicts-and-lists).

An example:

pampy.matchAll(json, {_: {age: Number}}, (key, age) => age)

Finds any object with a Numeric age, that is value of any key in a json object we don't know the structure of.

[+] freddie_mercury|7 years ago|reply
> Could the same be said for this library?

This is clearly answered on the page.

"By default match() is strict. If no pattern matches, it raises a MatchError"

[+] cle|7 years ago|reply
What's the theoretical difference between this kind of pattern matching and polymorphism? Instead of encoding the logic for each branch in a switch, do we get the same kind of exhaustive static guarantees if instead we encode each branch as a polymorphic implementation on each type?

For example, Java's Optional type seems to provide the same static guarantees without pattern matching (assuming it didn't have the unsafe get() method).

[+] marmaduke|7 years ago|reply
It seems match checking requires static typing, so that’ll be a job for a mypy extension.
[+] bow_|7 years ago|reply
Looks neat! Pattern matching is one of those features that I sorely miss when I have to program in Python.

I do feel a bit wary seeing this[1] though, given that `_` can be considered a special character in Python. I suppose you can use `ANY` instead of `_` in the pattern matches, but of course that would not look as clean.

[1] https://github.com/santinic/pampy/blob/4c8e6e0cabada82a5ed79...

[+] olooney|7 years ago|reply
I agree. Lots of Python programmers use "_" as a variable name to indicate (by convention) that they don't intend to use the variable yet are syntactically required to give it a name[1]. Some common examples:

    for _ in range(10):
        ...

    red, _, _ = rgb(color)

    first, *_, last = some_list
This convention conflicts with the use of a global variable named "_" because the first time a programmer uses this convention and rebinds "_" to something arbitrary it will mask the "_" imported from pampy. Of course a programmer is free to give it a more conventional name:

    from pampy import _ as placeholder
But for my money pampy should have given "_" a real name and left it up to the programmer to give it a short alias if so desired:

    from pampy import placeholder as _
This is in line with common conventions around say, numpy, which is conventionally aliased to the shorter "np" on import:

    import numpy as np
[1]: https://stackoverflow.com/questions/5893163/what-is-the-purp...

EDIT: So it turns out that pampy already has a more explicitly named alternative to "_" called "ANY". So anybody who doesn't like the _ syntax can use "from pampy import ANY" and use that instead.

[+] richard_shelton|7 years ago|reply
And here is an another pattern matching implementation for Python [1]. It was made for compilers construction task and may look similiar for those who has an experience with Prolog, Stratego or Refal.

And here is a toy term rewriting system [2].

[1] https://github.com/true-grue/raddsl

[2] https://github.com/true-grue/code-snippets/blob/master/ttrs....

[+] tom_mellior|7 years ago|reply
> And here is an another pattern matching implementation for Python [1].

Oh, I like the prettier syntax:

    calc_rules = alt(
      Int(id),
      rule(BinOp("+", Int(X), Int(Y)), to(lambda v: Int(v.X + v.Y))),
      rule(BinOp("-", Int(X), Int(Y)), to(lambda v: Int(v.X - v.Y))),
      rule(BinOp("*", Int(X), Int(Y)), to(lambda v: Int(v.X * v.Y))),
      rule(BinOp("/", Int(X), Int(Y)), to(lambda v: Int(v.X // v.Y)))
    )
(https://github.com/true-grue/raddsl/blob/master/examples/cal...)

This way of sharing variable names between the pattern and the associated action is pretty neat. It's much nicer than Pampy's use of '_' for all variables.

[+] marmaduke|7 years ago|reply
It’s neat that Python allows writing such things, but it’d be nice to see what the effect on debugging and stack traces are before writing anything with it.
[+] bjoli|7 years ago|reply
Pattern matching is actually not very hard to write. There is a portable pattern matcher for scheme that uses macros to provide high quality code generation.

In guile the module (ice-9 match) generally has zero runtime cost compared to equal hand-written code.

[+] hyperopt|7 years ago|reply
Nice! Extending this further to support associative and commutative operations yields a functionality similar to that provided in Mathematica. This approach was taken by the MatchPy project to hopefully integrate into SymPy so that they can use the Rubi integration ruleset.
[+] kolinko|7 years ago|reply
Oh my god, I was just considering building exactly the same thing for my project. Thanks a lot :)