Abilities

An Ability defines a set of functions that can be implemented by different types.

Abilities are used to constrain the types of function arguments to only those which implement the required functions.

The function toJson below is an example which uses the Encoding Ability.

toJson : a -> List U8 where a implements Encoding
toJson = \val ->
    val |> Encode.toBytes JSON.encoder

By specifying the type variable a implements the Encoding Ability, this function can make use of Encode.toBytes and JSON.encoder to serialise val, without knowing its specific type.

All types which implement the Encoding Ability can therefore use the Encode.toBytes (and also Encode.append) functions to conveniently serialise values to bytes.

Builtins

Roc's Builtin types such as numbers, records, and tags, are automatically derived for the builtin Abilities. This means that you can use these Abilities without needing to provide a custom implementation.

Eq Ability

The Eq Ability defines the isEq function, which can be used to compare two values for structural equality. The infix operator == can be used as shorthand for isEq.

Eq is not derived for F32 or F64 as these types do not support structural equality. If you need to compare floating point numbers, you must provide your own function for comparison.

Example showing the use of isEq and == to compare two values.

Colors : [Red, Green, Blue]

red = Red
blue = Blue

expect isEq red Red # true
expect red == blue # false

Definition of the Eq Ability.

# Bool.roc
Eq implements
    isEq : a, a -> Bool where a implements Eq

Structural equality is defined as follows:

  1. Tags are equal if their name and also contents are equal.
  2. Records are equal if their fields are equal.
  3. The collections Str, List, Dict, and Set are equal iff they are the same length and their elements are equal.
  4. Num values are equal if their numbers are equal. However, if both inputs are NaN then isEq returns Bool.false. Refer to Num.isNaN for more detail.
  5. Functions cannot be compared for structural equality, therefore Roc cannot derive isEq for types that contain functions.

Hash Ability

The Hash Ability defines the hash function, which can be used to hash a value. The hash function takes a Hasher as an argument, which is used to compute the hash.

# Hash.roc
Hash implements
    hash : hasher, a -> hasher where a implements Hash, hasher implements Hasher

Sort Ability

Implementation Status - Design Proposal, implementation has not yet started. See zulip discussion thread for more information. If you would like to help implement this, please let us know.

The Sort Ability defines the compare function, which can be used to compare two values for ordering.

Sort is not derived for Str as working with utf-8 strings which is a variable length encoding scheme is complex and is achieved through a dedicated library such as roc-lang/unicode.

Proposed Definition of the Sort Ability.

# Sort.roc
Sort implements
    compare : a, a -> [LessThan, Equals, GreaterThan] where a implements Sort

Encoding Ability

The Encoding Ability defines toEncoder which can be used with an Encoder to serialise value from Roc to bytes using the Encoding.toBytes and Encoding.append functions.

Functions are not serialisable, therefore Roc does not derive Encoding for types that contain functions.

Encoding for Dict values has not been implemented, see #5294 for more details. If you would like to help implement this, please let us know.

Example showing the use of Encoding.toBytes to serialise a Roc List (Str, U32) to a JSON encoded string.

bytes : List U8
bytes = "[[\"Apples\",10],[\"Bananas\",12],[\"Oranges\",5]]" |> Str.toUtf8

fruitBasket : List (Str, U32)
fruitBasket = [
    ("Apples", 10),
    ("Bananas", 12),
    ("Oranges", 5)
]

expect Encode.toBytes fruitBasket json == bytes # true

Definition of the Encoding Ability.

# Encode.roc
Encoding implements
    toEncoder : val -> Encoder fmt where val implements Encoding, fmt implements EncoderFormatting

Decoding Ability

The Decoding Ability defines decoder which can be used with a Decoder to de-serialise from bytes to Roc values using the Decoding.fromBytesPartial and Decoding.fromBytes functions.

Decoding for Dict values has not been implemented, see #5294 for more details. If you would like to help implement this, please let us know.

Example showing the use of Decoding.fromBytes to decode a Roc List (U32, Str) from a JSON encoded string.

bytes : List U8
bytes =
    """
    [
        [ 10, \"Green Bottles\" ],
        [ 12, \"Buckle My Shoe\" ],
        [ 5, \"Little Ducks\" ]
    ]
    """
    |> Str.toUtf8

result : Result (List (U32, Str)) _
result = Decode.fromBytes bytes json

expect result == Ok [(10, "Green Bottles"), (12, "Buckle My Shoe"), (5, "Little Ducks")] # true

Definition of the Decoding Ability.

# Decode.roc
Decoding implements
    decoder : Decoder val fmt where val implements Decoding, fmt implements DecoderFormatting

Inspect Ability

