Skip to content

apfel.core.function_object

Extend functions with operator overloads for function calling.

Conceptually, function objects are objects that implement the function call operator.

Rationale

Python codes contain extensive use of parentheses, which can be cumbersome in interactive environments. This module simply provides a way to reduce the pain of wrapping huge expressions in parentheses. You can use application operators provided by FunctionObject to apply the function on the following expression. Use the fob function to turn a function or a sequence of functions into FunctionObjects.

Usage

For example, f(..) is equivalent to f | (...) or f @ (...). Writing f(...) as f@(...) seems redundant, but these @ usages can be purged easily with simple search and replace. FunctionObject’s operators are carefully chosen, as they:

  1. are relatively rare in Python
  2. have different precedence and associativity

This is especially useful when you wrap some debugging or logging code around a function call. For example, with the following usage with icecream, you can replace ic @ with empty string to remove all debugging code, without worrying of breaking the get_pic() call:

from icecream import ic as _ic
ic = fob(_ic)
result = ic @ (
get_pic(iris)
.filter(...)
.sort_values(...)
.apply(...)
.head(10)
)

| and @ have different operator precedence, the | has almost the lowest precedence of all Python operators, while the @ has almost the highest precedence. This allows you combine expressions more flexibly without worrying about parentheses.

& operator can be used to apply the function to its left-hand side, if the left-hand side does not overload the & operator. This is similar to &, |> or roughly %>%. With & you can write your code naturally from left to right.

from icecream import ic as _ic
ic = fob(_ic)
result = (
get_pic(iris)
.filter(...)
# ...
) & ic

** operator has the highest precedence of all, and it has a unique associtivity from right to left. It can be used in wrapping multiple calls together without parentheses, like f(g(x)) can be written as f ** g ** x. It roughly simulates $.

% operator is used for calling multi-argument functions. Check its documentation for more details.

API#

Classes

NameDescription
FunctionObjectNo description.
FunctionObjectA0No description.
FunctionObjectA0PNo description.
FunctionObjectA1No description.
FunctionObjectA1PNo description.

Functions

NameDescription
fobpython
reveal_fobpython

FunctionObjectclass#

class FunctionObject(func)

__call__attributeinstance attribute#

__call__ = func

__doc__attributeinstance attribute#

__doc__ = func.__doc__

__name__attributeinstance attribute#

__name__ = getattr(func, '__name__', '<λ>')

__qualname__attributeinstance attribute#

__qualname__ = getattr(func, '__qualname__', '<λ>')

__slots__attributeclass attributeinstance attribute#

__slots__ = ('__call__', '__wrapped__', '__doc__', '__name__', '__qualname__')

__wrapped__attributeinstance attribute#

__wrapped__ = func

__init__method#

def __init__(func)

__matmul__method#

def __matmul__(value)
def @[T, R](self, value: T) -> R

Function application operator @ for FunctionObjects.

f @ x is equivalent to f(x). This operator behaves the same as |, but with a different precedence.

@fob
def f(x):
return x * 2
f @ 1 + 2
# 4

__mod__method#

def __mod__(value)
def %[R](self, value) -> R

Function application operator % for FunctionObjects of multi-argument functions.

  • If the right hand side is a Sequence, spreads the sequence as positional arguments. For example, x % (a, b, c) is equivalent to x(a, b, c).
  • If the right hand side is a Mapping, spreads the mapping as keyword arguments. For example, x % { "a": 1, "b": 2, "c": 3 } is equivalent to x(a=1, b=2, c=3).
  • Specifically, you can use ... as the map key to pass keyword arguments with keyword arguments at the same time, x % { ...: (1, 2), "c": 3 } is equivalent to x(1, 2, c=3).
  • Otherwise, it calls on the right-hand side. This catches the case where you forget the trailing comma in the right-hand side tuple.
@fob
def f(a, b, c):
return a + b + c
f % (1, 2, 3) # 6
f % { "a": 1, "b": 2, "c": 3 } # 6
f % { ...: (1, 2), "c": 3 } # 6

__or__method#

def __or__(value, /)
def |[T, R](self, value: T) -> R

Function application operator | for FunctionObjects.

f | x is equivalent to f(x). This operator behaves the same as @, but with a different precedence.

@fob
def f(x):
return x * 2
f | 1 + 2
# 6

__pow__method#

def __pow__(value)
def **[T, R](self, value: T) -> R

Function application operator ** for FunctionObjects.

f ** g ** x is equivalent to f(g(x)). This operator has the highest precedence of all overloadable operators, and it binds from right to left. It intends to simulate $ operator, except the precedence.

@fob
def f(x):
return x + 1
@fob
def g(x):
return x * 2
f ** g ** 1
# 3

__rand__method#

def __rand__(value)
def &[T, R](self, value: T) -> R

Reverse function application operator & for FunctionObjects. This operator overloading targets the right-hand side.

x & f is equivalent to f(x), if & operator (left, __and__) is not overloaded by x’s type.

@fob
def f(x):
return x + 1
1 + 2 & f
# 4

__repr__method#

def __repr__()

__str__method#

def __str__()

FunctionObjectA0class#

class FunctionObjectA0(FunctionObject)

Bases: FunctionObject

__call__method#

def __call__() -> R

FunctionObjectA0Pclass#

class FunctionObjectA0P(FunctionObject)

Bases: FunctionObject

__call__method#

def __call__(*args: P.args, **kwargs: P.kwargs) -> R

FunctionObjectA1class#

class FunctionObjectA1(FunctionObject)

Bases: FunctionObject

__call__method#

def __call__(value: T) -> R

__matmul__method#

def __matmul__(value: T) -> R

__or__method#

def __or__(value: T) -> R

__pow__method#

def __pow__(value: T) -> R

__rand__method#

def __rand__(value: T) -> R

FunctionObjectA1Pclass#

class FunctionObjectA1P(FunctionObject)

Bases: FunctionObject

__call__method#

def __call__(value: T, *args: P.args, **kwargs: P.kwargs) -> R

fobfunction#

def fob(func: F, /, *funcs: *Fs) -> F | tuple[*Fs]
def fob[F: Callable](func: F) -> F
def fob[*Fs](*funcs: *Fs) -> tuple[*Fs]

Turn callables into FunctionObject yet keeps their original type hints.

@fob
def f(a: int) -> int:
return a + 1
f | 1
# 2
print, display, str = fob(print, display, str)

reveal_fobfunction#

def reveal_fob(func: Callable[[], R] | Callable[[T], R] | Callable[Concatenate[T, P], R] | Callable[P, R] | Callable, /) -> FunctionObjectA0[R] | FunctionObjectA1[T, R] | FunctionObjectA1P[T, P, R] | FunctionObjectA0P[P, R] | FunctionObject
def reveal_fob(func: Any) -> FunctionObject raise TypeError

Cast a FunctionObject to FunctionObject type.

Raises

TypeError
This function performs runtime check and raises TypeError if the input is not a FunctionObject.