Custom types
Every type so far is structural: two types are the same if they have the same shape. Any
list(name = "Ada") is a list{name: character}, and every double is interchangeable with every
other. That is the right default for R, where values are plain data, but it cannot tell Celsius from
Fahrenheit, or a user ID from an email address.
alias names a shape and keeps it structural, while type declares a nominal type, distinct by
name. Both also work for ry’s own records, tuples and tag unions, as
Records and tags shows.
Nominal types
Section titled “Nominal types”A nominal type is distinct by name, even from a type with the same shape. type declares one,
and @Money(value) marks the one place that creates its values. The comment spelling is
#: @type and #: @new:
#: @type Money {list{amount: double, currency: character}}
#: fn(amount: double, currency: character) -> Moneymoney <- function(amount, currency) { if (amount < 0) stop("negative amount") #: @new Money structure(list(amount = amount, currency = currency), class = "Money")}
#: fn(m: Money) -> doubleamount_of <- function(m) m$amount
amount_of(list(amount = 1, currency = "EUR"))type Money = list{amount: double, currency: character}
money <- function(amount: double, currency: character) -> Money { if (amount < 0) stop("negative amount") @Money(structure(list(amount = amount, currency = currency), class = "Money"))}
amount_of <- function(m: Money) -> double m$amount
amount_of(list(amount = 1, currency = "EUR"))type-mismatch
x expected `Money`, found `list{amount: double, currency: character}`. The value has the right shape, but `Money` is a nominal type, so only `@Money(...)` or `#: @new Money` creates oneA list with the right fields is still not a Money. Put @Money(...) in one constructor and
nowhere else, and every Money in the program has passed its checks: @Money(...) checks the
shape when ry analyzes the code, and stop() checks the values when it runs. At run time
@Money(x) is just x.
Scalars work the same way. With type UserId = character and type Email = character, passing an
Email where a UserId is expected is an error, although both are strings at run time.
Operators
Section titled “Operators”A nominal value is still accepted where its representation is, so nchar() works on a UserId.
Arithmetic gives the plain representation back: with type Celsius = double, the sum of two
Celsius values is a double, because ry cannot assume that adding two temperatures gives a
temperature. Money is a list, so + on it is an error until you declare the operator:
#: fn(a: Money, b: Money) -> Money`+.Money` <- function(a, b) money(a$amount + b$amount, a$currency)`+.Money` <- function(a: Money, b: Money) -> Money { money(a$amount + b$amount, a$currency)}Now money(1, "EUR") + money(2, "EUR") is a Money. R finds +.Money through the class attribute,
which is why the constructor sets one; ry itself needs no class to check the type.
Parameters and aliases
Section titled “Parameters and aliases”A type can take parameters: with type Page<T> = list{items: list[T], total: integer}, reading
items from a Page<integer> gives a list[integer].
alias names a type without making it nominal, so alias Row = list{id: integer} is a shorter
way to write the shape.
Definitions belong at the top level of a file. In the comment spelling, a @type or @alias line
needs its own #: block, separated from other annotations by a blank line. If you keep both
spellings, put the #: block directly above the inline definition: ry reads the pair as one
definition and warns when the two disagree.
R6 and S4 objects
Section titled “R6 and S4 objects”Existing R6 and S4 classes keep working, but their objects are Unknown, and ry warns on each
class definition (object-system) because records and tags
model data with every field checked. To check who receives one, give it a nominal type over Any and wrap its
constructor:
#: @type Account {Any}
#: fn() -> Accountnew_account <- function() { #: @new Account AccountClass$new()}type Account = Any
new_account <- function() -> Account @Account(AccountClass$new())