# apfel.core.function_object

*module*

Extend functions with operator overloads for function calling.

Conceptually, [**function objects**](https://en.wikipedia.org/wiki/Function_object) 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`][apfel.core.function_object.fob] function to turn a function or a sequence of functions into `FunctionObject`s.

## Usage

> **TL;DR**
>
> Use and only use function objects in interactive environments.

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 [:python `icecream`](https://pypi.org/project/icecream/),
you can replace `ic @ ` with empty string to remove all debugging code,
without worrying of breaking the `get_pic()` call:

```python
from icecream import ic as _ic

ic = fob(_ic)
result = ic @ (
    get_pic(iris)
        .filter(...)
        .sort_values(...)
        .apply(...)
        .head(10)
)
```

[`|`][apfel.core.function_object.FunctionObject.__or__] and [`@`][apfel.core.function_object.FunctionObject.__matmul__] 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.

[`&`][apfel.core.function_object.FunctionObject.__rand__] 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 [:haskell `&`](https://hackage.haskell.org/package/base/docs/Data-Function.html#v:-38-), [:julia `|>`](https://docs.julialang.org/en/v1/manual/functions/#Function-composition-and-piping) or roughly [:rlang `%>%`](https://magrittr.tidyverse.org/reference/pipe.html).
With `&` you can write your code naturally from left to right.

```python
from icecream import ic as _ic
ic = fob(_ic)

result = (
    get_pic(iris)
        .filter(...)
        # ...
) & ic
```

[`**`][apfel.core.function_object.FunctionObject.__pow__] 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 [:haskell `$`](https://hackage.haskell.org/package/base/docs/Prelude.html#v:-36-).

[`%`][apfel.core.function_object.FunctionObject.__mod__] operator is used for calling multi-argument functions.
Check its documentation for more details.

> **Tip**
>
> Function objects come with runtime costs.
> Although negligible most of the time, the cost could accumulate on critical paths.
> 
> `FunctionObject`s also have less static typing support.
> Do not use them in type-checked code.

> **Warning**
>
> Wrapping callables other than functions with `FunctionObject` may lose attributes
> and methods of the original callable.

> **Warning**
>
> For performance considerations, `apfel` APIs are not wrapped in `FunctionObject`s.

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L1-L1)

## apfel.core.function_object.FunctionObject

*class*

```python
class FunctionObject
```

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L91-L241)

### apfel.core.function_object.FunctionObject.__call__

*attribute* · *instance attribute*

```python
__call__ = func
```

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L102-L102)

### apfel.core.function_object.FunctionObject.__doc__

*attribute* · *instance attribute*

```python
__doc__ = func.__doc__
```

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L99-L99)

### apfel.core.function_object.FunctionObject.__name__

*attribute* · *instance attribute*

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

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L100-L100)

### apfel.core.function_object.FunctionObject.__qualname__

*attribute* · *instance attribute*

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

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L101-L101)

### apfel.core.function_object.FunctionObject.__slots__

*attribute* · *class attribute* · *instance attribute*

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

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L96-L96)

### apfel.core.function_object.FunctionObject.__wrapped__

*attribute* · *instance attribute*

```python
__wrapped__ = func
```

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L103-L103)

### apfel.core.function_object.FunctionObject.__init__

*method*

```python
def __init__(func)
```

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L98-L103)

### apfel.core.function_object.FunctionObject.__matmul__

*method*

```python
def __matmul__(value)
```

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

Function application operator `@` for `FunctionObject`s.

`f @ x` is equivalent to `f(x)`. This operator behaves the same as [`|`](#operator-or), but with a different precedence.

```python
@fob
def f(x):
    return x * 2

f @ 1 + 2
# 4
```

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L156-L175)

### apfel.core.function_object.FunctionObject.__mod__

*method*

```python
def __mod__(value)
```

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

Function application operator `%` for `FunctionObject`s of multi-argument functions.

- If the right hand side is a [:python `Sequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.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 [:python `Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.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.

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

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

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L205-L241)

