Pampy is pretty small (150 lines), reasonably fast, and often makes your code more readable and hence easier to reason about.
Patterns are evaluated in the order they appear.
The operator _ means "any other case I didn't think of".
from pampy import match, _
def fibonacci(n):
return match(n,
1, 1,
2, 1,
_, lambda x: fibonacci(x-1) + fibonacci(x-2)
)
from pampy import match, REST, _
def lisp(exp):
return match(exp,
int, lambda x: x,
callable, lambda x: x,
(callable, REST), lambda f, rest: f(*map(lisp, rest)),
tuple, lambda t: list(map(lisp, t)),
)
plus = lambda a, b: a + b
minus = lambda a, b: a - b
from functools import reduce
lisp((plus, 1, 2)) # => 3
lisp((plus, 1, (minus, 4, 2))) # => 3
lisp((reduce, plus, (range, 10))) # => 45
match(x,
3, "this matches the number 3",
int, "matches any integer",
(str, int), lambda a, b: "a tuple (a, b) you can use in a function",
[1, 2, _], "any list of 3 elements that begins with [1, 2]",
{'x': _}, "any dict with a key 'x' and any value associated",
_, "anything else"
)
from pampy import match, HEAD, TAIL, _
x = [1, 2, 3]
match(x, [1, TAIL], lambda t: t) # => [2, 3]
match(x, [HEAD, TAIL], lambda h, t: (h, t)) # => (1, [2, 3])
TAIL
and REST
actually mean the same thing.
from pampy import match, _
x = [1, [2, 3], 4]
match(x, [1, [_, 3], _], lambda a, b: [1, [a, 3], b]) # => [1, [2, 3], 4]
pet = { 'type': 'dog', 'details': { 'age': 3 } }
match(pet, { 'details': { 'age': _ } }, lambda age: age) # => 3
match(pet, { _ : { 'age': _ } }, lambda a, b: (a, b)) # => ('details', 3)
It feels like putting multiple _ inside dicts shouldn't work. Isn't ordering in dicts not guaranteed ? But it does because in Python 3.7, dict maintains insertion key order by default
class Pet: pass
class Dog(Pet): pass
class Cat(Pet): pass
class Hamster(Pet): pass
def what_is(x):
return match(x,
Dog, 'dog',
Cat, 'cat',
Pet, 'any other pet',
_, 'this is not a pet at all',
)
what_is(Cat()) # => 'cat'
what_is(Dog()) # => 'dog'
what_is(Hamster()) # => 'any other pet'
what_is(Pet()) # => 'any other pet'
what_is(42) # => 'this is not a pet at all'
As Pattern you can use any Python type, any class, or any Python value.
The operator _
and built-in types like int
or str
, extract variables that are passed to functions.
Types and Classes are matched via instanceof(value, pattern)
.
Iterable
Patterns match recursively through all their elements. The same goes for dictionaries.
Pattern Example | What it means | Matched Example | Arguments Passed to function | NOT Matched Example |
---|---|---|---|---|
"hello" |
only the string "hello" matches |
"hello" |
nothing | any other value |
None |
only None |
None |
nothing | any other value |
int |
Any integer | 42 |
42 |
any other value |
float |
Any float number | 2.35 |
2.35 |
any other value |
str |
Any string | "hello" |
"hello" |
any other value |
tuple |
Any tuple | (1, 2) |
(1, 2) |
any other value |
list |
Any list | [1, 2] |
[1, 2] |
any other value |
MyClass |
Any instance of MyClass. And any object that extends MyClass. | MyClass() |
that instance | any other object |
_ |
Any object (even None) | that value | ||
ANY |
The same as _ |
that value | ||
(int, int) |
A tuple made of any two integers | (1, 2) |
1 and 2 |
(True, False) |
[1, 2, _] |
A list that starts with 1, 2 and ends with any value | [1, 2, 3] |
3 |
[1, 2, 3, 4] |
[1, 2, TAIL] |
A list that start with 1, 2 and ends with any sequence | [1, 2, 3, 4] |
[3, 4] |
[1, 7, 7, 7] |
{'type':'dog', age: _ } |
Any dict with type: "dog" and with an age |
{"type":"dog", "age": 3} |
3 |
{"type":"cat", "age":2} |
{'type':'dog', age: int } |
Any dict with type: "dog" and with an int age |
{"type":"dog", "age": 3} |
3 |
{"type":"dog", "age":2.3} |
re.compile('(\w+)-(\w+)-cat$') |
Any string that matches that regular expression expr | "my-fuffy-cat" |
"my" and "puffy" |
"fuffy-dog" |
By default match()
is strict. If no pattern matches, it raises a MatchError
.
You can prevent it using strict=False
. In this case match
just returns False
if nothing matches.
>>> match([1, 2], [1, 2, 3], "whatever")
MatchError: '_' not provided. This case is not handled: [1, 2]
>>> match([1, 2], [1, 2, 3], "whatever", strict=False)
False
Pampy supports Python's Regex. You can pass a compiled regex as pattern, and Pampy is going to run patter.search()
, and then pass to the action function the result of .groups()
.
def what_is(pet):
return match(pet,
re.compile('(\w+)-(\w+)-cat$'), lambda name, my: 'cat '+name,
re.compile('(\w+)-(\w+)-dog$'), lambda name, my: 'dog '+name,
_, "something else"
)
what_is('fuffy-my-dog') # => 'dog fuffy'
what_is('puffy-her-dog') # => 'dog puffy'
what_is('carla-your-cat') # => 'cat carla'
what_is('roger-my-hamster') # => 'something else'
Currently it works only in Python >= 3.6 Because dict matching can work only in the latest Pythons.
I'm currently working on a backport with some minor syntax changes for Python2.
To install it:
$ pip install pampy
or
$ pip3 install pampy