Interfaces and Generics

Refingere has no classes and no inheritance. A type's data is its fields; what it can do is described by interfaces - lists of functions without bodies - which types promise to have with implement blocks. Code written against an interface works with every type that implements it, and generic code takes the type itself as a parameter. Interfaces are also how operators, loops and printing work on your own types.

Declaring an interface

Each member is a function declaration with no body. A member whose first parameter is self is called on a value. One without self is called on the type, like a constructor. Self stands for whichever type is implementing the interface. See Interface Definition.

Implementing one

implement Shape for Circle gives the bodies for one type, with self typed as that type. Every member has to be there. A type can implement any number of interfaces, each in its own block. See Implement an Interface.

Interfaces as types and as constraints

An interface can be used in two ways. As a type, Array<Shape> holds values of different types together, and each call runs the member of the value's own type. As a constraint on a type parameter, S: Shape, every value is of one type, S, known where the function is called. The constraint also makes the type's own functions available, as S.unit(). Use the type form for mixed collections, and the constraint form when the caller's type should come back out, as in largest.

Generic types

Structs, enums, unions, interfaces, type names and functions can all take type parameters in angle brackets. Labeled<T> attaches a label to a value of any type, and because describe returns a Labeled<S>, the value keeps its own type - labeled.value.radius works, since it's still a Circle. Type arguments are inferred from the values, and can be written out - largest<Circle>(...) - when they can't be.

Implementing for any type

Duration is builtin, and this file adds to it

An implement block is written apart from the type, in any file - so a type can be given an interface after the fact, including builtin types and types from another folder. The code that needs Describable doesn't have to own Duration to make it fit.

Methods every implementor gets

A method written for an interface, instead of for a type, is given to every type that implements the interface. It can only use what the interface declares, and each type gets it without writing anything. The builtin interfaces use this a lot: Iterable declares one member, and every iterable type gets map, filter, sum and the rest from it. See Implementations.

The builtin interfaces

Operators and language features are backed by builtin interfaces, so implementing one makes them work on your type:

InterfaceGives your type
Equatable==, !=, in and contains
Comparable<, <=, >, >=, sorting, min and max
Hashableuse as a Map key or a Set element
ToStringtoString(), and printing in #format
FromStringString.parse<T>(), and HTTP route parameters
Numeric+, -, *, /, //, %, ^, and sum
Iterablefor loops, .. spreading, and every collection method
Indexablevalue[key], subscripts and set value[key] := ...
Concatenable++
Callablecalling a value like a function
Castablevalue as T

The Interfaces reference lists all of them, with their members.

Deriving implementations

For interfaces with an obvious implementation - compare field by field, hash every field, print every field - @derive writes the implement block for you. It works on structs and enums, as long as every field supports the interface too. Write the block by hand when the obvious one isn't what you want, as with Money above. You can write derive macros of your own.

Combined types

A struct combined from others with && gets their implementations as well as their fields: every implement block and method of a part is written again for it, with self and Self now the combined struct, and whatever was derived is derived again over all its fields. It still isn't one of its parts, though - a Sign can't be passed where a Named is wanted - so a copied body that would need that is an error on the combined struct.

A struct's own implement block, @derive or method replaces the one it would get. Two of the structs it's combined from implementing the same interface, or having a method of the same name, is an error, unless the combined struct writes its own.

An enum combined with || gets an interface or method when every part has it, and each call goes to the part the value's case came from. Members that have no single case to go by - one called on the type, like unit, or one taking a second Self, like compare - can't be passed on this way, so unless every part derives them the enum implements them itself. See Enum Definition for the exact rules.

Generic interfaces

An interface can take type parameters, and each implement block fills them in. The builtin Iterable<T> and Indexable<Key, Value> work this way, so a type says what it iterates over and what it's indexed by.