owned this note
owned this note
Published
Linked with GitHub
---
title: wg-async meeting 2023-08-10
tags: minutes, wg-async
date: 2023-08-10
url: https://hackmd.io/LfWcsTSYTPK5TuORJ4l22g
---
# Agenda
* Additions to the Edition 2024 prelude https://github.com/rust-lang/wg-async/issues/310
* [#111347](https://github.com/rust-lang/rust/pull/111347) Future::map
# Attendance
Present: yosh, tmandry, TC
# `Future`/`IntoFuture` in the 2024 prelude
## Arguments for/against Future in the prelude
tmandry: Does anyone want to argue that Future should not be in the prelude? The reasons I can see are
* "Reifies the async effect", much like Iterator with iteration
* Comparable frequency of usage to Iterator (though generally lower)
* Removes a papercut to writing async code
## Arguments for/against IntoFuture in the prelude
yosh: This was only stabilized a year ago, so not as much usage. But if we think about "how things ought to be" I think we should, because `IntoIterator` is there.
tmandry: People often take `impl IntoIterator` for convenience, e.g. so you can pass a container. Don't see that happening with `IntoFuture`.
TC: If it's not in the prelude, someone could write a trait with an `into_future` method and not get conflicts until that person pulls `IntoFuture` into scope. I'd almost rather someone *does* get the conflict immediately. I want someone who is going to do that to do it very consciously.
yosh: Sounds like you're saying if we do this we shouldn't do it piecemeal.
tmandry: I guess I don't see any downsides of having it. Whereas if it's not there, there's potential for surprise due to inconsistency.
TC: Also, `Future` and `IntoFuture` are part of the language. If the prelude is about anything, it's about language items. Because of how it's wired into `async/await`, you can't write your own equivalent `Future` trait. This trait is part of the language; it's something more than just a helper in the standard library.
## Communicating
tmandry: I can file an issue in rust-lang/rust with our recommendation and ask T-libs-api to FCP.
# `Future::map`
## async vs sync
yosh:
```rust
trait Iterator {
fn map(self, f: FnMut()) -> Map { .. }
}
trait async Iterator {
async fn map(self, f: async FnMut()) -> Map { .. }
}
trait Future {
fn map() {}
async fn map() {} // or
}
```
yosh: map shouldn't carry the effect of the trait that it's on, i.e. Result::map shouldn't expect a Result, so Future::map shouldn't expect a Future to be returned.
TC: There's a type theoretic argument here. Having `map` take an `async fn` would violate the `Functor` contract (to use Haskell terminology) that is upheld by other uses of `map` in Rust. Let me find an example from my notes.... here:
(The important thing to focus on is the very first trait.)
```rust
// This is an experiment to write a Funtor, Applicative, Monad trait
// hierarchy using GATs using Rust-like names.
#![allow(clippy::manual_map)]
// functor trait
trait Map<T> {
type Wrapped<U>;
fn map<U, F>(self, f: F) -> Self::Wrapped<U>
where
F: FnMut(T) -> U;
}
// applicative trait
trait Apply<T>: Map<T> {
fn new(x: T) -> Self;
fn apply<U, F>(self, f: Self::Wrapped<F>) -> Self::Wrapped<U>
where
F: FnMut(T) -> U;
}
// monad trait
trait AndThen<T>: Apply<T> {
fn and_then<U, F>(self, f: F) -> Self::Wrapped<U>
where
F: FnMut(T) -> Self::Wrapped<U>;
}
// Option
impl<T> Map<T> for Option<T> {
type Wrapped<U> = Option<U>;
//#[refine]
fn map<U, F>(self, f: F) -> Self::Wrapped<U>
where
F: FnOnce(T) -> U,
{
match self {
Some(x) => Some(f(x)),
None => None,
}
}
}
impl<T> Apply<T> for Option<T> {
fn new(x: T) -> Option<T> {
Some(x)
}
//#[refine]
fn apply<U, F>(self, f: Option<F>) -> Self::Wrapped<U>
where
F: FnOnce(T) -> U,
{
match (f, self) {
(Some(f), Some(x)) => Some(f(x)),
_ => None,
}
}
}
impl<T> AndThen<T> for Option<T> {
//#[refine]
fn and_then<U, F>(self, f: F) -> Self::Wrapped<U>
where
F: FnOnce(T) -> Self::Wrapped<U>,
{
match self {
Some(x) => f(x),
None => None,
}
}
}
// Vec
impl<T> Map<T> for Vec<T> {
type Wrapped<U> = Vec<U>;
fn map<U, F>(self, mut f: F) -> Self::Wrapped<U>
where
F: FnMut(T) -> U,
{
let mut y = Vec::with_capacity(self.len());
for x in self.into_iter() {
y.push(f(x));
}
y
}
}
impl<T> Apply<T> for Vec<T> {
fn new(x: T) -> Self {
vec![x]
}
fn apply<U, F>(self, f: Self::Wrapped<F>) -> Self::Wrapped<U>
where
F: FnMut(T) -> U,
{
assert!(self.len() == f.len());
let mut y = Vec::with_capacity(self.len());
for (mut f, x) in f.into_iter().zip(self) {
y.push(f(x))
}
y
}
}
impl<T> AndThen<T> for Vec<T> {
fn and_then<U, F>(self, mut f: F) -> Self::Wrapped<U>
where
F: FnMut(T) -> Self::Wrapped<U>,
{
let mut y = Vec::new();
for x in self.into_iter() {
for x in f(x).into_iter() {
y.push(x);
}
}
y
}
}
```
TC: The `map` methods in std are defined in a such a way that they could be functors. We could later wrap these up in a trait as above in std (or in a crate in the ecosystem). We probably want to preserve that property.
yosh: Does effect polymorphism play nice with this...? For example:
```rust
// functor trait
trait Map<T> {
type Wrapped<U>;
fn map<U, F>(self, f: F) -> Self::Wrapped<U>
where
F: FnMut(T) -> U;
}
trait Iterator {
maybe_async fn map(self, f: FnMut()) -> { .. }
}
trait Iterator<const IS_ASYNC: bool = false> {
fn map(self, f: FnMut()) -> Map<IS_ASYNC> { .. }
}
```
(Continuing the tangent later...)
TC: So tell me about the effect generics work.
yosh: I'll be giving a talk about effect generics at RustConf.
yosh: We're doing work based on a paper, [Capabilities: Effects for Free](https://www.cs.cmu.edu/~aldrich/papers/effects-icfem2018.pdf).
yosh: The idea is that we can actually express this in a way that can be desugared today. [Here's](https://github.com/yoshuawuyts/maybe-async-channel/blob/main/tests/test.rs) an example of that. @oli and I have been working on this.
## `Future` (in stdlib) vs `FutureExt` (in ecosystem)
tmandry: I can see some downsides [ed: to adding methods to `Future` in the standard library], like adding entries to the vtable. It'd be nice if we had sealed methods [ed: because then static dispatch is guaranteed as so they do not affect the vtable].
yosh: The `Iterator::map` combinator is effectively sealed, because you can't construct a `Map`.
yosh: I'd prefer for us to use `async fn` for `map`. But that would remove this property and therefore we'd need sealed methods.
tmandry: Seems useful to be able to name it.
TC: yosh, are you proposing to not name it or to name it differently, e.g. with TAIT or ATPIT?
yosh: https://doc.rust-lang.org/core/future/index.html
yosh: If we asyncify most of std, most methods probably should not have named futures. But maybe methods on the `Future` trait should.
yosh: We don't have a mechanism to seal this. We want it to be sealed in the future, probably, right?
tmandry: If we didn't allow naming it, it could impact embedded users.
yosh: We should want to add concurrency extensions to `Future` as well.
tmandry: If we had a way to mark it is sealed, it wouldn't be a problem.
all: We should land it in nightly and have `Future::map` return a desugared and named `Future`, and we should do this on `Future` rather than on `FutureExt`. We should move toward stabilizing it.
## Conflicts with `FutureExt`
yosh: We need to either accept the breakage
tmandry: Yeah, we should block on not breaking existing code or making error messages worse.
TC: We could do this in the 2024 edition and treat the breakage with the ecosystem as part of the edition upgrade.
---