Pampy: Pattern Matching for Python
github.com
github.com
>>> hd, *tl = range(5)
>>> hd
0
>>> tl
[1, 2, 3, 4]
[1] Tested on 3.6.5, don't know about older versions 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/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.This is clearly answered on the page.
"By default match() is strict. If no pattern matches, it raises a MatchError"
This allows for early catching of logic errors during unit testing and negates the need for additional tests.
It's not misleading. It answers the question: there is no compile-time check. The OP didn't have to "wonder" they could have just read the link.
Please note that my psuedo code in haskell was not function calls. It's a function definition using pattern matching in the function signature.
Think of Maybe as an enum with two values. A Just(a) and a Nothing.
The below definition basically says if the enum enters a function as a Just(int a) return int a. (Just(str) is a type error handled by the signature). If the enum enters the function as a Nothing return -1.
handleMaybeNum :: Maybe int -> int
handleMaybeNum Just a = a
handleMaybeNUm Nothing = -1
if the last pattern match (Nothing) isn't written, the compiler knows that your logic isn't handling a specific case, and it lets you know about this BEFORE doing any matching. The haskell compiler will literally tell you that your function is missing logic to handle certain cases.It is a simple mechanism that actually proves your programs to be correct. Type checking proves your program to be type safe. Exclusive use of pattern matching in haskell proves that your program logic handled every possible permutation of that type entering a function.
It is a powerful mechanism that is far greater than unit testing. Unit testing just says your is program correct for a specific test case. Haskell pattern matching and type checking provides a proof across every parameter permutation.
I realize python is an interpreted language. So an error can only be thrown at runtime. What I expect for an exhaustive match check in python is this:
match(Just(1),
Just(a), lambda a: a)
match(Just(1),
Just(a), lambda a: a
Nothing, 1)
Both matches above have produce a successful match with Just(1) matching Just(a) in the first expression. However for haskell pattern matching the first match will STILL throw an error because the Nothing case for the Maybe enum was not handled.Apparently from the examples in the docs it clearly does not do this. You can literally match across types and it is unreasonable for the match operator to throw an error if you didn't cover every possible type that exists.
Look up type annotations for python 3 and the mypy external type checker.
Static type checking is now a very real thing with modern versions of python.
Additionally it is possible to code the match function to implement the functionality I am talking about during runtime instead of compile time. Definitely not irrelevant.
Maybe, but I'm not sure there's more value in that.
I didn't say it was the same. I said that if he bothered to read the link he would have seen the answer to his question.
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).
Haskell's implementation of "match" is so elegant that it will take you a while to realize that the "exhaustive" pattern matching is a big part of what is contributing to haskells safety.
If that's the case, then the conversation shifts from "you should use pattern matching" to "when should I use pattern matching?" Which is a pretty interesting question. Logic locality seems to be an obvious criteria (does the logic live with the type, or with the consumer of the type?).
Pattern matching is syntactic sugar for extracting values from a pattern. In a statically typed language the compiler has the option of forcing the user to handle every possible type permutation with a pattern thus ensuring safety.
Polymorphism is just a generic type.
That's as far as I know... could you elaborate more on what you mean?
sum types:
you can easily add new functions that accept your sum type, but adding a new "variant" to the type will require modifying every function you've written to support the new case
polymorphism (interfaces):
you can easily add new types that implement your interface, but if you want to add a new method/function to it, you're going to have to implement it for every type that implements your interface
essentially, we've got two "extensibility dimensions": one for adding new operations, and one for adding new types. sum types are good on the operation axis, interfaces are good on the type axis. (it's an open research question if there's a solution that's good on both.) i hope this makes sense!
as a side note, when discussing this, it's useful to distinguish ad-hoc polymorphism (subclassing, interfaces/typeclasses) and parametric polymorphism (generics) – here, we're talking about the "ad-hoc" variety. I mention this because i saw some confusion about this in a sibling thread but can't reply there.
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.
$ 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.)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.
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...
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.
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....
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.
In guile the module (ice-9 match) generally has zero runtime cost compared to equal hand-written code.
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.
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.
with matching(expr) as m:
@m(pattern)
def do_for_pattern(v):
...
@m(_)
def catchall(v):
... m(pattern)(lambda v: ...)
Design would be to consider the execution of the match and dispatch as a “cleanup” of the specification created in the block. Which makes a ton of sense in a weird way.May end up using this, or one of us should send a PR to the OP!
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)