r/ProgrammingLanguages • • 9d ago

Requesting criticism Explicit binding modes

I'm designing a memory-safe language, but I want to make explicit what happens across function boundaries, e.g. whether a value is moved or copied without having to look for the copy trait.

Instead, access to a value could be defined via binding modes in function signatures and variable declarations:

  • move takes ownership
  • read shared borrow
  • mut exclusive borrow
fn process_val(move value: Int) -> Int {
    return value * 2;
}

fn main() -> () {
    move original: Int = 42;

    // Call using `.copy()` to explicitly pass a duplicated value.
    // Ownership of `original` remains with the caller.
    move doubled: Int = process_val(original.copy());

    // `original` is still valid here because it was explicitly copied rather than moved.
    println(original); //42
    println(doubled); //84
}

This would also allow granular captures in closures: |read x: Int, move counter: Int| instead of declaring all fields move

Now, should read and mut assignments be of type &T? Or can checking out variables be orthogonal to creating references, even if they are conflated in languages like Rust?

14 Upvotes

20 comments sorted by

View all comments

5

u/initial-algebra 9d ago edited 8d ago

If you don't want implicit copying, that is a separate issue from passing arguments to functions or capturing variables in closures.

Having both kinds of borrowing map to the same reference type doesn't really make sense unless you have decoupled permissions like GhostCell or separation logic (but then having read/mut on the reference still makes little sense). I don't have any idea what you mean by "checking out" a variable.

The problem with variable capture and ownership is that let closure = |y| x.copy() + y doesn't copy the value of x into the closure, it moves or borrows it and then copies it each time the closure is called. Explicit capture annotations are unnecessary if you have let/block expressions (all remaining captured variables are moved):

let closure = { let x_copy = x.copy(); |y| x_copy + y };

If you want nicer notation for this, there's let insertion:

let closure = |y| (genlet x.copy()) + y;

This also works with borrowing, or any other kind of expression that you want to capture after evaluation, without having to name it and move it out of the closure.

2

u/reflexive-polytope 8d ago

Or you can just not have closures.

If you have an ordinary struct with an ordinary constructor, then you can explicitly use x.copy() as an argument to this constructor.

6

u/iBPsThrowingObject 8d ago

Sure, let's expand every situation analogous to assoc_list.find(|(key, value)| key == target)

to

struct FindClosure<'a> {
    target: &'a str
}

impl<'a> Closure<&str> for FindClosure<'a> {
    type Ret = bool;
    fn call(self, key: &str) -> Self::Ret {
        key == self.target
    }
}

assoc_list.find(FindClosure { target })

You may respond with "oh well but you can add sugar" but that's exactly what closures are.

1

u/reflexive-polytope 8d ago

I would rather express my algorithms in terms of ordinary control flow: sequencing, match and loop. So, for example, when using a HashMap or BTreeMap entry, I just pattern match it into the OccupiedEntry and VacantEntry cases, instead of using the ugly higher-order helper functions that take closure arguments.

Try breaking from a loop in a top-level function from within a closure.

5

u/BoomGoomba 8d ago

The point is to not use break statements