|
| 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