The Pattern Matching for Python you always dreamed of

## Project description # Pampy: Pattern Matching for Python

Pampy is pretty small (150 lines), reasonably fast, and often makes your code more readable and hence easier to reason about. There is also a JavaScript version, called Pampy.js. ## You can write many patterns

Patterns are evaluated in the order they appear. ## You can write Fibonacci

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)
)
```

## You can write a Lisp calculator in 5 lines

```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
```

## You can match so many things!

```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"
)
```

## You can match [HEAD, TAIL]

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

## You can nest lists and tuples

```from pampy import match, _

x = [1, [2, 3], 4]

match(x, [1, [_, 3], _], lambda a, b: [1, [a, 3], b])           # => [1, [2, 3], 4]
```

## You can nest dicts. And you can use _ as key!

```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

## You can match class hierarchies

```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'
```

## Using Dataclasses

Pampy supports Python 3.7 dataclasses. You can pass the operator `_` as arguments and it will match those fields.

```@dataclass
class Pet:
name: str
age: int

pet = Pet('rover', 7)

match(pet, Pet('rover', _), lambda age: age)                    # => 7
match(pet, Pet(_, 7), lambda name: name)                        # => 'rover'
match(pet, Pet(_, _), lambda name, age: (name, age))            # => ('rover', 7)
```

## Using typing

Pampy supports typing annotations.

```class Pet:          pass
class Dog(Pet):     pass
class Cat(Pet):     pass
class Hamster(Pet): pass

timestamp = NewType("year", Union[int, float])

def annotated(a: Tuple[int, float], b: str, c: E) -> timestamp:
pass

match((1, 2), Tuple[int, int], lambda a, b: (a, b))             # => (1, 2)
match(1, Union[str, int], lambda x: x)                          # => 1
match('a', Union[str, int], lambda x: x)                        # => 'a'
match('a', Optional[str], lambda x: x)                          # => 'a'
match(None, Optional[str], lambda x: x)                         # => None
match(Pet, Type[Pet], lambda x: x)                              # => Pet
match(Cat, Type[Pet], lambda x: x)                              # => Cat
match(Dog, Any, lambda x: x)                                    # => Dog
match(Dog, Type[Any], lambda x: x)                              # => Dog
match(15, timestamp, lambda x: x)                               # => 15
match(10.0, timestamp, lambda x: x)                             # => 10.0
match([1, 2, 3], List[int], lambda x: x)                        # => [1, 2, 3]
match({'a': 1, 'b': 2}, Dict[str, int], lambda x: x)            # => {'a': 1, 'b': 2}
match(annotated,
Callable[[Tuple[int, float], str, Pet], timestamp], lambda x: x
)                                                               # => annotated
```

For iterable generics actual type of value is guessed based on the first element.

```match([1, 2, 3], List[int], lambda x: x)                        # => [1, 2, 3]
match([1, "b", "a"], List[int], lambda x: x)                    # => [1, "b", "a"]
match(["a", "b", "c"], List[int], lambda x: x)                  # raises MatchError
match(["a", "b", "c"], List[Union[str, int]], lambda x: x)      # ["a", "b", "c"]

match({"a": 1, "b": 2}, Dict[str, int], lambda x: x)            # {"a": 1, "b": 2}
match({"a": 1, "b": "dog"}, Dict[str, int], lambda x: x)        # {"a": 1, "b": "dog"}
match({"a": 1, 1: 2}, Dict[str, int], lambda x: x)              # {"a": 1, 1: 2}
match({2: 1, 1: 2}, Dict[str, int], lambda x: x)                # raises MatchError
match({2: 1, 1: 2}, Dict[Union[str, int], int], lambda x: x)    # {2: 1, 1: 2}
```

Iterable generics also match with any of their subtypes.

```match([1, 2, 3], Iterable[int], lambda x: x)                     # => [1, 2, 3]
match({1, 2, 3}, Iterable[int], lambda x: x)                     # => {1, 2, 3}
match(range(10), Iterable[int], lambda x: x)                     # => range(10)

match([1, 2, 3], List[int], lambda x: x)                         # => [1, 2, 3]
match({1, 2, 3}, List[int], lambda x: x)                         # => raises MatchError
match(range(10), List[int], lambda x: x)                         # => raises MatchError

match([1, 2, 3], Set[int], lambda x: x)                          # => raises MatchError
match({1, 2, 3}, Set[int], lambda x: x)                          # => {1, 2, 3}
match(range(10), Set[int], lambda x: x)                          # => raises MatchError
```

For Callable any arg without annotation treated as Any.

```def annotated(a: int, b: int) -> float:
pass

def not_annotated(a, b):
pass

def partially_annotated(a, b: float):
pass

match(annotated, Callable[[int, int], float], lambda x: x)     # => annotated
match(not_annotated, Callable[[int, int], float], lambda x: x) # => raises MatchError
match(not_annotated, Callable[[Any, Any], Any], lambda x: x)   # => not_annotated
match(annotated, Callable[[Any, Any], Any], lambda x: x)       # => raises MatchError
match(partially_annotated,
Callable[[Any, float], Any], lambda x: x
)                                                              # => partially_annotated
```

TypeVar is not supported.

## All the things you can match

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"`
`Pet(name=_, age=7)` Any Pet dataclass with `age == 7` `Pet('rover', 7)` `['rover']` `Pet('rover', 8)`
`Any` The same as `_` that value
`Union[int, float, None]` Any integer or float number or None `2.35` `2.35` any other value
`Optional[int]` The same as `Union[int, None]` `2` `2` any other value
`Type[MyClass]` Any subclass of MyClass. And any class that extends MyClass. `MyClass` that class any other object
`Callable[[int], float]` Any callable with exactly that signature `def a(q:int) -> float: ...` that function `def a(q) -> float: ...`
`Tuple[MyClass, int, float]` The same as `(MyClass, int, float)`
`Mapping[str, int]` Any subtype of `Mapping` acceptable too any mapping or subtype of mapping with string keys and integer values `{'a': 2, 'b': 3}` that dict `{'a': 'b', 'b': 'c'}`
`Iterable[int]` Any subtype of `Iterable` acceptable too any iterable or subtype of iterable with integer values `range(10)` and `[1, 2, 3]` that iterable `['a', 'b', 'v']`

## Using default

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

You can instead provide a fallback value using `default` to be used when 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", default=False)
False
``````

## Using Regular Expressions

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'
```

## Install for Python3

Pampy works in Python >= 3.6 Because dict matching can work only in the latest Pythons.

To install it:

`\$ pip install pampy`

or `\$ pip3 install pampy`

## If you really must use Python2

Pampy is Python3-first, but you can use most of its features in Python2 via this backport by Manuel Barkhau:

`pip install backports.pampy`

```from backports.pampy import match, HEAD, TAIL, _
```

## Project details

This version 0.3.0 0.2.1 0.2.0 0.1.10 0.1.9 0.1.8 0.1.7 0.1.6 0.1.5 0.1.4 0.1.3