# Fundamental Issue of Effects and In-place-init
I think I can finally put into words the fundamental issue that plagues all of our in-place-init proposals. At first it seemed that out-ptr does not have this issue, which made it preferable over init expressions. But I will now attempt to convince you that we will have this issue with any solution we end up with. To fully solve it, we require _effect generic_ functions and traits.
## The Basic Issue of Effects interacting with Init-Expressions
Let's observe the problem with init expressions first. Let's say we want to create an initializer for our custom type:
```rust
pub struct Points {
x: [i32; 1024 * 1024],
y: [i32; 1024 * 1024],
}
```
Since it is rather big, we don't want it to exist on the stack.
```rust
impl Points {
pub fn new() -> impl Init<Self> {
Self {
x <- [0; 1024 * 1024],
y <- [0; 1024 * 1024],
}
}
}
```
This looks very good so far. But what if we now want to add a new initializer that reads from the network to fill our points? We will use a `tokio::sync::mpsc` channel to represent this:
```rust
impl Points {
pub fn from_network(receiver: Receiver<(i32, i32)>) -> impl Init<Self> {
Self {
// what to write here?
// what if the receiver has no more elements?
}
}
}
```
It's pretty clear that doing this should return an asynchronous initializer. But `Init` doesn't support that effect natively. We don't run into this issue with outptr:
```rust
impl Points {
pub async fn from_network(
receiver: Receiver<(i32, i32)>,
out: &uninit Self,
) -> Option<&own Self> {
for i in 0..(1024 * 1024) {
let (x, y) = receiver.recv().await?;
out.x[i] = x;
out.y[i] = y;
}
Some(out)
}
}
```
Here the initializing function itself is `async` and potentially returns `None` when the receiver runs out of elements.
## Outptr have the same Issue
For this, we need to consider a _generic_ container type of a `T` that wants to be in-place initialized. One good example of this is the `Mutex<T>` from the linux kernel. It additionally requires pinning, since it is self-referential (and needs to already be initialized in a self-referential state!).
We directly start out with the outptr version:
```rust
pub struct Mutex<T> {
value: UnsafeCell<T>,
mutex: Opaque<bindings::mutex>,
}
impl<T> Mutex<T> {
pub fn new(
value: for<'a> impl FnOnce(&'a uninit T) -> &'a own T,
out: &uninit Self,
) -> &own Self {
out.value <- value(&uninit out.value);
out.mutex <- Opaque::ffi_init(&uninit out.mutex, |ptr| unsafe {
bindings::__mutex_init(ptr)
});
out
}
}
```
The problem with this definition is that it is _incompatible with effects_. So if I have an initializer for `T` that is fallible or asynchronous, then I can't use `Mutex::new`.
Outptrs allow us to write separate functions that support all of the different effects, but that will lead to combinatorial explosion when we have to provide a `try_new`, `try_new_async` etc.
## Taylor's Idea
I think Taylor already tried to address this exact issue in [our meeting](https://rust-lang.zulipchat.com/#narrow/channel/528918-t-lang.2Fin-place-init/topic/uninit-pointer.2C.20Nov.202025.20edition/with/558197637). The idea, if I understood it correctly, is to have a way to go from `&uninit Container<T>` to a variable number of `&uninit X` with the additional property that when all of those are initialized, the parent `&uninit Container<T>` is considered initialized.
This is one way to solve this issue, but to me it is unclear if we can even implement it in a nice way:
1. Is it possible to define such a function in the first place? The `Mutex` example requires initializing the `bindings::mutex` in addition to initializing the data, where and when would that take place? What if initializing the other fields fail, how would we "drop" it in that case (that would involve calling `bindings::__mutex_destroy`)?
2. How would one implement that function? How does the borrow checker grant the completeness condition? For `Mutex` we'd need some unsafe way of asserting that the `mutex` field is initialized?
3. Doing things this way seems to me like it will be ergonomically bad, since one always has to perform initialization _locally_. And it is impossible to refactor one common initialization pattern into a function. One would either have to use macros, or always destructure their data deeply (all nesting levels) and then initialize the leafs.
For these reasons, I think we should choose the next approach instead.
## Keyword Generics
I think I already have seen this idea somewhere, but I currently don't have the time to go digging. What if we give trait and function authors a builtin way to generalize over effects granted by keywords? So in our `Mutex` example, we could use outptrs and keyword generics to allow fallibility and asynchronousness:
```rust
impl<T> Mutex<T> {
pub ~async ~try fn new(
value: for<'a> impl ~async ~try FnOnce(&'a uninit T) -> &'a own T,
out: &uninit Self,
) -> &own Self {
out.value <- value(&uninit out.value).~await~?;
out.mutex <- Opaque::ffi_init(&uninit out.mutex, |ptr| unsafe {
bindings::__mutex_init(ptr)
});
out
}
}
```
The `~async` syntax means that we implement `new` for both cases of `async` and non-`async`. We need to specify a `~await` in the code to make it type-check in the `async` case.
Trait definitions also are allowed to have `~kw` in front of their definition. For example:
```rust
pub ~const ~async ~try trait FnOnce<Args: Tuple> {
type Output;
extern "rust-call" ~const ~async ~try fn call_once(
self,
args: Args,
) -> Self::Output;
}
```
This mechanizes the manual process of adding `AsyncFnOnce` traits with `async fn async_call_once`.
With this, we can also implement the `Mutex::new` function using init expressions!
```rust
impl<T> Mutex<T> {
pub ~async ~try fn new(
value: impl ~async ~try PinInit<T>,
) -> impl ~async ~try PinInit<Self> {
Self {
value <- value,
mutex <- Opaque::ffi_init(|ptr| unsafe {
bindings::__mutex_init(ptr)
}),
}
}
}
```
## Takeaways
Any in-place-init proposal will run into this issue. This essentially boils down to having a type that represents an initializer. I think we very much want and need to have such a type. Otherwise it will be impossible to write constructor functions.
In the outptr proposal this is any function taking a `&uninit` and returning a possibly wrapped `&own`. There is no single type that represents both async and non-async functions. Similarly for init expressions, we need separate traits for `async` and non-`async` initializers.
Keyword generics allows us in both proposals to express the type of all initializers with additional effects.