Present: yosh, tmandry, TC
Future
/IntoFuture
in the 2024 preludetmandry: Does anyone want to argue that Future should not be in the prelude? The reasons I can see are
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.
tmandry: I can file an issue in rust-lang/rust with our recommendation and ask T-libs-api to FCP.
Future::map
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.
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.