Allow natural function notations like (1,2) *dot* (3,4) for dot((1,2), (3,4)) or 1 /frac/ 3 for Fraction(1,3), pipes and other useful operators to functions.

## Project description

Always wanted to add custom operators to your functions ?

a = 2 + (1,2,3) /dot/ (4,5,6) # a = 2 + dot((1,2,3), (4,5,6)) Y = [1,2,7,0,2,0] |no_zero |plus(1) |to(set) # Y == {2,3,8} square = elipartial(pow, ..., 2) # square = lambda x: pow(x, 2) display = hex *compose* ord # display = lambda x: hex(ord(x))

This example shows how infix operators can be created,
the library also introduces bash like *pipes* and
shortcuts to create partial functions or function
composition inspired by functional languages.

# Using infix

Infix operators can be created using the `infix`

class.

It works for existing functions, like `numpy.dot`

:

import numpy dot = infix(numpy.dot) a = (1,2,3) /dot/ (4,5,6) # use as an infix

If you already have `dot`

in your namespace, don't worry, it still works as a function:

a = dot((1,2,3), (4,5,6)) # still works as a function

Or for custom functions as a decorator:

@infix def crunch(x,y): """ Do a super crunchy operation between two numbers. """ return x + 2 * y a = 1 |crunch| 2 # a = crunch(1, 2) a = crunch(1, 2) # still works help(crunch.function) # to get help about the initial function

Any binary operator can be used, `1 |f| 2`

can be written `1 *f* 2`

, `1 %f% 2`

or `1 << f << 2`

but `/`

or `|`

should be clean for all use cases.
Beware if you use `**`

, the operator is right to left:

b = 1 **f** 2 # a = f(2, 1)

# Useful for dot and cross product

Dot and cross products are used heavily in mathematics and physics as an infix operator `·`

or `×`

.

import numpy dot = infix(numpy.dot) a = (1,2,3) /dot/ (4,5,6) a = (1,2,3) |dot| (4,5,6) # same r = 2 + (1,2,3) /dot/ (4,5,6) # here "/" has priority over "+" like in normal python r = 2 + (1,2,3) *dot* (4,5,6) # for a dot PRODUCT, "*" seems logical r = 2 + dot((1,2,3), (4,5,6)) # still works as a function cross = infix(numpy.cross) tau = (1,2) /cross/ (3,4) Z = (1,2,3) /cross/ (4,5,6)

# Using `|`

for low priority

In some use cases, one want to mix classic operators with function operators,
the `|`

operator may be used as a low priority operator.

Y = A + B |dot| C # is parsed as Y = (A + B) |dot| C Y = A + B /dot/ C # is parsed as Y = A + (B /dot/ C)

# Useful for fractions

When using the `fractions`

module, often you want to transition from `float`

to `Fraction`

.
Your current code uses `/`

for division and you can just replace the slashes with `/frac/`

, the expression stays natural to read.

from fractions import Fraction frac = infix(Fraction) a = 1 + 1 / 3 # 1.3333... a = 1 + 1 /frac/ 3 # Fraction(4, 3) b = 2 * Fraction(a + 3, a + 1) # very different from '(a + 3) / (a + 1)' b = 2 * (a + 3) /frac/ (a + 1) # almost identical to '(a + 3) / (a + 1)'

# Useful for ranges, do you like `2..5`

in ruby?

In many languages, iterating over a range has a notational shortcut, like `2..5`

in ruby.
Now you can even write `for i in 1 /inclusive/ 5`

in python.

@infix def inclusive(a,b): return range(a, b+1) for i in 2 /inclusive/ 5: print(i) # 2 3 4 5 for i in inclusive(2, 5): print(i) # 2 3 4 5

However, redefining `range = infix(range)`

is a bad idea because it would break code like `isinstance(x, range)`

.
In that particuliar example, I would choose `exclusive = infix(range)`

.

# Useful for isinstance, do you like `instanceof`

in Java and Js?

