Passing Values
Every parameter answers one question: does the caller still have this afterwards? There are three answers, and the code says which one at both ends.
Borrow: the default
// No marker: the callee reads, the caller keeps the value.
func describe(account: Account) -> string {
return "{account.owner}: {account.balance}"
}
No marker, no ceremony. The callee reads; the caller keeps the value and carries on using it. This is most parameters in most programs, which is why it’s the default.
A borrow isn’t yours to give away. Try to pass it on to something that takes ownership and the compiler stops you:
error[E0835]: cannot give away `account` — it's borrowed, not owned
--> docs/book/errors/passing-values/consume_borrowed.rk:15:22
|
14 | func describe(account: Account) -> i64 {
| ------- `account` is declared as a borrowed parameter
15 | return close_out(account)
| ^^^^^^^ `close_out` takes ownership, and `account` isn't yours to give
= fix: take it: `take account: …` in the signature — then the caller can see it goes
= why: the caller keeps a parameter it didn't mark `take` and goes on using it, so consuming it here would leave them holding something that's gone. For a `@resource` that's a second close of a real handle. [mem.parameters/PM1, mem.linear/L1]
The caller never marked this as given, so they’re still using it. Consuming it here would leave them holding something that’s gone. For a file handle or a transaction, that’s a second close of a real resource.
Mutate: write through, caller keeps it
// `mutate`: the callee writes through, the caller still owns it afterwards.
func deposit(mutate account: Account, amount: i64) {
account.balance += amount
}
The call site marks it too, and that’s not optional:
deposit(mutate account, 50) // mutate: marker required
Leave the marker off and you get the one-token fix, plus the reason:
error[E0373]: `deposit` mutates `account` — mark it at the call site
--> docs/book/errors/passing-values/missing_marker.rk:16:13
|
16 | deposit(account, 50)
| ^^^^^^^ passed to the `mutate account` parameter
= fix: deposit(mutate account, …)
= why: the compiler backstops a misread *move* — using a value after it's moved is an error — but nothing backstops a misread mutation: both readings are legal code, so the one that can't be caught gets written down. The marker follows the signature, not the argument's size, so a Copy argument writes it too. A method receiver is exempt — `player.take_damage(10)` operates on the receiver by construction [mem.parameters/PM4, PM5]
Here’s the thinking behind that. If you misread a move, the compiler catches it for you: use the value again and you get an error naming where it went. If you misread a mutation, nothing catches it. Both readings are legal code, and the value looks the same afterwards, just different. So the case nobody can catch for you is the one you write down.
Because the rule is syntactic it has no exceptions to memorise: the marker is required exactly when
the parameter says mutate, whatever the argument’s type or size. An i64 writes it too.
One thing catches everyone once. let is deep:
error[E0302]: cannot mutate `account` — declared `let`
--> docs/book/errors/passing-values/let_as_mutate.rk:16:20
|
16 | deposit(mutate account, 50)
| ^^^^^^^ `account` is a let binding — immutable
= fix: replace `let account` with `mut account`
= why: `let` bindings forbid rebinding and mutation. Use `mut` when you need to modify the value or call mutating methods.
let doesn’t mean “this name won’t be reassigned.” It means nothing changes through this name,
including through a mutating method, an index, or a field assignment.
Take: the callee keeps it
// `take`: the callee keeps it. The name dies at the call site.
func close_out(take account: Account) -> i64 {
return account.balance
}
No marker needed, though own account is available when you want the call site to shout. After the
call the name is gone:
error[E0800]: use of moved value: `account`
--> docs/book/errors/passing-values/use_after_take.rk:17:19
|
16 | let n = close_out(account)
| ------- value moved here
17 | println("{n} {account.balance}")
| ^^^^^^^ value used here after move
= note: `Account` is 24 bytes (copy threshold is 16) — assignment moves instead of copying
= help: add `account.clone()` if you need an independent copy
That note is the whole reason take needs no marker: the compiler will tell you exactly where the
value went, the moment you reach for it again.
Receivers are never marked
extend Account {
// A receiver is never marked at the call site, mutating or not.
func charge_fee(mutate self, fee: i64) {
self.balance -= fee
}
}
account.charge_fee(5) // receiver: never marked
charge_fee takes mutate self and the call site still says nothing. The receiver is the thing
being operated on, which is what the dot means. Marking it would put noise on every mutating method
in the language.
Putting it together
mut account = Account { owner: "Ada", balance: 100 }
println(describe(account)) // borrow: no marker
deposit(mutate account, 50) // mutate: marker required
account.charge_fee(5) // receiver: never marked
println(describe(account))
let final = close_out(account) // take: account is gone after this
println("closed with {final}")
Read the markers and you know the shape of that block without opening a single signature: two borrows, one mutation, one method on the receiver, one hand-off. That’s what making them visible buys. Running it prints:
Ada: 100
Ada: 145
closed with 145
doubled to 10, and fee is still 5
Small values are copied, not given
func charge_twice(take amount: i64) -> i64 {
return amount * 2
}
// Small values (16 bytes or less, all-Copy fields) copy instead of moving,
// so handing one to `take` leaves the caller's copy alone.
let fee = 5
let doubled = charge_twice(fee)
println("doubled to {doubled}, and fee is still {fee}")
take means “I’m keeping this”, but you can’t take away what the caller never gave up. Values of
16 bytes or less whose fields are all Copy get copied on the way in, so fee is untouched. Past 16
bytes it’s a real move and the name dies, which is what the Account error above shows, sizes and
all.
The threshold is fixed at 16 bytes and isn’t configurable. Moving it would change what existing programs mean, so it’s a semantic boundary rather than a tuning knob.
Rules behind this page
- Parameter modes: the normative version
- Value semantics: the copy threshold
- Linearity: why a borrow can’t be consumed