owned this note changed a year ago
Published Linked with GitHub

Agenda

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:

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

// 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:

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

yosh: The idea is that we can actually express this in a way that can be desugared today. Here's 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.


Select a repo