Skip to content

Commit 6169312

Browse files
committed
Add encode example demonstrating static dispatch
- Add examples/encode.roc showing Encode module with custom JSON format - Add docs/encode.md with documentation for the new encoding pattern - Demonstrates where clauses for encode_str and encode_list methods
1 parent e914e0a commit 6169312

2 files changed

Lines changed: 327 additions & 0 deletions

File tree

docs/encode.md

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# Encode Module with Static Dispatch
2+
3+
This document explains the new `Encode` pattern in Roc using static dispatch via `where` clauses, based on [this commit](https://github.com/roc-lang/roc/commit/22cf61ff9332f0de7a0d5d7f42b7f5836232a744).
4+
5+
## Overview
6+
7+
The Encode module enables serializing values to bytes using **static dispatch** instead of the old Abilities system. Users define custom format types with specific methods, and the compiler resolves method calls at compile time.
8+
9+
## How It Works
10+
11+
### 1. Builtin Methods with `where` Clauses
12+
13+
The builtins define `encode` methods on `Str` and `List` with `where` clauses that require the format type to have specific methods:
14+
15+
```roc
16+
# In Str (builtin)
17+
encode : Str, fmt -> List(U8)
18+
where [fmt.encode_str : fmt, Str -> List(U8)]
19+
encode = |self, format| {
20+
Fmt : fmt
21+
Fmt.encode_str(format, self)
22+
}
23+
24+
# In List (builtin)
25+
encode : List(item), fmt -> List(U8)
26+
where [
27+
fmt.encode_list : fmt, List(item), (item, fmt -> List(U8)) -> List(U8),
28+
item.encode : item, fmt -> List(U8)
29+
]
30+
encode = |self, format| {
31+
Fmt : fmt
32+
Item : item
33+
Fmt.encode_list(format, self, |elem, f| Item.encode(elem, f))
34+
}
35+
```
36+
37+
### 2. Define a Custom Format Type
38+
39+
To use `Str.encode` or `List.encode`, you define a nominal type with the required methods:
40+
41+
```roc
42+
JsonFormat := [Format].{
43+
# Required by Str.encode
44+
encode_str : JsonFormat, Str -> List(U8)
45+
encode_str = |_fmt, str| {
46+
quoted = "\"${str}\""
47+
Str.to_utf8(quoted)
48+
}
49+
50+
# Required by List.encode
51+
encode_list : JsonFormat, List(item), (item, JsonFormat -> List(U8)) -> List(U8)
52+
encode_list = |fmt, items, encode_item| {
53+
var $result = ['[']
54+
var $first = Bool.True
55+
56+
for item in items {
57+
if $first {
58+
$first = Bool.False
59+
} else {
60+
$result = $result.append(',')
61+
}
62+
encoded_item = encode_item(item, fmt)
63+
$result = $result.concat(encoded_item)
64+
}
65+
66+
$result.append(']')
67+
}
68+
}
69+
```
70+
71+
Key points:
72+
- `JsonFormat := [Format].{...}` defines a nominal type wrapping a tag union `[Format]`
73+
- Methods are defined inside the `.{...}` block
74+
- `encode_str` handles string encoding
75+
- `encode_list` handles list encoding, receiving a callback to encode each item
76+
77+
### 3. Create a Format Instance
78+
79+
Construct the format using the tag constructor:
80+
81+
```roc
82+
json_fmt = JsonFormat.Format
83+
```
84+
85+
### 4. Encode Values
86+
87+
Call `.encode()` on strings or lists:
88+
89+
```roc
90+
# Encode a string
91+
hello_str = "Hello, World!"
92+
encoded_str = hello_str.encode(json_fmt)
93+
# Result: [34, 72, 101, 108, 108, 111, ...] (UTF-8 bytes of "Hello, World!")
94+
95+
# Encode a list of strings
96+
names = ["Alice", "Bob", "Charlie"]
97+
encoded_list = names.encode(json_fmt)
98+
# Result: UTF-8 bytes of ["Alice","Bob","Charlie"]
99+
```
100+
101+
### 5. Convert Back to String
102+
103+
```roc
104+
match Str.from_utf8(encoded_str) {
105+
Ok(json_str) => Stdout.line!("As JSON: ${json_str}")
106+
Err(_) => Stdout.line!("(invalid UTF-8)")
107+
}
108+
```
109+
110+
## Complete Example
111+
112+
```roc
113+
app [main!] { pf: platform "../platform/main.roc" }
114+
115+
import pf.Stdout
116+
117+
# Define a custom JSON-like format type with the required methods
118+
JsonFormat := [Format].{
119+
encode_str : JsonFormat, Str -> List(U8)
120+
encode_str = |_fmt, str| {
121+
quoted = "\"${str}\""
122+
Str.to_utf8(quoted)
123+
}
124+
125+
encode_list : JsonFormat, List(item), (item, JsonFormat -> List(U8)) -> List(U8)
126+
encode_list = |fmt, items, encode_item| {
127+
var $result = ['[']
128+
var $first = Bool.True
129+
130+
for item in items {
131+
if $first {
132+
$first = Bool.False
133+
} else {
134+
$result = $result.append(',')
135+
}
136+
encoded_item = encode_item(item, fmt)
137+
$result = $result.concat(encoded_item)
138+
}
139+
140+
$result.append(']')
141+
}
142+
}
143+
144+
main! : List(Str) => Try({}, [Exit(I32)])
145+
main! = |_args| {
146+
json_fmt = JsonFormat.Format
147+
148+
# Encode a string
149+
hello_str = "Hello, World!"
150+
encoded_str = hello_str.encode(json_fmt)
151+
152+
Stdout.line!("Encoded string:")
153+
match Str.from_utf8(encoded_str) {
154+
Ok(json_str) => Stdout.line!(" ${json_str}")
155+
Err(_) => Stdout.line!(" (invalid UTF-8)")
156+
}
157+
158+
# Encode a list of strings
159+
names = ["Alice", "Bob", "Charlie"]
160+
encoded_list = names.encode(json_fmt)
161+
162+
Stdout.line!("Encoded list:")
163+
match Str.from_utf8(encoded_list) {
164+
Ok(json_str) => Stdout.line!(" ${json_str}")
165+
Err(_) => Stdout.line!(" (invalid UTF-8)")
166+
}
167+
168+
Ok({})
169+
}
170+
```
171+
172+
## Output
173+
174+
```
175+
Encoded string:
176+
"Hello, World!"
177+
Encoded list:
178+
["Alice","Bob","Charlie"]
179+
```
180+
181+
## Static Dispatch Pattern
182+
183+
The key insight is that `where` clauses enable **ad-hoc polymorphism**:
184+
185+
1. A function declares what methods it needs via `where [type.method : signature]`
186+
2. Any type that has those methods can be used
187+
3. The compiler resolves the correct method at compile time (static dispatch)
188+
4. No runtime overhead from dynamic dispatch
189+
190+
This replaces the old Abilities system with a simpler, more flexible pattern.

examples/encode.roc

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
app [main!] { pf: platform "../platform/main.roc" }
2+
3+
import pf.Stdout
4+
5+
# Example demonstrating the new Encode module with static dispatch
6+
# Based on: https://github.com/roc-lang/roc/commit/22cf61ff9332f0de7a0d5d7f42b7f5836232a744
7+
#
8+
# The Encode module uses static dispatch via where clauses:
9+
# - Str.encode requires: where [fmt.encode_str : fmt, Str -> List(U8)]
10+
# - List.encode requires: where [fmt.encode_list : fmt, List(item), (item, fmt -> List(U8)) -> List(U8)]
11+
12+
# Define a custom JSON-like format type with the required methods
13+
JsonFormat := [Format].{
14+
# Method required by Str.encode where clause
15+
encode_str : JsonFormat, Str -> List(U8)
16+
encode_str = |_fmt, str| {
17+
# Wrap string in quotes and convert to bytes
18+
quoted = "\"${str}\""
19+
Str.to_utf8(quoted)
20+
}
21+
22+
# Method required by List.encode where clause
23+
encode_list : JsonFormat, List(item), (item, JsonFormat -> List(U8)) -> List(U8)
24+
encode_list = |fmt, items, encode_item| {
25+
var $result = ['[']
26+
var $first = Bool.True
27+
28+
for item in items {
29+
if $first {
30+
$first = Bool.False
31+
} else {
32+
$result = $result.append(',')
33+
}
34+
encoded_item = encode_item(item, fmt)
35+
$result = $result.concat(encoded_item)
36+
}
37+
38+
$result.append(']')
39+
}
40+
}
41+
42+
# Define a Person type that can be encoded as a JSON object
43+
Person := [Person({ name : Str, age : U64 })].{
44+
# Custom encode method for Person - encodes as JSON object
45+
encode : Person, JsonFormat -> List(U8)
46+
encode = |self, fmt| {
47+
# Get the inner record via pattern match
48+
match self {
49+
Person({ name, age }) => {
50+
# Encode name as JSON string
51+
name_bytes = name.encode(fmt)
52+
53+
# Encode age as number (no quotes)
54+
age_bytes = Str.to_utf8(age.to_str())
55+
56+
# Build: {"name":"...","age":...}
57+
var $result = Str.to_utf8("{\"name\":")
58+
$result = $result.concat(name_bytes)
59+
$result = $result.concat(Str.to_utf8(",\"age\":"))
60+
$result = $result.concat(age_bytes)
61+
$result = $result.concat(Str.to_utf8("}"))
62+
$result
63+
}
64+
}
65+
}
66+
}
67+
68+
main! : List(Str) => Try({}, [Exit(I32)])
69+
main! = |_args| {
70+
# Create our format instance using the tag constructor
71+
json_fmt = JsonFormat.Format
72+
73+
# Encode a string using static dispatch
74+
# This calls Str.encode which requires fmt.encode_str
75+
hello_str = "Hello, World!"
76+
encoded_str = hello_str.encode(json_fmt)
77+
78+
Stdout.line!("Encoded string:")
79+
Stdout.line!(" Input: ${hello_str}")
80+
81+
# Convert back to string to show the JSON format
82+
match Str.from_utf8(encoded_str) {
83+
Ok(json_str) => Stdout.line!(" As JSON: ${json_str}")
84+
Err(_) => Stdout.line!(" (invalid UTF-8)")
85+
}
86+
87+
Stdout.line!("")
88+
89+
# Encode a list of strings using static dispatch
90+
# This calls List.encode which requires fmt.encode_list and item.encode
91+
names = ["Alice", "Bob", "Charlie"]
92+
encoded_list = names.encode(json_fmt)
93+
94+
Stdout.line!("Encoded list of strings:")
95+
Stdout.line!(" Input: [\"Alice\", \"Bob\", \"Charlie\"]")
96+
97+
match Str.from_utf8(encoded_list) {
98+
Ok(json_str) => Stdout.line!(" As JSON: ${json_str}")
99+
Err(_) => Stdout.line!(" (invalid UTF-8)")
100+
}
101+
102+
Stdout.line!("")
103+
104+
# Encode a Person as a JSON object
105+
alice : Person
106+
alice = Person.Person({ name: "Alice", age: 30 })
107+
person_bytes = alice.encode(json_fmt)
108+
109+
Stdout.line!("Encoded Person object:")
110+
Stdout.line!(" Input: { name: \"Alice\", age: 30 }")
111+
112+
match Str.from_utf8(person_bytes) {
113+
Ok(json_str) => Stdout.line!(" As JSON: ${json_str}")
114+
Err(_) => Stdout.line!(" (invalid UTF-8)")
115+
}
116+
117+
Stdout.line!("")
118+
119+
# Encode a list of Person objects
120+
people : List(Person)
121+
people = [
122+
Person.Person({ name: "Alice", age: 30 }),
123+
Person.Person({ name: "Bob", age: 25 }),
124+
Person.Person({ name: "Charlie", age: 35 }),
125+
]
126+
people_bytes = people.encode(json_fmt)
127+
128+
Stdout.line!("Encoded list of Person objects:")
129+
Stdout.line!(" Input: [{ name: \"Alice\", age: 30 }, { name: \"Bob\", age: 25 }, { name: \"Charlie\", age: 35 }]")
130+
131+
match Str.from_utf8(people_bytes) {
132+
Ok(json_str) => Stdout.line!(" As JSON: ${json_str}")
133+
Err(_) => Stdout.line!(" (invalid UTF-8)")
134+
}
135+
136+
Ok({})
137+
}

0 commit comments

Comments
 (0)