This module is both Python 2 and 3 compatible.
Table of contents
Two families of decorators are introduced:
case: allows multiple function clause definitions and dispatches to correct one. Dispatch happens on the values of call arguments or, more generally, when call arguments’ values match specified guard definitions.
dispatch: convenience decorator for dispatching on argument types. Equivalent to using
guardwith type checking.
guard: allows arguments’ values filtering and raises
GuardErrorwhen argument value does not pass through argument guard.
rguard: Wrapper for
guardwhich converts first positional decorator argument to relguard. See.
rguard, but converts return annotation. See.
- All Python versions:
import function_pattern_matching as fpm @fpm.case def factorial(n=0): return 1 @fpm.case @fpm.guard(fpm.is_int & fpm.gt(0)) def factorial(n): return n * factorial(n - 1)
- Python 3 only:
import function_pattern_matching as fpm @fpm.case def factorial(n=0): return 1 @fpm.case @fpm.guard def factorial(n: fpm.is_int & fpm.gt(0)): # Guards specified as annotations return n * factorial(n - 1)
Of course that’s a poor implementation of factorial, but illustrates the idea in a simple way.
Note:This module does not aim to be used on production scale or in a large sensitive application (but I’d be happy if someone decided to use it in his/her project). I think of it more as a fun project which shows how flexible Python can be (and as a good training for myself).
I’m aware that it’s somewhat against duck typing and EAFP (easier to ask for forgiveness than for permission) philosophy employed by the language, but obviously there are some cases when preliminary checks are useful and make code (and life) much simpler.
function-pattern-matching can be installed with pip:
$ pip install function-pattern-matching
Module will be available as
function_pattern_matching . It is recommended to import as
guard decorator it is possible to filter function arguments upon call. When argument value does not pass through specified guard, then
GuardError is raised.
When global setting
strict_guard_definitions is set
True (the default value), then only
GuardFunc instances can be used in guard definitions. If it’s set to
False , then any callable is allowed, but it is not recommended, as guard behaviour may be unexpected (
RuntimeWarning is emitted), e.g. combining regular callables will not work.
GuardFunc objects can be negated with
~ and combined together with
^ logical operators. Note however, that xor isn’t very useful here.
Note:It is not possible to put guards on varying arguments (*args, **kwargs).
List of provided guard functions
Every following function returns/is a callable which takes only one parameter – the call argument that is to be checked.
_– Catch-all. Returns
Truefor any input. Actually, this can take any number of arguments.
eq(val)– checks if input is equal to val
ne(val)– checks if input is not equal to val
lt(val)– checks if input is less than val
le(val)– checks if input is less or equal to val
gt(val)– checks if input is greater than val
ge(val)– checks if input is greater or equal to val
Is(val)– checks if input is val (uses
Isnot(val)– checks if input is not val (uses
isoftype(_type)– checks if input is instance of _type (uses
isiterable– checks if input is iterable
eTrue– checks if input evaluates to
True(converts input to
eFalse– checks if input evaluates to
False(converts input to
In(val)– checks if input is in val (uses
notIn(val)– checks if input is not in val (uses
Although it is not advised (at least for simple checks), you can create your own guards:
- by using
makeguarddecorator on your test function.
- by writing a function that returns a
GuardFuncobject initialised with a test function.
Note that a test function must have only one positional argument.
# use decorator @fpm.makeguard def is_not_zero_nor_None(inp): return inp != 0 and inp is not None # return GuardFunc object def is_not_val_nor_specified_thing(val, thing): return GuardFunc(lambda inp: inp != val and inp is not thing) # equivalent to (fpm.ne(0) & fpm.Isnot(None)) | (fpm.ne(1) & fpm.Isnot(some_object)) @fpm.guard(is_not_zero_nor_None | is_not_val_nor_specified_thing(1, some_object)) def guarded(argument): pass
The above two are very similar, but the second one allows creating function which takes multiple arguments to construct actual guard.
Note:It is not recommended to create your own guard functions. In most cases combinations of the ones shipped with fpm should be all you need.
Define guards for function arguments
There are two ways of defining guards:
As decorator arguments
positionally: guards order will match decoratee’s (the function that is to be decorated) arguments order.
@fpm.guard(fpm.isoftype(int) & fpm.ge(0), fpm.isiterable) def func(number, iterable): pass
as keyword arguments: e.g. guard under name a will guard decoratee’s argument named a .
@fpm.guard( number = fpm.isoftype(int) & fpm.ge(0), iterable = fpm.isiterable ) def func(number, iterable): pass
As annotations (Python 3 only)
@fpm.guard def func( number: fpm.isoftype(int) & fpm.ge(0), iterable: fpm.isiterable ): # this is NOT an emoticon pass
If you try to declare guards using both methods at once, then annotations get ignored and are left untouched.
Relguard is a kind of guard that checks relations between arguments (and/or external variables).
fpm implements them as functions (wrapped in
RelGuard object) whose arguments are a subset of decoratee’s arguments (no arguments is fine too).
There are a few ways of defining a relguard.
guardwith the first (and only) positional non-keyword argument of type
@fpm.guard( fpm.relguard(lambda a, c: a == c), # converts lambda to RelGuard object in-place a = fpm.isoftype(int) & fpm.eTrue, b = fpm.Isnot(None) ) def func(a, b, c): pass
guardwith the return annotation holding a
RelGuardobject (Python 3 only):
@fpm.guard def func(a, b, c) -> fpm.relguard(lambda a, b, c: a != b and b < c): pass
rguardwith a regular callable as the first (and only) positional non-keyword argument.
@fpm.rguard( lambda a, c: a == c, # rguard will try converting this to RelGuard object a = fpm.isoftype(int) & fpm.eTrue, b = fpm.Isnot(None) ) def func(a, b, c): pass
raguardwith a regular callable as the return annotation.
@fpm.raguard def func(a, b, c) -> lambda a, b, c: a != b and b < c: # raguard will try converting lambda to RelGuard object pass
As you can see, when using
guard you have to manually convert functions to
RelGuard objects with
relguard method. By using
raguard decorators you don’t need to do it by yourself, and you get a bit cleaner definition.
Multiple function clauses
case decorator you are able to define multiple clauses of the same function.
When such a function is called with some arguments, then the first matching clause will be executed. Matching clause will be the one that didn’t raise a
GuardError when called with given arguments.
dispatch (discussed later) disables default functionality of default argument values. Functions with varying arguments (*args, **kwargs) and keyword-only arguments (py3-only) are not supported.
@fpm.case def func(a=0): print("zero!") @fpm.case def func(a=1): print("one!") @fpm.case @fpm.guard(fpm.gt(9000)) def func(a): print("IT'S OVER 9000!!!") @fpm.case def func(a): print("some var:", a) # catch-all clause >>> func(0) 'zero!' >>> func(1) 'one!' >>> func(9000.1) "IT'S OVER 9000!!!" >>> func(1337) 'some var: 1337'
If no clause matches, then
MatchError is raised. The example shown above has a catch-all clause, so
MatchError will never occur.
Different arities (argument count) are allowed and are dispatched separetely.
@fpm.case def func(a=1, b=1, c): return 1 @fpm.case def func(a, b, c): return 2 @fpm.case def func(a=1, b=1, c, d): return 3 @fpm.case def func(a, b, c, d): return 4 >>> func(1, 1, 'any') 1 >>> func(1, 0, 0.5) 2 >>> func(1, 1, '', '') 3 >>> func(1, 0, 0, '') 4
As you can see, clause order matters only for same-arity clauses. 4-arg catch-all does not affect any 3-arg definition.
Define multi-claused functions
There are three ways of defining a pattern for a function clause:
Specify exact values as decorator arguments (positional and/or keyword)
@fpm.case(1, 2, 3) def func(a, b, c): pass @fpm.case(1, fpm._, 0) def func(a, b, c): pass @fpm.case(b=10) def func(a, b, c): pass
Specify exact values as default arguments
@fpm.case def func(a=0): pass @fpm.case def func(a=10): pass @fpm.case def func(a=fpm._, b=3): pass
Specify guards for clause to match
@fpm.case @fpm.guard(fpm.eq(0) & ~fpm.isoftype(float)) def func(a): pass @fpm.case @fpm.guard(fpm.gt(0)) def func(a): pass @fpm.case @fpm.guard(fpm.Is(None)) def func(a): pass
dispatch decorator is similar to
case , but it lets you to define argument types to match against. You can specify types either as decorator arguments or default values (or as guards, of course, but it makes using
@fpm.dispatch(int, int) def func(a, b): print("integers") @fpm.dispatch def func(a=float, b=float): print("floats") >>> func(1, 1) 'integers' >>> func(1.0, 1.0) 'floats'
Examples (the useful ones)
Working on it!
- singledispatch from functools
- http://www.artima.com/weblogs/viewpost.jsp?thread=101605 (by Guido van Rossum, BDFL)
MIT (c) Adrian Włosiak