Skip to content

Commit 19f2f5a

Browse files
committed
Bool assert idea
1 parent 6671778 commit 19f2f5a

File tree

1 file changed

+198
-0
lines changed

1 file changed

+198
-0
lines changed

docs/ideas/bool-assert.md

+198
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# Bool assert
2+
3+
There are times where we wish our Gleam code to deliberately panic:
4+
5+
- In tests when the code has shown incorrect behaviour.
6+
- In production code when the code has entered into an invalid state.
7+
8+
In both cases we want to have sufficient information with the panic so the
9+
programmer can understand what the problem is and correct it as easily as
10+
possible. This might include:
11+
12+
- That the panic is intentional and not from bug in FFI code.
13+
- The path of the file the panic occurred in.
14+
- The line number the panic occurred on within the file.
15+
- A message supplied by the programmer, should they want to include one.
16+
- The values that were checked when deciding whether to panic.
17+
18+
It is challenging to achieve most of this in Gleam today.
19+
20+
The approach that comes closest is to manually construct a message and use it
21+
with the `panic` keyword.
22+
23+
```gleam
24+
case check_some_property(a, b, c) {
25+
True -> Nil
26+
False -> {
27+
let detail = "<Message explaining the problem here>
28+
29+
a: " <> string.inspect(a) <> "
30+
b: " <> string.inspect(b) <> "
31+
c: " <> string.inspect(c)
32+
33+
panic as detail
34+
}
35+
}
36+
```
37+
38+
This contains most the information but has some problems.
39+
40+
The information is unstructured so it cannot be used easily by runtime
41+
systems such as test frameworks, exception trackers, and the logger. At best
42+
they can print the message as the programmer formatted it.
43+
44+
It is verbose. The programmer likely doesn't see this code as valuable enough
45+
to invest the time in writing, which results it being rarely done. More commonly
46+
`panic` will be used with no message, or a message without dynamic information
47+
such as the runtime values that caused the problem.
48+
49+
Another approach is to use the `let assert` syntax with a bool pattern.
50+
51+
```gleam
52+
let assert True = check_some_property(a, b, c)
53+
```
54+
55+
This does have the file and line information, but has no message or information
56+
about the values. We are planning to add support for a custom message to `let
57+
assert`, at which point is could have the same amount of information as `case` +
58+
`panic` approach, but with a slightly more concise syntax.
59+
60+
I have seen this pattern used in test code, but it is uncommon.
61+
62+
The last approach is to use runtime functions which internally use the `case` +
63+
`panic` pattern, or some FFI equivalent. This is most commonly done
64+
with assertion functions from test frameworks such as those found in
65+
`gleeunit`'s `gleeunit/should` module.
66+
67+
```gleam
68+
check_some_property(a, b, c)
69+
|> should.be_true
70+
71+
some_computation(abc)
72+
|> should.equal(xyz)
73+
```
74+
75+
When using FFI to panic these can provide the information in a structured way
76+
that can be used by test frameworks etc to provide more useful feedback, but
77+
they can only provide information that a function can know about.
78+
79+
They cannot tell the line number they have been called from. They may be able to
80+
determine the file by using FFI to throw and catch an exception and then parsing
81+
the stacktrace, but it's an ugly and potentially brittle solution.
82+
83+
In `x |> should.equal(y)` the function has a reference to `x` and `y`, so it can
84+
include them in the error information. In `f(abc) |> should.be_true` it only
85+
receives a `True` or a `False` value, so it is unable to include anything about
86+
the expression that evaluated to the bool, in this case `f` and `abc`.
87+
88+
This pattern is very common in test code.
89+
90+
## The proposal
91+
92+
I think Gleam would benefit from having a bool `assert` syntax, as the final way
93+
to `panic`. This syntax would take an expression that evaluates to a `Bool`,
94+
panicking if it is `False`, and including all of the information above in a
95+
structured form in the panic.
96+
97+
```python
98+
assert check_some_property(a, b, c)
99+
100+
assert wibble() == 123
101+
102+
assert level >= 30
103+
104+
assert wobble(a) |> wubble |> woo(b)
105+
```
106+
107+
It could also take an optional message from the programmer.
108+
109+
```python
110+
assert level >= 30 as "frozen orb requires level 30"
111+
```
112+
113+
Gleam [runtime errors](../runtime-errors.md) always have at least the below
114+
properties, and so `assert` would be no different.
115+
116+
| Key | Type | Value |
117+
| --- | ---- | ----- |
118+
| gleam_error | Atom | See individual errors |
119+
| message | String | See individual errors |
120+
| module | String | The module the error occured in |
121+
| function | String | The function the error occured in |
122+
| line | Int | The line number the error occured on |
123+
124+
`gleam_error` would be `"assert"`, and `message` would be the message supplied
125+
by the programmer, defaulting to `"Assertion failed"` if none is supplied.
126+
127+
Depending on the expression given `assert` can include other information too.
128+
129+
```python
130+
assert level >= 30
131+
```
132+
133+
| Key | Type | Value |
134+
| --- | ---- | ----- |
135+
| kind | Atom | `binary_operator` |
136+
| operator | Atom | `>=` |
137+
| left | Map | See expression table below |
138+
| right | Map | See expression table below |
139+
140+
```python
141+
assert check_some_property(a, b, c)
142+
```
143+
144+
| Key | Type | Value |
145+
| --- | ---- | ----- |
146+
| kind | Atom | `function_call` |
147+
| arguments | List of map | See expression table below |
148+
149+
150+
```python
151+
assert other_expression
152+
```
153+
154+
| Key | Type | Value |
155+
| --- | ---- | ----- |
156+
| kind | Atom | `expression` |
157+
| expression | Map | See expression table below |
158+
159+
Where the expression maps have this structure:
160+
161+
| Key | Type | Value |
162+
| --- | ---- | ----- |
163+
| value | t1 | the value the expression evaluated to at runtime |
164+
| kind | Atom | `literal` or `expression` |
165+
166+
With this the programmer would write minimal boilerplate (just the keyword
167+
`assert`) and the runtime error handling tooling would be able to have a high
168+
degree of capability with the error. For example, a test runner could produce
169+
these errors:
170+
171+
```python
172+
assert lock_user(username, 10, role) as "Lock required for user"
173+
```
174+
```
175+
176+
error: Lock required for user
177+
test/myapp/user_test.gleam:45
178+
179+
assert lock_user(username, 10, role)
180+
181+
1. "lucystarfish"
182+
2. 10
183+
3. Admin
184+
```
185+
186+
```python
187+
assert level >= 30
188+
```
189+
```
190+
191+
error: Assertion failed
192+
test/myapp/user_test.gleam:45
193+
194+
assert level >= 30
195+
196+
left: 27
197+
right: 30
198+
```

0 commit comments

Comments
 (0)