In Java and Javascript, testing the class of an object is done via `x instanceof Class`

,
the python builtin `isinstance`

could be enhanced with infix notation or be renamed to `instanceof`

.

isinstance = infix(isinstance) assert 1 /isinstance/ int assert [] /isinstance/ (list, tuple) assert 1 / 2 |isinstance| float

# Useful for pipes: postfix (alias to)

In bash, a functionality called *pipes* is useful to reuse an expression and change the behavior by just adding code *at the end*.
The library can be used for that.

@postfix def no_zero(L): return [x for x in L if x != 0] @postfix def plus_one(L): return [x+1 for x in L] Y = [1,2,7,0,2,0] |no_zero |plus_one # Y == [2,3,8,3] Y = plus_one(no_zero([1,2,7,0,2,0])) # Y == [2,3,8,3]

Using `to = postfix`

makes it quite readable in a pipe.

>>> from funcoperators import postfix as to # funcoperators version 0.x >>> from funcoperators import to # funcoperators version 1.x >>> 'hello' |to(str.upper) |to(lambda x:x + '!') |to('I said "{}"'.format) |to(print) I said "HELLO!" >>> print('I said "{}"'.format('hello'.upper() + '!')) I said "HELLO!"

# Pipes with arguments: pipe factory

Sometimes, pipes want extra information, for example in our last example, `no_zero`

is a special case of a pipe that filters out a value,
use the *pipe factory* recipe like so:

def filter_out(x): @postfix def f(L): return [y for y in L if y != x] return f # shorter with a lambda def filter_out(x): return postfix(lambda L:[y for y in L if y != x]) L = [1,2,7,0,2,0] | filter_out(0) # L == [2,3,8,3] from funcoperators import mapwith s = '1 2 7 2'.split() | mapwith(int) | to(sum) # s = 12 = sum(map(int, '1 2 7 2'.split())) from funcoperators import mapwith as Map from funcoperators import filterwith as Filter S = '1 2 7 2'.split() | Map(int) | Filter(lambda x:x < 5) | to(set)

# Useful for format and join

>>> 42 | format /curryright/ 'x' '2a' >>> formatwith = lambda fmt: postfix(lambda value: format(value, fmt)) >>> 52 |formatwith('x') '2a' >>> from funcoperators import to >>> 3.1415 |to('{:.02}'.format) '3.1' >>> [1, 2, 7, 2] |mapwith(str) |to('/'.join) |to(print) 1/2/7/2

# Function composition (compose, alias circle)

In mathematics and functional programming languages, function composition is naturally used using a `circle`

operator to write things like `h = f ∘ g`

.

s = hex(ord('A')) # s = '0x41' from funcoperators import compose display = hex /compose/ ord s = display('A') # s = '0x41' display = hex *circle* ord from funcoperators import compose as o display = hex -o- ord # looks like a dot

`compose`

can have more than two functions.

f = compose(str.upper, hex, lambda x:x+1, ord) # simple function f = str.upper /compose/ hex /compose/ (lambda x:x+1) /compose/ ord # operator

## Using call for inline compose

>>> print(5 + 2 * 10, 'B', sep='/') 25 >>> print |call(5 + 2 * 10, 'B', sep='/') 25/B >>> print(','.join('abcdef' + 3 * 'x')) a,b,c,d,e,f,x,x,x >>> print *compose* ' '.join |call('abcdef' + 3 * 'x') a,b,c,d,e,f,x,x,x >>> compose(print, ','.join) |call('abcdef' + 3 * 'x') a,b,c,d,e,f,x,x,x

>>> len |infixcall| 'hallo' * 3 15

>>> print |callargs| ('a', 5) a 5

# More partial syntax

The library adds sugar to functools.partial, using functions called `curry`

(and variants like `curryright`

, `simplecurry`

) and `partially`

. The name `curry`

comes from other languages.

