Receiver closures
A receiver closure is a closure whose first parameter is called self, so names inside it resolve against that receiver first, exactly as inside a method.
Not built natively yet. A closure that captures a var parameter or var self is not built by the native back
end yet, so torb run refuses the examples here that use it. torb check accepts them, and the rules are the
language's.
Inside a method, a bare name can mean a field or another method of self without writing self. in front of it. A
receiver closure gets the same treatment: a closure whose first parameter is
named self reads and writes that receiver the same way a method body does.
Example#
type Counter {
var value: Int = 0
var fn add(amount: Int) {
value = value + amount
}
}
fn build(configure: (var self: Counter) => Void): Counter {
var counter = Counter()
configure counter
counter
}
const counter = build {
add 3
add 4
}
print counter.valueSyntax#
(self: <Receiver>) => <Type> a receiver closure, read-only
(var self: <Receiver>) => <Type> a receiver closure that may change the receiverRules#
A closure whose function type names its first parameter
selfis a receiver closure.add 3inside the trailing closure above iscounter.add(3), reached the same way it would be inside a method ofCounter.Name resolution inside a closure or a method tries the local scope first, then the innermost receiver, then the module. A local binding always wins over a field of the same name, and a field of the receiver always wins over a top-level name of the module.
A position is not a name, so a receiver never makes one implicit:
.0and.1always stand behind a dot (entry.0,_.0,self.0), and a bare0is always the number. What a receiver makes implicit are the members of a type, and a tuple position is not one.Exactly one receiver is implicit at a time. A method has exactly one
self, and a receiver closure has exactly one receiver in scope this way - nesting one receiver closure inside another does not add the outer one to what a bare name can mean inside the inner one.Naming a receiver closure's parameter is what reaches it from inside a nested receiver closure, the way
server { s => s.database { url = "{s.host}/db" } }readss.hostfrom inside the nesteddatabaseblock. Naming the parameter makes it an ordinaryvarparameter, and a nested closure that only reads it and runs immediately - never escaping the call it was passed to - does not capture it past that call.type DatabaseConfig { var url: String = "" } type ServerConfig { var host: String = "localhost" var database: DatabaseConfig = DatabaseConfig() } fn server(configure: (var self: ServerConfig) => Void): ServerConfig { var config = ServerConfig() configure config config } const config = server { s => s.database { url = "{s.host}/db" } } print config.database.urlA closure that stores the named parameter instead of reading it immediately is still rejected, because storing it - assigning it into a field, the way
onStartbelow keeps a closure for later - is exactly the kind of outliving the call that rule 4's nested closure avoids by running at once.Does not compiletype ServerConfig { var host: String = "localhost" var onStart: () => Void = {} } fn server(configure: (var self: ServerConfig) => Void): ServerConfig { var config = ServerConfig() configure config config } const config = server { s => s.onStart = { print s.host } } // error: This closure captures the `var` parameter `s` and may outlive the callA field named after a keyword is written through
self, in a receiver closure as in a method. A bare name reaches the receiver's fields, but a bare keyword is always the keyword -type = "click"at the start of a line begins a declaration - so the one spelling for that field isself.type = "click", and the bare form is an error that says so (Lexical structure, rule 6).type Event { var type: String = "" var target: String = "" } fn event(configure: (var self: Event) => Void): Event { var built = Event() configure built built } const clicked = event { self.type = "click" target = "button" } print "{clicked.type} {clicked.target}"A receiver closure is an ordinary closure value, passed as an argument. The receiver type never declares it, so two functions that both take a
(var self: Counter) => Voidcan build completely different things out of the same receiver - what a receiver closure does is decided by the function it is passed to, not by the receiver type.
What this is not#
A receiver closure is not a method Receiver declares. Row above has no member that looks like this closure;
the closure exists only as a value at the call site of grid, and Row itself never changes because of it.
type Row {
var cells: List<Int> = []
var fn cell(value: Int) {
cells.append value
}
}
fn describe(build: (var self: Row) => Void): Row {
var row = Row()
build row
row
}
const row = describe { cell 1 }
print rowtype Row {
var cells: List<Int> = []
}
var row = Row()
row.build { cell 1 }
// error: `Row` has no member `build`Related#
- Parameter modes - a receiver closure as one of the shapes a parameter can take.
- Builders and DSLs - the function around a receiver closure that makes a configuration block work.
- Property commands - what a command call does when the name is a field, not a method.