Passing Values
Every parameter answers one question: does the caller still have this afterwards?
For a lot of values the answer is yes and there’s nothing to think about. For the rest, the code says which answer applies, at both ends. This page goes from the first kind to the second.
Small values are copied
Start with the easy half. Pass an i64 and the callee gets its own copy:
func charge_twice(amount: i64) -> i64 {
return amount * 2
}
// Small values (16 bytes or less, all-Copy fields) are copied, so the
// callee gets its own and nothing is marked at either end.
let fee = 5
let doubled = charge_twice(fee)
println("doubled to {doubled}, and fee is still {fee}")
No marker at either end, because nothing happened to fee that the caller needs to know about.
The line is 16 bytes or less, with every field itself copyable. That covers the integers, the
floats, bool, char, and small structs built out of those. It’s fixed and not configurable:
moving it would change what existing programs mean, so it’s a semantic boundary rather than a
tuning knob.
So for that whole family, ignore the rest of this page. Pass them around and stop worrying.
Everything else has one owner
Now the other half: a string, a Vec, a struct bigger than the line. Copying one of those means
copying whatever it owns on the heap, and Rask won’t do that behind your back — a copy that costs
an allocation is a copy you should be able to see.
So a value like that has exactly one owner at a time. One name is responsible for it, and when that name goes out of scope the value is released.
Which leaves two ways to hand it to a function:
- Lend it. The callee uses it for the length of the call. You still own it afterwards.
- Give it away. The callee owns it now. You don’t have it any more.
The next three sections are those two options — lending to read, lending to write, and giving.
Lend it to read: the default
// No marker: the callee reads, the caller keeps the value.
func describe(account: Account) -> string {
return "{account.owner}: {account.balance}"
}
No marker at either end, and the reason is that nothing happened. account is the same after the
call as it was before: same owner, same contents. There’s no cost to flag and no surprise to warn
about, so the syntax stays out of the way. This is what most parameters do.
The lend lasts exactly as long as the call. That’s what makes it safe without any annotation —
describe can’t stash the account somewhere and still be holding it after it returns, because
a reference can’t be stored.
A borrow isn’t yours to give away, either. 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 gave this up, 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.
Lend it to write: mutate
Same deal — the caller still owns it afterwards — except the callee writes through:
// `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, 50)
= 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 — the
copy rule decides what the callee gets, not whether you have to say so.
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.
Give it away: take
// `take`: the callee keeps it. The name dies at the call site.
func close_out(take account: Account) -> i64 {
return account.balance
}
Now the caller doesn’t have it. Reach for the name again and it’s 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 at the call site: the compiler will tell you
exactly where the value went, the moment you reach for it again. own account is available when
you want the call site to shout anyway.
You can’t take a small value
Back to the easy half for a second, because the two rules meet here. Write take on an i64 and
the compiler stops you:
error[E0404]: `take` on `i64` takes nothing — a Copy value is copied, not given
--> docs/book/errors/passing-values/take_on_copy.rk:6:24
|
6 | func charge_twice(take amount: i64) -> i64 {
| ^^^^^^ `amount` is a `i64`, and the caller keeps theirs
= fix: drop the keyword: `amount: i64`
= why: a mode marker is read at the call site to know what happens to the
argument, so one that says the wrong thing is worse than none
[mem.parameters/PM6c]
take is a promise to the caller that they lose the value, and an i64 is copied — they don’t.
A marker that says the wrong thing is worse than no marker, so the signature has to drop it.
That is about what you write. Handing a Copy value to a take parameter is still fine, and
common — it just happens through a generic, where the declaration couldn’t know:
// A Copy value handed to a generic `take` is copied too. `Vec.push` is
// declared `push(mutate self, take item: T)`, and `fee` survives both.
mut charges: Vec<i64> = []
charges.push(fee)
charges.push(fee * 2)
println("charged {charges[0]} and {charges[1]}, and fee is still {fee}")
Vec.push is push(mutate self, take item: T). At T = i64 the push copies, and fee is still
there for the next line.
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 lends, one write, 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
charged 5 and 10, and fee is still 5
Rules behind this page
- Parameter modes: the normative version
- Value semantics: the copy threshold
- Linearity: why a borrow can’t be consumed