Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow pipe into await #727

Closed
evhub opened this issue Apr 14, 2023 · 12 comments
Closed

Allow pipe into await #727

evhub opened this issue Apr 14, 2023 · 12 comments

Comments

@evhub
Copy link
Owner

evhub commented Apr 14, 2023

Adding fmap support for awaitables would allow writing

await (
    get_data()
    |> async_func
    |> fmap$(non_async_func))
)

instead of having to write

(await async_func(get_data()) |> non_async_func
@evhub evhub added the feature label Apr 14, 2023
@evhub evhub added this to the v3.0.0 milestone Apr 14, 2023
@evhub
Copy link
Owner Author

evhub commented Apr 14, 2023

Notably this is easily definable manually as

async def acall(f, awaitable) = f(await awaitable)

and adding this to fmap might be a bit confusing as it makes fmap work on a non-iterable and non-asynchronous-iterable object, which is currently never works on.

We could alternatively add acall as a new built-in.

@evhub
Copy link
Owner Author

evhub commented Apr 14, 2023

More general acall:

async def acall(f, /, *args, **kwargs) =
    f(*(await x for x in args), **{k: await v for k, v in kwargs.items()})

Alternatively, we could also do this more like lift:

data alift(f):
    async def __call__(self, *args, **kwargs) =
        self.f(*(await x for x in args), **{k: await v for k, v in kwargs.items()})

@evhub
Copy link
Owner Author

evhub commented Apr 14, 2023

New proposed built-in:

async def async_bind(f, /, *args, **kwargs):
    out = f(*(await x for x in args), **{k: await v for k, v in kwargs.items()})
    if hasattr(got, "__await__"):
        out = await out
    return out

Maybe we should also allow inputs to be non-awaitables too?

@evhub evhub changed the title Allow fmap over an awaitable Add async_bind built-in Apr 14, 2023
@evhub
Copy link
Owner Author

evhub commented Apr 14, 2023

Here's another alternative here: an |await> pipe. Would let you just do

(
    get_data()
    |> async_func
    |await> non_async_func
)

Some documentation:

(|await>)    => await pipe forward
(|await*>)   => await multi-arg pipe forward
(|await**>)  => await keyword arg pipe forward
(<await|)    => await pipe backward
(<*await|)   => await multi-arg pipe backward
(<**await|)  => await keyword arg pipe backward
(|await?>)   => await None-aware pipe forward
(|await?*>)  => await None-aware multi-arg pipe forward
(|await?**>) => await None-aware keyword arg pipe forward
(<?await|)   => await None-aware pipe backward
(<*?await|)  => await None-aware multi-arg pipe backward
(<**?await|) => await None-aware keyword arg pipe backward

The await pipe operators simply await whatever is piped into them (which means all await pipes must be passed a single awaitable), then behave like the equivalent non-await pipe operator on the result of the await. For dealing with asynchronous iterators rather than single awaitables, see fmap.

@evhub evhub changed the title Add async_bind built-in Add await pipes Apr 14, 2023
@evhub
Copy link
Owner Author

evhub commented Apr 15, 2023

The problem with the await pipe operators is that, unlike all other pipe operators, operator functions and composition pipes would not be possible (unless they have a different type signature such that they return an awaitable, which would be very confusing).

@evhub
Copy link
Owner Author

evhub commented Apr 15, 2023

Maybe we should just add some sort of special syntax that makes it clear that this really only works in pipes? Something like:

(
    get_data()
    |> async_func
    |> await
    |> non_async_func
)

Problem with this syntax, though, is that await is a valid Python variable name. Nevermind, it looks like this was removed in 3.7, so we could do this.

Also, notably, what neither this nor the pipe syntax do, but the built-ins do accomplish, is let you work with awaitables in non-async contexts.

@yggdr
Copy link

yggdr commented Apr 15, 2023

Nice timing, I wanted to start a discussion about async pipes as well because I recently found an old implementation of mine, back from 1.x times :)

Now what surprises me a little is the semantics you seem to want for a |await> b. You seem to want that to await the part being passed into (a), I would've assumed it awaits b(a) with my reasoning and original motivation being that I wanted an async-version of val |>= some_func. Would your version of val |await>= some_func compile to val = some_func(await val)? I don't think that's a pattern I've seen used. O.t.o.h. val = await some_func(val) I have used a few times.

How would you want a starpipe to work? Does

a |**await> b

compile to

b(**(await a))

(which is how I read your proposal) or

await b(**a)

? I would expect/want the second version.

What would most people expect or want this

a |> b |await> c |> d

to compile to? I can see why people might see the await in there as sort of a marker, like "do the pipe thing until here, then await the result, then continue the piping with the result from the awaiting". I would however argue that that functionality (if desired) should go to a separate syntax, like the a |> await |> b you suggested, now that await is no longer a valid name in python. This would basically make await a function within pipelines.

edit
Also I'm not sure what you mean with your last sentence about awaitables in non-async context. Can you give an example?

@evhub
Copy link
Owner Author

evhub commented Apr 16, 2023

Yeah, the |await> pipe is probably too confusing. I think letting you pipe into await is probably what I'll go with here.

@evhub evhub changed the title Add await pipes Allow pipe into await Apr 16, 2023
@evhub evhub added the resolved label Apr 16, 2023
evhub added a commit that referenced this issue Apr 16, 2023
@evhub
Copy link
Owner Author

evhub commented Apr 16, 2023

The pipe into await version is now live on coconut-develop>=3.0.0-a_dev24.

@evhub evhub closed this as completed Apr 16, 2023
@yggdr
Copy link

yggdr commented Apr 16, 2023

I like the |> await syntax 👍

(edit b/c of accidental send)
But unfortunately I can't have my original wish of an async version of val |>= func. I can make my own async pipe operator of course, but there doesn't seem to be a functionality to make an in-place version of it.
Is the discussion of an |await> pipe that would come with an |await>= version closed or would you still be open to it?

@yggdr
Copy link

yggdr commented Apr 16, 2023

After a bit of testing I just realised that operator |await> can't work, because no matter how I try to define it, there will always be an outer await missing. I guess some sort of macro system would be needed to make this work?

@evhub
Copy link
Owner Author

evhub commented Apr 16, 2023

What sort of use case are you imagining here that you would want |await> for?

@evhub evhub mentioned this issue May 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants