Skip to content

Function Object#

This module provides FunctionObject, a wrapper to extend functions with methods and operator overloads, and can be called just like normal Python functions. Yet they support numerous additional operators for function calling.

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

Usage#

Tip

Use FunctionObject in interactive environments.

FunctionObjects are particularly useful in interactive environments. Instead of wrapping a large expression in another level of parentheses, you can use application operators provided by FunctionObject to apply the function on the following expression.

f | although(we(are_not(using(lisp() or are_we()))))
do_not(want_to(see(these(parentheses())))) & 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 = func(_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 %>%.

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

Tip

Do not use FunctionObjects in library or main code base. Only use them in scripts, notebooks or REPLs. Function objects come with runtime costs. Albeit negligible most of the time, the cost could accumulate on critical paths.

FunctionObjects also have less static typing support. Do not use them in type-checked code.

Warning

Wrapping callables with FunctionObject will lose fields and methods of the original callable. Be cautious especially when wrapping other callable objects.

Warning

For performance considerations, the majority of apfel APIs are not wrapped in FunctionObjects.

Classes#

FunctionObject#

|#

def |[T, R](self, rhs: 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.

Example

@func
def f(x):
    return x * 2

f | 1 + 2
# 6

@#

def @[T, R](self, rhs: 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.

Example

@func
def f(x):
    return x * 2

f @ 1 + 2
# 4

&#

def &[T, R](self, lhs: 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.

Warning

np.array and array-like types are common overloaders of &, therefore this operator cannot be used with them.

Example

@func
def f(x):
    return x + 1

1 + 2 & f
# 6

**#

def **[T, R](self, rhs: 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.

Example

@func
def f(x):
    return x + 1

@func
def g(x):
    return x * 2

f ** g ** 1
# 3

%#

def %[R](self, rhs) -> 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.

Example

@func
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

Warning

This operator does not support the case where ... (the Ellipsis, not "...") is used as a keyword argument. However, this case is relatively rare, as ... cannot be declared as argument name.

Functions#

func#

def func[F: Callable](f: F) -> F
def func[*Fs](*fs: *Fs) -> tuple[*Fs]

Turn callables into FunctionObject yet keeps their original type hints.

Example

@func
def f(a: int) -> int:
    return f + 1

f | 1
# 2

print, display, str = func(print, display, str)

Note

As a callable, FunctionObject has less static typing support. @func erases type hints of FunctionObject while keeping the runtime type. If you want to retain the type hints, directly use FunctionObject's constructor, or use reveal_func on an object with runtime type FunctionObject.

reveal_func#

def reveal_func(func: Any) -> FunctionObject raise TypeError

Cast a FunctionObject to FunctionObject type.

Exception

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