Methods add functional behavior to a defined type, offering capabilities consistent with and helpful for values of that type. For example (as mentioned earlier), `+` is one of several methods defined for the i32 type. It adds two integers together and returns the result.
Methods are pre-defined for the number types. However, for some custom-declared types, such as structs or enums, no default methods are provided. Instead, methods are defined in the same block as the data structure declarations.
This example defines several methods as part of the Point declaration:
struct Point: x f32 y f32 // Calculate distance between two points fn distance(pt2) f32: (self-pt2).len // Subtract two points fn `-`(pt2) Point: Point[pt2.x - x, pt2.y - y] // Calculate length of point fn len() f32: _lensq().sqrt // _lensq() is equivalent to self._lensq() // A private method fn _lensq() f32: x*x + y*y
There's a lot to unpack here, which we will get to soon. The end result is that the above type definition for Point now makes it possible to write logic that uses these methods:
imm from = Point[x: 1., y: 1.] imm to = Point[x: 4., y: 5.] imm dist = from.distance(to) // == 5.0. Pythagoras would be proud
Methods may be defined for named types, such as structs, enums, integers, and floating point. However, methods may not be defined for the array types, as they are unnamed. This restriction is easily remedied by wrapping an array in a named struct.
In addition, the extend command may be used to add methods to an existing type. For example:
extend i32: fn double() Self: self+self
A method definition looks (and behaves) like a function definition: It has a name, named parameter(s), return value type(s), and a statement body. Let's examine how method definitions differ from functions.
As with fields, the names given to methods will not conflict with another type's method or field names, nor with the names of variables or functions outside the type. However, a type cannot have a method and field with the same name.
Two or more methods may be given the same name. This is called method overloading. Each same-named method should be given a different parameter "signature": they should vary in number of parameters or in the types of the parameters. A call to a method of this name will be resolved by matching the method call's argument types to the defined methods' signatures. The method that matches best is used. Supporting multiple methods of the same name makes it convenient for a type to support a similar capability for a broad range of differently-typed values.
A method's first parameter is always named self. If a self parameter is not specified, one is inserted at the beginning of the parameter list. If the type of self is not specified, it is assumed to be the same as the type we are specifying (Self). If a type is specified for self, it must either be the same as Self, or some kind of reference to Self.
Implicit parameter types
As with the self parameter, a method's other parameters need not specify their type. If omitted, Self is assumed.
Implicit field and method access
Nearly always, a method's logic needs access to the data object referred to by self. Often, it may also want to invoke other methods in the same type. As one would expect, such capabilities are easily achieved through the use of self:
imm len = (self.x * self.x + self.y * self.y).sqrt self.clear()
Fortunately, a more concise approach is possible, by omitting "self." altogether:
imm len = (x * x + y * y).sqrt clear()
Using the more concise syntax depends on not defining any local variables in the method that would conflict with the type's field or method names.