
Pampy: Pattern Matching for Python - fagnerbrack
https://github.com/santinic/pampy/blob/master/README.md
======
lou1306
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
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/](https://www.python.org/dev/peps/pep-0448/)

~~~
quietbritishjim
I was mistaken, sorry. Those unpackings work as of Python 3.0 due to PEP 3132
"Extended Iterable Unpacking" [2]. PEP 448 is just for those examples in the
second half of my comment.

[2]
[https://www.python.org/dev/peps/pep-3132/](https://www.python.org/dev/peps/pep-3132/)

------
crimsonalucard
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.

~~~
cle
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).

~~~
thesz
You are right assuming the more or less equality between two approaches.

For example, you can encode Just as fJust :: a -> (a -> b) -> b and Nothing as
fNothing :: b -> b. fJust receives a computation that accepts argument of Just
constructor, while fNothing receives just a value to be returned. Or,
alternatively, you can look at the type of maybe function which is pattern
matching on Maybe in disguise: maybe :: b -> (a -> b) -> Maybe a -> b.

(I believe this method is called Church encoding)

But when you go from structure of types and patterns to functions, you lose
the ability of analysis (of exhaustivity or anything else). You now deal with
something that is Turing complete instead of fixed size structure.

And this is delimition of "possible" and "impossible". With the Church
encoding analysis of matching completeness is impossible and even writing
matchers for lists or trees is nigh to impossible, actually.

~~~
solomon_
I believe this is actually Scott Encoding.

------
bow_
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...](https://github.com/santinic/pampy/blob/4c8e6e0cabada82a5ed794f935e9a6d249d2833a/pampy/pampy.py#L9)

~~~
olooney
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...](https://stackoverflow.com/questions/5893163/what-is-the-purpose-of-
the-single-underscore-variable-in-python)

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
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](https://github.com/true-grue/raddsl)

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

~~~
tom_mellior
> 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...](https://github.com/true-
grue/raddsl/blob/master/examples/calc/calc.py))

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.

------
methyl
There is also a JavaScript port of this library, by the same author:
[https://github.com/santinic/pampy.js](https://github.com/santinic/pampy.js)

------
marmaduke
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
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.

~~~
marmaduke
This may be true but misses my point: when I navigate a stack trace and try to
figure out how execution went through this match process, it will be more
difficult to understand than idiomatic Python code, because the tooling is
line-oriented.

Good FP debuggers would be able to step through it of course so it can be
restated as a “toolchain issue” but that ignores the fact that Python is
largely statement oriented, in terms of its execution model.

~~~
bjoli
Sorry, I should have been clearer. The macro way of doing it doesn't mess with
stack traces as it expands to pretty clean scheme code, at least after the
optimizer has had it's way.

I am sad that most languages don't have macros since it is a very powerful
tool. Not only does it let you abstract the ugliness away from approaches like
this python one (which is actually pretty decent), but it also let's you
produce good machine code from a "bolted on" construct. The python matcher is
bound to have some overhead, whereas a syntactically expanded one is not.

~~~
marmaduke
Ah I see what you mean (I wrote macros in Clojure years ago). Yep that would
do it, but would require an AST-rewriting decorator in Python.

~~~
uranusjr
That’s an interesting thought! I guess I just found a good project to play on
during the holidays. I wonder if something like this would work:

    
    
        with matching(expr) as m:
    
            @m(pattern)
            def do_for_pattern(v):
                ...
    
            @m(_)
            def catchall(v):
                ...

~~~
b3orn
Something like this?

    
    
      class matching:
          def __init__(self, expr):
              self.expr = expr
              self.patterns = []
              self.result = None
      
          def __call__(self, pattern):
              def _inner(func):
                  self.patterns.append((pattern, func))
                  return func
              return _inner
      
          def __enter__(self):
              return self
      
          def __exit__(self, *args):
              for pattern, func in self.patterns:
                  if pattern.match(self.expr):
                      self.result = func(self.expr)

------
hyperopt
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
Oh my god, I was just considering building exactly the same thing for my
project. Thanks a lot :)