def f(x,y,z): return x + y + z from funcoperators import curry g = f /curry/ 5 y = f(2,1) # y = 8 from funcoperators import curryright square = pow /curryright/ 2 # square(x) = x ** 2 square = curryright(pow, 2) # square(x) = x ** 2 from funcoperators import provide_right # alias provide_right = curryright square = provide_right(pow, 2) # square(x) = x ** 2 square = pow /provide_right/ 2 # square(x) = x ** 2 from funcoperators import simplecurry g = f |simplecurry(1, z=3) y = g(2)

`partially`

allows to *upgrade* a function to provide methods like `f.partial`

and provides `f[arg]`

to curry.

from funcoperators import partially @partially def f(x,y,z): return x - y + 2 * z r = f(1,2,3) g = f[1] # g = a function with two arguments: y,z r = g(2,3) r = f[1](2,3) r = f[1][2][3]() # Notice that "f[1,2]" doesn't work because it gives only one argument: a tuple (@see partiallymulti) g = f[1] # gives positional arguments g = f.val(1) # gives positional arguments g = f.key(z=3) # gives keyword arguments g = f.partial(1, z=3) # gives positional and keyword arguments # alias part = assuming = given = where = partial g = f.part(1, z=3) g = f.where(1, z=3) g = f.given(1, z=3)

`partiallymulti`

allows `f[arg1, arg2]`

.

from funcoperators import partiallymulti @partiallymulti def f(x,y,z): return x - y + 2 * z r = f(1,2,3) g = f[1,2] # g = a function with one argument: z r = g(3)

# Using partiallyauto

In functional languages, function composition is sometimes not dissociable from function call,
`partiallyauto`

only works for methods with N fixed positional arguments.

@partiallyauto def f(x,y,z): return x - y + 2 * z r = f(1,2,3) # r = 6 r = f(1)(2)(3) # r = 6 r = f(1)(2,3) # r = 6 g = f(1) # g = a function with two arguments r = g(2,3) # r = 6 k = g(2) # k = a function with one argument

# Using Ellipsis

Python's `functools.partial`

only works for arguments that will be provided later, one must use keywords arguments.
However, not all functions accept keywords arguments, like the builtin `pow`

, one can use `curryright`

because pow only has two arguments.

square = curryright(pow, 2) # square(x) = x ** 2

The library also proposes to use Python's `...`

(`Ellipsis`

) as a natural placeholder for arguments.
The functions using this convention have a name beginning with `eli`

.

tenexp = elipartial(pow, 10) # = pow(10, something) y = tenexp(2) # 10 ** 2 square = elipartial(pow, ..., 2) # = pow(something, 2) y = square(5) # 5 ** 2 square = pow |elicurry(..., 2) # = pow(something, 2) y = square(5) # 5 ** 2

If you like the `partially`

and `partiallymulti`

syntax, there is `bracket`

that has all the concepts in one class.

@bracket def f(x,y,z): return x - y + 2 * z r = f(1,2,3) g = f[1, ..., 3] # g = a function with one argument: y r = g(2) g = f.partial(1, ..., 3) # as a method g = f.partial(1, z=3) # allowing keyword arguments

Here is a more complex example using `elicurry`

, we define `show`

to be the `print`

function with arguments `1`

, *something*, `3`

and keyword argument `sep='/'`

.

show = print |elicurry(1, ..., 3, sep='/') show(2) # prints 1/2/3

Let's note that `elicurry`

has many aliases:

show = print |elicurry(1, ..., 3, sep='/') show = print |with_arguments(1, ..., 3, sep='/') show = print |deferredcall(1, ..., 3, sep='/') show = print |latercall(1, ..., 3, sep='/')

# More examples

See more examples in the test cases in source code.

# Release Notes

Version 1.0 created some non backward-compatible change (`call`

) and included useful use cases (`to`

, `mapwith`

)

## Project details

## Release history Release notifications

## Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Filename, size & hash | File type | Python version | Upload date |
---|---|---|---|

funcoperators-1.1.3.tar.gz (15.3 kB) View hashes | Source | None |