dgilland / pydash

The kitchen sink of Python utility libraries for doing "stuff" in a functional way. Based on the Lo-Dash Javascript library.

Home Page:http://pydash.readthedocs.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Embrace the tuple

DeviousStoat opened this issue · comments

Would you be open to PR that switches zip_, zip_with and to_pairs to return list of tuples instead?

Using a list in python is very similar to using a tuple but in static typing having lists is a bit annoying.
For example:

import pydash

d: dict[str, int] = {"key1": 1, "key2": 2}

pairs = pydash.to_pairs(d)  # this is list[list[str | int]]

first_key, first_value = pairs[0]  # first_key is `str | int` and first_value is `str | int`

This doesn't make much sense, we know first_key should be str and first_value should be int. If to_pairs returned list of tuples instead we could type it better and not have this problem.

Same with zip_:

import pydash

zipped = pydash.zip_([1, 2, 3], ["hello", "hello", "hello"])

zipped is a list of lists of str | int, we lost the order on the type level, int and str are blend in the lists.

And I think the hardest to work with might be zip_with

import pydash

def more_hello(s: str, time: int) -> str:
    return s.upper() * time

zipped = pydash.zip_with(
    ["hello", "hello", "hello"],
    [1, 2, 3],
    iteratee=more_hello  # type error here: cannot assign `str | int` to `str` and `str | int` to `int`
)

while this is valid at runtime, the type checker doesn't know that the first argument is str and second is int because it is all blend into the list[str | int]

This makes them really hard to use in a type checked context. Chaining functions with the result of these functions is very impractical. I think it would make a lot more sense for these functions to return tuples.

Makes sense to me! I'm all in favor.

Are there any other places where a type change like this would also make sense/make things easier?

Not really type changes but improvement ideas I had:

There is the sort_by min_by max_by functions that don't support accessing the key of a mapping in the iteratee. i think it would be cool if we could. I had this use case recently and I had to go out of the chain to use builtins min, max instead.

Also one thing that would be cool to have in the library is a maybe apply function kinda thing. pydash.get is really cool but I am a bit sad that it is not typable in the current python type system. One very common use case of it I believe is to get data from an optional value:

import pydash as _


class SomeClass:
    attribute: int = 5

    @classmethod
    def build(cls) -> "SomeClass | None":
        ...


some_class = SomeClass.build()

attr = _.get(some_class, "attribute")  # `attr` is `Any`, we cannot type `get` properly

But we could have a maybe_apply function thing that would take a callable:

attr = _.maybe_apply(some_class, lambda x: x.attribute)  # `attr` is `int | None`

this is typable.

And it is not restricted to attribute or key getting, we can just apply anything to an optional value, it abstracts this pattern:

def add1(x: int) -> int:
    return x + 1
    
some_int: Optional[int]
if some_int is not None:
    some_int = add1(some_int)
    
# instead just do
some_int = _.maybe_apply(some_int, add1)

And with the chaining interface I think it would look really cool, eg:

import pydash as _
from dataclasses import dataclass

@dataclass
class SomeAddress:
    city: str | None


@dataclass
class SomeUser:
    addr: SomeAddress | None


@dataclass
class SomeClass:
    user: SomeUser | None


some_class: SomeClass

maybe_upper_city: str | None = (
    _.chain(some_class)
    .maybe_apply(lambda x: x.user)
    .maybe_apply(lambda x: x.addr)
    .maybe_apply(lambda x: x.city)
    .maybe_apply(lambda x: x.upper())
)