0101/pipetools

New Feature: Add the ability to pipe args and kwargs.

tallerasaf opened this issue · 6 comments

PR 'Add the ability to pipe args and kwargs.' -> #23

I added the functionality to pipe *args and **kwargs to a function.
And now you don't need to pipe a tuple with the first argument as a function and the second argument as a parameter
Now you can pass *args and **kwargs to a function using pipe '|'.
Now prepare_function_for_pipe knows how to handle keyword-only arguments.
And or knows how to handle next_func as *args and **kwargs to self.func.

    # Automatic partial with *args
    range_args: tuple[int, int, int] = (1, 20, 2)
    # Using pipe
    my_range: Callable = pipe | range | range_args
    # Using tuple
    my_range: Callable = pipe | (range, range_args)
    # list(my_range()) == [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]

    # Automatic partial with **kwargs
    dataclass_kwargs: dict[str, bool] = {'frozen': True, 'kw_only': True, 'slots': True}
    # Using pipe
    my_dataclass: Callable = pipe | dataclass | dataclass_kwargs
    # Using tuple
    my_dataclass: Callable = pipe | (dataclass, dataclass_kwargs)
    @my_dataclass
    class Bla:
        foo: int
        bar: str

    # Bla(5, 'bbb') -> Raises TypeError: takes 1 positional argument but 3 were given
    # Bla(foo=5, bar='bbb').foo == 5
0101 commented

Hey @tallerasaf! Can you please describe the new functionality and what use cases does it solve?

Hey @tallerasaf! Can you please describe the new functionality and what use cases does it solve?
I added the functionality to pipe *args and **kwargs to a function.
And now you don't need to pipe a tuple with the first argument as a function and the second argument as a parameter
Now you can pass *args and **kwargs to a function using pipe '|'.
Now prepare_function_for_pipe knows how to handle keyword-only arguments.
And or knows how to handle next_func as *args and **kwargs to self.func.

    # Automatic partial with *args
    range_args: tuple[int, int, int] = (1, 20, 2)
    # Using pipe
    my_range: Callable = pipe | range | range_args
    # Using tuple
    my_range: Callable = pipe | (range, range_args)
    # list(my_range()) == [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]

    # Automatic partial with **kwargs
    dataclass_kwargs: dict[str, bool] = {'frozen': True, 'kw_only': True, 'slots': True}
    # Using pipe
    my_dataclass: Callable = pipe | dataclass | dataclass_kwargs
    # Using tuple
    my_dataclass: Callable = pipe | (dataclass, dataclass_kwargs)
    @my_dataclass
    class Bla:
        foo: int
        bar: str

    # Bla(5, 'bbb') -> Raises TypeError: takes 1 positional argument but 3 were given
    # Bla(foo=5, bar='bbb').foo == 5
0101 commented

Sorry this just seems really confusing to me. You want the ability to pipe a callable into its own arguments? What problem does that solve?

It's confusing because if you just see a | b | c you wouldn't know what is a callable and what is arguments. Whereas with a tuple it's clearly separated. Also it seems backwards in terms of piping which should be just things flowing through a pipe.

So suddenly you'd have 2 ways to do partial application and | would mean 2 different things, which goes against simplicity and I don't really see any benefits.

What could be useful is ability to somehow nicely do partial application of kwargs. However how would you distinguish between applying keyword arguments and applying a single positional argument of a dictionary?

def func(a, b=1, c=None):
   ...

pipe | (func, {'a': "hello", 'b': 3, 'c': X})  # what happens now?

For now probably best we can do is:

from pipetools import pipe, xpartial as P

pipe | P(func, a="hello", b=3, c=X)

@0101

Sorry this just seems really confusing to me. You want the ability to pipe a callable into its own arguments? What problem does that solve?
-> I want the ability to pipe *args and **kwargs to a function as a partial function, it solved the problem of adding a new function that is a combination of several functions and functions with some of their arguments as a partial function in one line.
It's really useful for decorators.. many decorators accepts arguments(*args or **kwargs) so now you can chain multiple decorators to one decorator.
So I will be able to do:

from dataclasses import dataclass
from typing import Callable
from dataclasses_json import dataclass_json
from pipetools import pipe

dataclass_kwargs: dict[str, bool] = {'frozen': True, 'kw_only': True, 'slots': True}
my_dataclass: Callable = pipe | dataclass | dataclass_kwargs | dataclass_json
@my_dataclass
   class Bla:
       foo: int
       bar: str

Instead of:

from dataclasses import dataclass
from dataclasses_json import dataclass_json

@dataclass_json
@dataclass(frozen=True, kw_only=True, slots=True)
   class Bla:
       foo: int
       bar: str

It's confusing because if you just see a | b | c you wouldn't know what is a callable and what is arguments.
-> You can distinguish it by the variable name and by the variable type(with type hinting).


So suddenly you'd have 2 ways to do partial application and | would mean 2 different things, which goes against simplicity and I don't really see any benefits.
This is much more simpler:

my_dataclass: Callable = pipe | dataclass | dataclass_kwargs | dataclass_json

Then this one:

my_dataclass: Callable = pipe | (dataclass, dataclass_kwargs) | dataclass_json

What could be useful is ability to somehow nicely do partial application of kwargs. However how would you distinguish between applying keyword arguments and applying a single positional argument of a dictionary?
-> It will be a convention' because even if you want one variable to be a dictionary you can use the keyword argument, for example:

def func(my_dict: dict, number: int, text: str):
    pass
my_pipe = pipe | (func, {'my_dict': {'a': "hello", 'b': 3, 'c': X}, 'number': 5, 'text': 'bla'})
# Or
my_pipe = pipe | func | {'my_dict': {'a': "hello", 'b': 3, 'c': X}, 'number': 5, 'text': 'bla'}

If you want only a single keyword argument of a dictionary you can do:

def func(my_dict: dict):
    pass
my_pipe = pipe | (func, {'my_dict': {'a': "hello", 'b': 3, 'c': X}})
# Or
my_pipe = pipe | func | {'my_dict': {'a': "hello", 'b': 3, 'c': X}}

Or If you want only a single positional argument of a dictionary you can do:

def func(my_dict: dict):
    pass
my_pipe = pipe | (func, ({'a': "hello", 'b': 3, 'c': X}))
# Or
my_pipe = pipe | func | ({'a': "hello", 'b': 3, 'c': X})

So to sum up:
For keyword arguments you will pass a dictionary exactly like **kwargs:

def func(my_dict: dict, my_tuple: tuple, number: int, text: str):
    pass
my_pipe = pipe | func | {'my_dict': {'a': "hello", 'b': 3, 'c': X}, 'my_tuple': (1, ,2, 3), 'number': 5, 'text': 'bla'}

For positional arguments you will pass a tuple exactly like *args:

def func(my_dict: dict, my_tuple: tuple, number: int, text: str):
    pass
my_pipe = pipe | func | ({'a': "hello", 'b': 3, 'c': X}, (1, 2, 3) 5, 'bla')

@0101 What do you think?

0101 commented

@tallerasaf I'm just not seeing it. In both your examples I think the current case is the simpler one. And the kwargs looks like it would be complicated to get right, but mainly it would not be backwards compatible. And I don't want anyone to update and get weird errors. Sorry.