-
-
Notifications
You must be signed in to change notification settings - Fork 320
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6068 from lukewilliamboswell/lang-ref-abilities
Add Abilities language reference
- Loading branch information
Showing
2 changed files
with
293 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,286 @@ | ||
|
||
# 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. | ||
|
||
```roc | ||
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](#builtins) | ||
- [`Eq` Ability](#eq-ability) | ||
- [`Hash` Ability](#hash-ability) | ||
- [`Sort` Ability](#sort-ability) | ||
- [`Encoding` Ability](#encoding-ability) | ||
- [`Decoding` Ability](#decoding-ability) | ||
- [`Inspect` Ability](#inspect-ability) | ||
- [Opaque Types](#opaque-types) | ||
- [Derived Implementions](#derived-implementions) | ||
- [Custom Implementations ](#custom-implementations) | ||
- [Advanced Topic: Defining a new Ability](#defining-a-new-ability) | ||
|
||
## [Builtins](#builtins) {#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](#eq-ability) {#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. | ||
|
||
```roc | ||
Colors : [Red, Green, Blue] | ||
red = Red | ||
blue = Blue | ||
expect isEq red Red # true | ||
expect red == blue # false | ||
``` | ||
|
||
**Definition** of the `Eq` Ability. | ||
|
||
```roc | ||
# 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](#hash-ability) {#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. | ||
|
||
```roc | ||
# Hash.roc | ||
Hash implements | ||
hash : hasher, a -> hasher where a implements Hash, hasher implements Hasher | ||
``` | ||
|
||
### [`Sort` Ability](#sort-ability) {#sort-ability} | ||
|
||
**Implementation Status** - Design Proposal, implementation has not yet started. See [zulip discussion thread](https://roc.zulipchat.com/#narrow/stream/304641-ideas/topic/ordering.2Fsorting.20ability/near/395539545) 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](https://github.com/roc-lang/unicode). | ||
|
||
**Proposed Definition** of the `Sort` Ability. | ||
|
||
```roc | ||
# Sort.roc | ||
Sort implements | ||
compare : a, a -> [LessThan, Equals, GreaterThan] where a implements Sort | ||
``` | ||
|
||
### [`Encoding` Ability](#encoding-ability) {#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 implemened**, see [#5294](https://github.com/roc-lang/roc/issues/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](https://www.json.org/json-en.html) encoded string. | ||
|
||
```roc | ||
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. | ||
|
||
```roc | ||
# Encode.roc | ||
Encoding implements | ||
toEncoder : val -> Encoder fmt where val implements Encoding, fmt implements EncoderFormatting | ||
``` | ||
|
||
### [`Decoding` Ability](#decoding-ability) {#decoding-ability} | ||
|
||
The `Decoding` Ability defines `decoder` which can be used with an Decoder to de-serialise from bytes to Roc values using the `Decoding.fromBytesPartial` and `Decoding.fromBytes` functions. | ||
|
||
Decoding for `Dict` values **has not been implemened**, see [#5294](https://github.com/roc-lang/roc/issues/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](https://www.json.org/json-en.html) encoded string. | ||
|
||
```roc | ||
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. | ||
|
||
```roc | ||
# Decode.roc | ||
Decoding implements | ||
decoder : Decoder val fmt where val implements Decoding, fmt implements DecoderFormatting | ||
``` | ||
|
||
### [`Inspect` Ability](#inspect-ability) {#inspect-ability} | ||
|
||
**Implementation Status** - Accepted Proposal, implementation in-progress. See [#5775](https://github.com/roc-lang/roc/pull/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: | ||
- A [LogFormatter](https://github.com/roc-lang/roc/blob/main/examples/LogFormatter.roc) which creates a string representation of Roc values, for use e.g. debug printing to the console. | ||
- A [GuiFormatter](https://github.com/roc-lang/roc/blob/main/examples/GuiFormatter.roc) which creates a GUI representation of Roc values for use e.g. debug visualization in a graphical application. | ||
|
||
**Definition** of the `Inspect` Ability. | ||
|
||
```roc | ||
# Inspect.roc | ||
Inspect implements | ||
toInspector : val -> Inspector f where val implements Inspect, f implements InspectFormatter | ||
``` | ||
|
||
## [Opaque Types](#opaque-types) {#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 derieved for Opaque Types. However, [Derived](#derived-implementions) and [Custom](#custom-implementations) implementations are two ways to work with abilities for your Opaque Types. | ||
|
||
### [Derived Implementions](#derived-implementions) {#derived-implementions} | ||
|
||
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. | ||
|
||
```roc | ||
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](#custom-implementations) {#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. | ||
|
||
```roc | ||
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](#defining-a-new-ability) {#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. | ||
|
||
```roc | ||
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" | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters