0101 / pipetools

Functional plumbing for Python
https://0101.github.io/pipetools/
MIT License
203 stars 17 forks source link

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

Closed tallerasaf closed 2 years ago

tallerasaf commented 2 years ago

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 2 years ago

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

tallerasaf commented 2 years ago

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 2 years ago

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)
tallerasaf commented 2 years ago

@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:
 ```python
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')
tallerasaf commented 2 years ago

@0101 What do you think?

0101 commented 2 years ago

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