The Inspect Ability lets you turn values into strings (or other things, but most commonly strings) that inform Roc programmers about the contents of the value.

Every Roc value has the Inspect ability automatically, although some of them (such as opaque types which do not list Inspect as one of their abilities) use a default Inspect implementation which doesn't expose any information about the value. Any opaque type can replace this default Inspect implementation with a custom one which exposes as much information as desired about that type's values.

Definition of the Inspect Ability.

Inspect implements
    toInspector : val -> Inspector f
        where
            val implements Inspect,
            f implements InspectFormatter

The toInspector function takes a value and returns an Inspector which describes how to abstractly represent that value's contents in a way that doesn't tie it to a particular representation (such as a string). Then a separate "formatter" can translate a given Inspector to a specific format (such as a string in the case of Inspect.toStr, but also possibly a structured log format, or an interactive GUI element).

Example formatters:

Opaque Types

Opaque Types are used to hide implementation details of a type. Modules export functions to define a public API for working with a type.

By default abilities are not derived for Opaque Types. However, Derived and Custom implementations are two ways to work with abilities for your Opaque Types.

Derived Implementations

Abilities can be automatically derived for Opaque Types where the type is an alias for a builtin, or it is composed of other types which also implement that ability.

For example you can automatically derive the Eq and Hash abilities using implements [ Eq, Hash ].

Example showing how to automatically derive the Eq and Hash abilities for an Opaque Type.

StatsDB := Dict Str { score : Dec, average : Dec } implements [ Eq, Hash ]

add : StatsDB, Str, { score : Dec, average : Dec } -> StatsDB
add = \@StatsDB db, name, stats -> db |> Dict.insert name stats |> @StatsDB

expect
    db1 = Dict.empty {} |> @StatsDB |> add "John" { score: 10, average: 10 }
    db2 = Dict.empty {} |> @StatsDB |> add "John" { score: 10, average: 10 }

    db1 == db2 # true

Custom Implementations

You can provide a custom implementation for an ability. This may be useful if a type is composed of other types which do not implement an ability, or if you would like to override the default behaviour.

Example showing how to provide a custom implementation for the Eq ability.

Color := [
        RGBAu8 U8 U8 U8 U8,
        RGBAf32 F32 F32 F32 F32,
    ]
    implements [
        Eq { isEq: colorEquality },
    ]

# Note that Eq is not available for an F32, hence we provide a custom implementation here.
colorEquality : Color, Color -> Bool
colorEquality = \a, b -> colorToU8 a == colorToU8 b

colorToU8 : Color -> (U8, U8, U8, U8)
colorToU8 = \@Color c->
    when c is
        RGBAu8 r g b a -> (r, g, b, a)
        RGBAf32 r g b a -> (f32toU8 r, f32toU8 g, f32toU8 b, f32toU8 a)

f32toU8 : F32 -> U8
f32toU8 = \f ->
    Num.floor (f * 255.0)

fromU8 : U8, U8, U8, U8 -> Color
fromU8 = \r, g, b, a -> @Color (RGBAu8 r g b a)

fromI16 : I16, I16, I16, I16 -> Result Color [OutOfRange]
fromI16 = \r, g, b, a ->
    if r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255 || a < 0 || a > 255 then
        Err OutOfRange
    else
        Ok (@Color (RGBAu8 (Num.toU8 r) (Num.toU8 g) (Num.toU8 b) (Num.toU8 a)))

fromF32 : F32, F32, F32, F32 -> Result Color [OutOfRange]
fromF32 = \r, g, b, a ->
    if r < 0.0 || r > 1.0 || g < 0.0 || g > 1.0 || b < 0.0 || b > 1.0 || a < 0.0 || a > 1.0 then
        Err OutOfRange
    else
        Ok (@Color (RGBAf32 r g b a))

Advanced Topic: Defining a new Ability

It is possible to define a new Ability in addition to those provided in builtins. This should be avoided if possible and only used in rare circumstances by package authors.

Example showing how to define a new Ability.

CustomInspect implements
    inspectMe : val -> Str where val implements CustomInspect

inspect : val -> Str where val implements CustomInspect
inspect = \val -> inspectMe val

Color := [Red, Green, Blue]
    implements [
        Eq,
        CustomInspect {
            inspectMe: inspectColor,
        },
    ]

inspectColor : Color -> Str
inspectColor = \@Color color ->
    when color is
        Red -> "Red"
        Green -> "Green"
        Blue -> "Blue"

expect
    [@Color Red, @Color Green, @Color Blue]
    |> List.map inspect
    |> Str.joinWith ","
    |> Bool.isEq "Red,Green,Blue"