@invoke built-in Decorator for Immediately Invoked Function (IIF) Ability

TLDR;

There have been times when I needed to define functions and make them execute on-definition and not by calling later on. They’re called Immediately Invoked Functions or IIFs.

In Python, I satisfy it by doing the following trick and it works perfectly fine.

@lambda _:_()
def func():
    ...

I thought it would be a great idea to have a functools.invoke decorator that does the exact same thing.

@invoke
def func():
    ...

@invoke(1, 2, 3)
def func(a, b, c):
    return a+b+c

It’s also worth noting that not all languages have built-in support for IIFs, but they can often be emulated with similar constructs. Also, IIFs may not look quite handy in 99% of situations. I know it. :face_with_peeking_eye:

3 Likes

Why is this better than just writing a call to the function underneath it? It hardly seems like this saves any typing, nor is it clearer.

11 Likes

Hey Karl. Thanks for your response. Respectfully, I believe that it’s pretty clean and readable. The reason we don’t call the function is more about the convention. It indicates that the function should be called once it’s defined. So instead of calling the function right after its definition…

def func():
    ...
func()

You specify its behavior by making it an IIF marked with some decoration keyword like @invoke or any other “more-descriptive” decorator name.

Thanks for this.

Did not know this concept, but I like it.

My note scripts have bunch of commented out function calls and it doesn’t look nice. Having it just above signature is much cleaner.

However, I am -1 on this because your lambda solution is pretty sweet, I don’t think anything else is needed.

it is 1 line to define it near constants at the top of the module if want a nicer look:

invoke = lambda *a, **k: lambda _:_(*a, **k)
2 Likes

Actually, I always go with lambda as it’s a neat solution. Other than the need to define it in all the projects that you may need such functionality, your implementation is only supported on the functions with input parameters.

@invoke(1, 2, 3)  # works fine :)
def func(a, b, c):
    return a+b+c

But…

@invoke   # not working :(
def func():
    return "Hello there"

This case should be considered as well. That’s why the invoke decorator might have a quite complex structure, but handy at the same time.

1 Like

I also believe defining the invoke decorator in the form of a lambda function might make it hard to understand. What do you think?!

It takes as many lines as the import would take :slight_smile: With extra benefit of no import!

I never liked this behaviour of decorators, where it supports both with and without argument applications. To me it always seemed as unnecessary complexity.

Also, how do you make sure that it is called without arguements?

if len(args) == 1 and callable(args[0]) and not kwds:

What if my function has one argument, which is callable?

BTW, your lambda solution destroys the function. To remain it intact:

IIF = lambda *a, **k: lambda _:_ if _(*a, **k) else _
1 Like

It’s possible to check the decorated function’s signature inside the decorator. I’m not talking about inspect or other modules. It’s possible to enforce the decoration process to be keyword-only. That way, we could define a None-default parameter which is responsible for checking if the first positional argument is the function itself or not.

def decorator(func=None, *, foo=..., bar=...):
    if func is None:    # means -> `@decorator(...)` pattern was used
        ...
    else:               # means -> `@decorator` pattern was used
        ...

It’s just a pseudo code. It seems right though.

1 Like

I see, so it is a trade-off. Giving up positional arguments for the benefit not needing to type ().

Doesn’t seem like a good deal to me.

2 Likes

Pretty fair. I agree.

1 Like

The original message’s parameterless call is already satisfied by @operator.call (new in Python 3.11). No need for addition of functools.invoke().
I find this intuitive and use it for constants, e.g.

@operator.call
def fact10() -> int:
    f = 1
    for i in range(2, 11):
        f *= i
    return f

assert fact10 == 3_628_800

The situation with keyword parameters could be solved with @functools.partial(operator.call, **kwargs).
However, I find that non-intuitive and wouldn’t do it.

8 Likes

Like when? Why does the code need to be written as a function just to be executed once right away when you can simply execute the same code directly? Can you give a real-world example to illustrate when an IIF actually serves a purpose?

Note that as a decorator the function name isn’t yet assigned so it can’t even be useful for recursion.

2 Likes

I don’t have one (and don’t have time at the moment to dig) but I have definitely worked with code bases where the following pattern allows for encapsulation of an object that needs to be built once:

def _myconst_maker(...):
    ...

myconst = _myconst_maker(...)

Another use case would be quickly-generating sentinels or singletons:

@operator.call
class Undefined:
    def __repr__(self): return self.__class__.__name__

But as shown, this already exists, so I also don’t feel the need to go dig up real world examples. I do thank OP for prompting @Dutcho to show me something new!

3 Likes

It’s a simple form of namespace. However, when I’ve wanted this, I’ve wanted several different forms of it, including “call this now, but also leave the function for later”, and “replace this with the result of calling it”, which are incompatible. So on the occasions where I want this sort of thing, I generally write the decorator at the top of the program. (When I want it, I usually want it the same way for half a dozen functions in the same file.)

7 Likes

Ah I get it now. That does look handy sometimes. Thanks! Also @Rosuav

2 Likes

That’s it, @Dutcho! +1

In addition, it’s a pretty useful and common convention in Javascript. I preferably use IIFs in Python to define my web app’s main function. Also as @Rosuav mentioned, there are cases where you need to keep that executed function in the bag for later use.

Additionally, Python already supports Immediately Invoked Function Expressions (IIFE):

(lambda x, y: print(x + y))(3, 4)
1 Like

By function, I mean function, not Lambda Functions. Normally, simple functions tend to be more readable and extendable.

I want to emphasize that these are immediately invoked function expression (IIFE), not IIF.

In Python, the equivalent of the JavaScript function keyword used to define a function inside an expression is lambda. Using lambdas for IIFE can be a quick solution to avoid shadowing any global variable.

JavaScript is my first programming language, so I naturally used IIFE until I learned that this isn’t how Python works. Coincidentally, even in JavaScript, I always use function expressions for simple expressions. I appreciate Python for not allowing yet another way to define functions.

2 Likes