### apfel.core.function_object.FunctionObject.__or__

*method*

```python
def __or__(value, /)
```

```python
def |[T, R](self, value: T) -> R
```

Function application operator `|` for `FunctionObject`s.

`f | x` is equivalent to `f(x)`. This operator behaves the same as [`@`](#operator-at), but with a different precedence.

```python
@fob
def f(x):
    return x * 2

f | 1 + 2
# 6
```

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L111-L130)

### apfel.core.function_object.FunctionObject.__pow__

*method*

```python
def __pow__(value)
```

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

Function application operator `**` for `FunctionObject`s.

`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 [:haskell `$`](https://hackage.haskell.org/package/base/docs/Prelude.html#v:-36-) operator, except the precedence.

```python
@fob
def f(x):
    return x + 1

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

f ** g ** 1
# 3
```

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L177-L203)

### apfel.core.function_object.FunctionObject.__rand__

*method*

```python
def __rand__(value)
```

```python
def &[T, R](self, value: T) -> R
```
Reverse function application operator `&` for `FunctionObject`s.
**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.

```python
@fob
def f(x):
    return x + 1

1 + 2 & f
# 4
```

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L132-L154)

### apfel.core.function_object.FunctionObject.__repr__

*method*

```python
def __repr__()
```

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L108-L109)

### apfel.core.function_object.FunctionObject.__str__

*method*

```python
def __str__()
```

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L105-L106)

## apfel.core.function_object.FunctionObjectA0

*class*

```python
class FunctionObjectA0(FunctionObject)
```

Bases: `apfel.core.function_object.FunctionObject`

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L17-L24)

### apfel.core.function_object.FunctionObjectA0.__call__

*method*

```python
def __call__() -> R
```

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L18-L18)

## apfel.core.function_object.FunctionObjectA0P

*class*

```python
class FunctionObjectA0P(FunctionObject)
```

Bases: `apfel.core.function_object.FunctionObject`

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L48-L53)

### apfel.core.function_object.FunctionObjectA0P.__call__

*method*

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

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L49-L49)

## apfel.core.function_object.FunctionObjectA1

*class*

```python
class FunctionObjectA1(FunctionObject)
```

Bases: `apfel.core.function_object.FunctionObject`

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L26-L37)

### apfel.core.function_object.FunctionObjectA1.__call__

*method*

```python
def __call__(value: T) -> R
```

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L27-L27)

### apfel.core.function_object.FunctionObjectA1.__matmul__

*method*

```python
def __matmul__(value: T) -> R
```

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L30-L30)

### apfel.core.function_object.FunctionObjectA1.__or__

*method*

```python
def __or__(value: T) -> R
```

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L28-L28)

### apfel.core.function_object.FunctionObjectA1.__pow__

*method*

```python
def __pow__(value: T) -> R
```

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L31-L31)

### apfel.core.function_object.FunctionObjectA1.__rand__

*method*

```python
def __rand__(value: T) -> R
```

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L29-L29)

## apfel.core.function_object.FunctionObjectA1P

*class*

```python
class FunctionObjectA1P(FunctionObject)
```

Bases: `apfel.core.function_object.FunctionObject`

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L39-L46)

### apfel.core.function_object.FunctionObjectA1P.__call__

*method*

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

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L40-L40)

## apfel.core.function_object.fob

*function*

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

```python
def fob[F: Callable](func: F) -> F
def fob[*Fs](*funcs: *Fs) -> tuple[*Fs]
```

Turn callables into `FunctionObject` yet keeps their original type hints.

```python
@fob
def f(a: int) -> int:
    return a + 1

f | 1
# 2

print, display, str = fob(print, display, str)
```

> **Note**
>
> As a callable, `FunctionObject` has less static typing support.
> `@fob` 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_fob` on an object with runtime type `FunctionObject`.

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L243-L269)

## apfel.core.function_object.reveal_fob

*function*

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

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

[View source](https://github.com/Yixuan-Wang/apfel/blob/main/src/apfel/core/function_object.py#L272-L287)
