Methods and impl blocks
Method call syntax and concrete or generic impl blocks in Prismio -- x.f(a) is f(x, a), and an impl block is where the free function is written.
Last verified
Prismio 0.1 has method call syntax and impl blocks. It does not have dynamic
dispatch, and the two facts are connected: a method call is rewritten into an
ordinary call before overload resolution runs, so there is only one dispatch
mechanism in the language and it is the one overloads already use.
x.f(a) is f(x, a)
A method call is a spelling, not a lookup. The receiver becomes argument zero and everything after it shifts right.
import std.io
struct Point {
x: Int,
y: Int
}
fn shifted(point: Point, dx: Int, dy: Int) -> Point {
return Point { x: point.x + dx, y: point.y + dy }
}
fn main() -> Int {
let origin = Point { x: 0, y: 0 }
// The same call, twice.
let a = shifted(origin, 3, 4)
let b = origin.shifted(3, 4)
println(a.x + b.y)
return 0
}Because the rewrite happens before overload resolution, a method call reaches every overload an ordinary call reaches, and nothing else. There is no separate method namespace to search and no receiver type to look a table up in.
The receiver may be any expression, including a literal:
import std.io
import std.string
fn main() -> Int {
println(" padded ".trim())
println("a,b,c".indexOf("b"))
return 0
}Properties
A property is a function you read like a field: s.length, '7'.isDigit,
process.pid. It is declared with prop where a method has fn, and it is
written without parentheses — s.length() is an error, just as s.trim is.
The spelling tells a reader which one they are looking at.
import std.io
import std.string
struct Rect { w: Int, h: Int }
impl Rect {
prop area(self) -> Int { return self.w * self.h }
fn scaled(self, by: Int) -> Rect { return Rect { w: self.w * by, h: self.h * by } }
}
fn main() -> Int {
let text = "prismio"
let r = Rect { w: 3, h: 4 }
println(text.length) // a property
println(text.trim()) // a method
println(r.scaled(2).area) // 48
return 0
}Calling one as the other is a compile error that names the fix:
import std.io
import std.string
fn main() -> Int {
let text = " padded "
println(text.length()) // `length` is a property, not a method
println(text.trim) // `trim` is a method, not a property
return 0
}A property takes only its receiver, returns a value, and does not allocate.
All three are checked where it is declared. The last one is the reason the two
spellings are kept apart: Prismio asks you to bind an owned result (let t = text.trim()), and a field-like name that hid an allocation would hide that
obligation with it. Parentheses mean "this may allocate".
prop is only a keyword directly before a name in a declaration, so a variable
may still be called prop. A property is still a function underneath, and
length(text) calls it like one. A struct field always wins over a property of
the same name.
The standard library's properties are the reads that describe a value rather
than compute a new one: length, isEmpty, first and last on String and
Vec; isDigit, isSpace and the other Char classifications; isEven, isNan
and the other number predicates; process.pid, process.args.count and
platform.current. Everything that converts, computes or acts — toUpper(),
sqrt(), parseInt(), wait() — is a method.
impl blocks
An impl block is where a method is written. It names the receiver's type once
so the methods inside need not repeat it.
import std.io
struct Point {
x: Int,
y: Int
}
impl Point {
fn sum(self) -> Int {
return self.x + self.y
}
fn shifted(self, dx: Int, dy: Int) -> Point {
return Point { x: self.x + dx, y: self.y + dy }
}
}
fn main() -> Int {
let point = Point { x: 1, y: 2 }
println(point.shifted(10, 20).sum())
return 0
}Each method is lowered to a plain top-level function whose first parameter is the receiver, so the two spellings below declare the same thing:
impl Point { fn sum(self) -> Int { ... } }
fn sum(self: Point) -> Int { ... }That is the whole of what impl does. In particular:
- A method is callable by name without a receiver --
sum(point)works. - Two
implblocks for the same type are allowed, and so are methods of the same name on different types; they resolve as overloads. - A function written inside an
implblock with noselfparameter belongs to the type. It is calledPoint.origin(), notorigin()— see type-level functions.
Type-level functions
A function in an impl block that takes no self builds or answers something about the type rather than a value of it. It is named for the type, so two types may each have a new or a default without colliding:
import std.string
struct Config {
retries: Int,
name: String
}
impl Config {
fn default() -> Config {
return Config { retries: 3, name: "app" }
}
fn named(name: String) -> Config {
let mut c = Self.default()
c.name = name
return c
}
}
struct Box<T> { value: T }
impl<T> Box<T> {
fn new(value: T) -> Box<T> { return Box<T> { value: value } }
}
fn main() -> Int {
let web = Config.named("web")
let n = Box.new(5) // T inferred from the argument
let s = Box<String>.new("x") // or written
return web.retries + n.value + s.value.length - 9
}Self.name() calls one from inside the block. default is allowed as the name, and Config.default() is then the type's own default. The default keyword on its own stays the compiler's zero value. A bare call, named("web"), is an unknown function, and the error names the type it belongs to.
Calling a type calls its new
A type written as a call is a call to its new: Box(5) is Box.new(5), and Box<String>("x") is Box<String>.new("x"). It is the same function, so overloading, generics and ownership are all new's; the spelling is the only difference.
import std.io
import std.map
struct Point {
x: Int,
y: Int
}
impl Point {
fn new(x: Int, y: Int) -> Point { return Point { x: x, y: y } }
}
fn main() -> Int {
let p = Point(3, 4) // Point.new(3, 4)
let seen = Map<String, Int>() // Map<String, Int>.new()
let empty: Map<String, Int> = Map() // <String, Int> from the annotation
let mut words = Vec<String>() // an empty Vec, as `[]` is
words.push("hi")
println(p.x + p.y + seen.length + empty.length + words.length) // 8
return 0
}A type with no new is an error that says so, rather than an unknown function. A generic type's arguments come from the call's own arguments, from the call's written <...>, or from the annotation it is assigned to — let m = Map() has none of the three and is Map() needs its type arguments.
self
self is not a reserved word. Inside an impl block, a parameter written as a
bare self -- with no : Type -- takes the block's type. Written with a type it
is an ordinary parameter, and outside an impl block it is an ordinary name.
Like every parameter, self is a borrow by default. A method does not consume
its receiver unless the parameter says sink. See
ownership and borrowing.
self must be the first parameter. x.f(a) puts the receiver at argument zero, so
a receiver written anywhere else is a method no method call can reach:
struct Point {
x: Int
}
impl Point {
fn bad(dx: Int, self) -> Int {
return self.x + dx
}
}
fn main() -> Int {
return 0
}Visibility on methods
A method takes a visibility modifier exactly as a top-level function does, and it
means the same thing: private is the declaring file, internal is the declaring
package, and no modifier is public.
import std.io
struct Counter {
n: Int
}
impl Counter {
fn value(self) -> Int { return self.n }
private fn doubled(self) -> Int { return self.n * 2 }
internal fn tripled(self) -> Int { return self.n * 3 }
fn quadrupled(self) -> Int { return self.doubled() + self.doubled() }
}
fn main() -> Int {
let c = Counter { n: 5 }
print(c.value())
print(c.quadrupled())
return 0
}quadrupled calls doubled because they are declared in the same file. A caller
in another file may call value and quadrupled, may call tripled from anywhere
in the same package, and may not call doubled at all.
That the modifier means the same thing inside a block is not a coincidence to be remembered separately: a method is a free function whose first parameter is the receiver, and it carries the same file identity either way. See visibility.
Generic impl blocks
Put type parameters immediately after impl. They must occur in the target type,
which lets the receiver determine them at a call:
struct Box<T> {
value: T
}
impl<T> Box<T> {
fn get(self) -> T {
return self.value
}
fn choose(self, other: Self, first: Bool) -> T {
if (first) { return self.value }
return other.value
}
}
fn main() -> Int {
let left = Box<Int> { value: 3 }
let right = Box<Int> { value: 7 }
return left.choose(right, false) - 7
}Self denotes the complete target, so it means Box<T> above rather than the
unapplied name Box. A method may add its own parameters, and impl-level bounds
are checked when that method is instantiated:
struct Box<T> {
value: T
}
trait Scored {
fn score(self) -> Int
}
impl Scored for Int {
fn score(self) -> Int { return self }
}
impl<T: Scored> Box<T> {
fn scoreOf(self) -> Int { return score(self.value) }
}
fn main() -> Int {
let boxed = Box<Int> { value: 7 }
return boxed.scoreOf() - 7
}impl Box<Int> is also accepted for a concrete specialization. Trait impls may
be generic too, as in impl<T: Scored> Display for Box<T>; their applicability,
bounds, and overlap are checked structurally. The trait itself cannot take type
arguments yet. See traits.
Indexing: at and set
A struct can be indexed like a Vec by giving it two methods:
| You write | It calls |
|---|---|
x[i] | x.at(i) |
x[i] = v | x.set(i, v) |
set changes the value, so it takes inout self, and the struct has to be in a let mut. A struct with an at and no set can be read by index but not assigned to: x[i] = v is an error that says to write a set. The index can be any type the method takes, which is how m["key"] reads a Map. A generic struct's set is not reached this way yet, so a Map is still written with m.set(key, value).
import std.io
struct Grid {
width: Int,
cells: Vec<Int>
}
impl Grid {
fn at(self, index: Int) -> Int { return self.cells[index] }
fn set(inout self, index: Int, value: Int) { self.cells[index] = value }
}
fn main() -> Int {
let mut grid = Grid { width: 3, cells: Vec<Int>.filled(9, 0) }
grid[4] = 7
println(grid[4]) // 7
return 0
}std.mem's Buffer uses exactly this for b[i] and b[i] = x.
Standard-library methods
std.string carries a method surface over its str* functions. The two names are
the same function:
import std.io
import std.string
fn main() -> Int {
let text = " Prismio "
println(text.trim())
println(text.trim())
return 0
}The str* prefix is not going away in 0.1. Prismio has no module namespacing yet,
so every top-level name in an imported module is visible unqualified; the prefix is
what keeps the standard library's names from claiming words an application wants.
The methods are additive — but they are not free of that cost, because a method is
a free function whose first parameter is the receiver, so a method name is a
global name. std.string claims 64 unprefixed names, and a program defining its
own fn isDigit(c: Char) -> Bool alongside it is a duplicate definition rather
than an overload.
Chained calls and temporaries
Chaining works, and one wrinkle is worth knowing about: an intermediate result in a
chain is an owned temporary passed straight into the next call, and Prismio 0.1
does not always release it. a.toUpper().reverse() allocates a string for
toUpper that is not freed until the process exits.
The one place this does not apply is a chain of +, which is flattened into a
single call precisely so that it has no intermediate — see
operators.
Binding the intermediate avoids it:
import std.io
import std.string
fn main() -> Int {
let upper = "ab".toUpper()
println(upper.reverse())
return 0
}This is a leak, not a correctness bug -- the value is valid for as long as it is
used, and --verify reports it as leaked rather than as a violation.