pre-RFC: #[match] attribute on functions


#1

In a previous post I proposed a solution to the Drop trait for safe partial move. As I gain some supporters I decided to formalise it as two pre-RFCs. This is the first part that propose a new artibute for function items. The second one is comming soon and will talk about the Destruct trait.


Summary

This rfc introduce an attribute #[match] for function items which require the function to pattern match on its only argument.

#[match]
fn example(input: Result<String,()>) -> String {
    match input {
        Ok(s) => s,
        () => "",
    }
}

Motivation

The motivation to do this is to prevent the function code to access the input object, so the input object is garanteed to be destructed by the function, as a consequence, writing code to access the same object is not possible. This allows a futher step to make the existing Drop right and less magic. There will be a seperate RFC for it. The purpose of this RFC though, is to make the machemism generic and can be used in more contexts.

Guide-level explaination

The function annotated with #[match] can only have one argument, so the following is illegal:

#[match] 
fn example(i1: i32, i2: Result<i32,i32>); //Error: #[match] function have multiple arguments

The argument can be a pattern match:

#[match] 
fn example((i,s): (i32, String)) {...}

In that case, there is no restriction on the function body. However, if the argument is a single variable, the body must start with a pattern match on the argument:

#[match]
fn example1(r: Result<String,()>) {
    if r.is_ok() {...} //Error: #[match] function not start with a pattern match
}
fn example2(r: Result<String,()>) {
    match r { //Ok, start with a pattern match
        ...
    }
}
fn example3(r: Result<String,()>) {
    if let Ok(r) = r { //OK, if let is another kind of pattern match
       ...
    }
}
//The other form of pattern match should also count

fn example4(r: Result<String,()>) {
    match &r {...} //Error: #[match] function match on an expression, not the argument
}
fn example5(r: Result<String,()>) {
    if let Ok(ref r) = r { //Error: ref is not supported as we expect the argument to be moved
       ...
    }
}

The function body is free to add code after the pattern match. However, we require the match must do move/copy the fields, not using ref, as we expect the argument to be moved after the pattern match, so the original varialbe is at most a copy only.

This attribute can also apply to the trait defintion. In that case, it means: implementations must also have this attribute. However, implementations are free to add this attribute even it is a trait method that the propotype did not define with this attribute.

trait Trait1 {
    #[match] fn method1(r: Result<String,()>) -> Self;
    #[match] fn method2(&self);
    fn method3(&self);
}
impl Trait1 for () {
    #[match] fn method1(r: Result<String,()>) -> Self{
        ...
    }
    fn method2(&self) { //Error: missing #[match]
        ...
    }
    #[match] fn method3(&self) { //Ok, even it is not declared with #[match]
        ...
    }
}

Reference level explanation

This only adds some checks to be done in the HIR level.

Drawbacks

Increased complexity by introducing a new attribute, and require a new HIR check.

Rationale and alternatives

When defining Drop it is a challenge to do partial move. And right now, when moving things out from something implements Drop, we get E0509. To allow defining a better Drop, we need some special restriction on the method drop.

The plan to reform Drop is to seperate its duty into two pieces:

  1. Release external resources and do what ever the object contract required to do to finalize the main object
  2. Destruct the object into pieces and drop its child objects

The first part is to be done by the same Drop trait but it should not do any partial move. The second part will be a new trait (to be proposed in another RFC) that have a method takes the object by value. However, as such an object will not have a drop clue, we must restrict the access of this object to only allow pattern match. All the limitations of this attribute are derived from this requirement.

Restrict only the new trait to use the attribute

This alternative is good enough, as it seems not making much sense to allow this attribute in other context. However, it may make the new trait being too magical. And who know how people will use this feature for good?

Other names

Should the attribute be called #[destruct] or anything else?

Argument level annotation

This is not supported yet but there are proposals. If possible, we can annotate the argument to be pattern matched with, rather than the function, so we can have multiple argument functions. However, I didn’t see a clear benifit of doing this.

Prior Art

(TODO)

Unresulved questions

Not any yet.


pre-RFC: the Destruct trait
#2

I assume this must also deny a match that binds the entire input value in any way?

#[match]
fn foo(r: Result<String,()>) {
    match r {
        r => r.is_ok(),
    };
}
#[match]
fn foo(r: Result<String,()>) {
    match r {
        p @ Ok(_) => p.is_ok(),
        Err(()) => true,
    };
}

Or one that doesn’t bind any sub-fields and re-uses the argument binding:

#[match]
fn foo(r: Result<String,()>) {
    match r {
        Ok(_) => r.is_ok(),
        Err(()) => true,
    };
}

#3

Is there any reason you’d need to enforce this outside the new drop implementations?


#4

The only utility of this guarantee is allowing destructuring to “defuse” a Drop type.

Do we want to be able to do this outside of Drop(/Destroy) implementations? I’m unsure.

I fail to see how the destructure is required; rather, it can be framed as alerting the localized drop glue to only apply the transitive parts of the drop implementation.

Requiring the destructure of the Drop type, however, is one way of accomplishing that, and can be relaxed to just the drop glue formulation in the future.


#5

Maybe; or may not. If we consider those matches are “reconstructed” it should not be a problem.

In this case, I can propose two options: r.is_ok() will be called with an reconstructed object, or this is disabled.

This can fallback to the previous case: r can be considered a re-bind in the match, so it is reconstructed or disabled.

No. I didn’t see a use yet. And I have said in the RFC already.

So if we decided not to do this it should be fine. But the purpose is to make the features being decoupled so we can discuss them separately.

In the destruct method writer point of view, it would be good if I am required to add an attribute to indicate myself (and the code readers) I need to write the function in a specific form. And If I can have such an attribute here, if I were new to the language I must ask why I cannot have it there.

I would include this as an alternative (in the other RFC). I think it is fair to say that in one hand it have one less trait, in the other hand it requires more for that trait and no able to relax E0509.

In my proposal, drop method should not traverse the object structure, at all. All it should do is to apply what it should do in the main object level. The child objects will be dropped later, and will be out of its control.

Then we can look at “auto execute closures” (a future proposal):

trait FnDrop: Drop + Destruct + FnOnce() -> () {};

// use #[match] in front of a closure indicate the compiler to
// derive `Drop` + `Destruct`, and the `Destruct` method executes the closure
let finally = #[match] || { println!("Leave scope!") };

(Note, because the closure takes self as its first argument, #[match] is applied to its state and so the #[match] requirements are always fulfilled, but we require the function not to take any arguments, so we can derive Destruct and ignore the unit argument under rust-call.)

In the case above, the drop clue of the closure does not do anything. the destruct part does all the thing. This is because there are no external duty for it to do, and we are sure no user code can touch the closure state.


pre-RFC: the Destruct trait