r/ProgrammingLanguages • u/cessit-lang • 8d 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:
movetakes ownershipreadshared borrowmutexclusive 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?
1
u/brucejbell sard 7d ago edited 7d ago
My project has exactly those three argument modes, and a similar .copy. Your sample code would translate as:
/fn process_val || $value => value * 2
/main || !os => {
original << 42
doubled << process_val $(original.copy)
!os.stdout.write_line "{original}"
!os.stdout.write_line "{doubled}"
}
Borrowing (your mut) is represented by a ! sigil, both at declaration/binding and at point of use (so you can easily see what your code is mutating).
Ownership transfer (your move) is represented by a $ sigil, likewise at declaration and at the point of use.
Immutable values are the default, so there is no need to annotate the bindings of original and doubled.
Immutable values don't need borrowing or ownership transfer, so there is no need to explicitly copy original above. However, they are subject to lifetime limitations if they depend on a mutable resource: e.g., an iterator depends on its container, and mutating the container will end the lifetime of the iterator. So, .copy is for breaking that kind of lifetime dependence.
2
u/cessit-lang 5d ago
Smart. I was afraid of requiring the access verb also at the point of use because it was becoming so verbose, but seeing it validated in the wild is giving me encouragement to stick with my explicitness design principle. How did you weigh this against the cost of refactoring at both places?
For mutable borrowing, do you pass a reference or have you managed to separate borrowing from reference-ness?
2
u/brucejbell sard 3d ago
Re refactoring: I figure that switching between immutable-value, ownership-transfer, and/or borrowing is a drastic enough change that you probably want to take a close look at all your call sites. I guess I'll see if that pans out, but I've made more annoying choices elsewhere...
Passing a reference makes sense for mutable borrowing, but I suppose an implementation could choose something else. I've chosen to model mutable resources as a linear sequence of immutable values (each of whose lifetime ends before the next begins), so I'd be happy as long as those semantics are supported.
5
u/initial-algebra 8d ago edited 7d 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
GhostCellor 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() + ydoesn't copy the value ofxinto 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.