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

Implementation Status - Accepted Proposal, implementation in-progress. See #5775 for progress on auto-deriving Inspect for builtin types.

The Inspect Ability defines the toInspector function which can be used with an Inspector to inspect Roc values using the Inspect.inspect function.

Example Inspectors:

Definition of the Inspect Ability.

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

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"