Stream: ideas

Topic: Change `expect` to debug arguments instead of values


view this post on Zulip Ryan Bates (Dec 20 2023 at 18:28):

One issue I have with the expect is that it requires I add a separate value to see useful output when the test fails.

expect
    result = double [1, 2, 3]
    result == [2, 4, 6]

I'd much rather write:

expect double [1, 2, 3] == [2, 4, 6]

However then I can't see the output of double [1, 2, 3].

Would it be possible to show what is given to == instead of requiring I assign a value? I don't know how == works under the hood, but if it compiles to a function you could have expect show any arguments passed to the top level function. This way it will work for other calls besides ==.

expect List.isEmpty (trim ['', '', ''])

That could output Expected List.isEmpty to return true given [''] or whatever the output of trim was.

Is this possible?

view this post on Zulip LoipesMas (Dec 20 2023 at 21:17):

I've also run into this. I think it would be nice if it could show every "expression" involved in the final expression.
So for expect double [1, 2, 3] == [2, 4, 7] it would be

double [1, 2, 3] = [2,4,7]
double [1, 2, 3] == [2, 4, 7] = Bool.False

(literals can be omitted)
And for expect List.isEmpty (trim ['', '', ''])

trim ['', '', ''] = ['']
List.isEmpty (trim ['', '', '']) == Bool.False

Not sure if trim ['','',''] should be replaced with the result of evaluation

view this post on Zulip Richard Feldman (Dec 20 2023 at 21:40):

yeah I'd like to change this so that we recursively output all the intermediate expressions that go into that final expression

view this post on Zulip Ryan Bates (Dec 20 2023 at 21:40):

@LoipesMas What about placing the evaluated value in each resulting expression?

double [1, 2, 3] = [2,4,7]
[2, 4, 7] == [2, 4, 6] = Bool.False
trim ["", "", ""] = [""]
List.isEmpty [""] = Bool.False

I like this more so I don't have to hunt it down (there could be a lot listed there).

view this post on Zulip Richard Feldman (Dec 20 2023 at 21:42):

we could have some heuristics like "don't bother printing it if it's entirely literals" e.g. [1, 2, 3] because you know what that is since it's right there in the source code

view this post on Zulip LoipesMas (Dec 20 2023 at 22:42):

Maybe a written-out step-by-step "evaluation" would be better?

List.isEmpty (trim ['', '', ''])
List.isEmpty ['']
Bool.False

(with a nice description/formatting/highlighting/syntax)
It would be nice if it could do all substitutions that can be done in a given step at once, for example, instead of

List.isEmpty [a, b, c]
List.isEmpty [1, b, c]
List.isEmpty [1, 2, c]
List.isEmpty [1,2,3]
Bool.False

it could merge some steps

List.isEmpty [a, b, c]
List.isEmpty [1,2,3]
Bool.False

view this post on Zulip Richard Feldman (Dec 20 2023 at 22:56):

I think it would be good to collect some real world examples!

view this post on Zulip Richard Feldman (Dec 20 2023 at 22:56):

like some actual expects you've written, and what it would look like if they failed in certain ways

view this post on Zulip Ryan Bates (Dec 21 2023 at 00:21):

Here are a few examples from advent of code that shows each step of evaluation (evaluating all leaves per step)

expect parseStr stepParser "ab=1" == Ok { label: ['a', 'b'], operation: AddLense 1 }

parseStr <opaque> "ab=1" == (Ok {label: [97, 98], operation: (AddLense 1)})

(Ok {label: [97, 98], operation: (AddLense 0)}) == (Ok {label: [97, 98], operation: (AddLense 1)})

Bool.false

Looks pretty useful.

expect part1 inputExample1 == 62

part1 "R 6 (#70c710)
D 5 (#0dc571)
L 2 (#5713f0)
D 2 (#d2c081)
R 2 (#59c680)
D 2 (#411b91)
L 5 (#8ceee2)
U 2 (#caa173)
L 1 (#1b58a2)
U 2 (#caa171)
R 2 (#7807d2)
U 3 (#a77fa3)
L 2 (#015232)
U 2 (#7a21e3)
" == 62

64 == 62

Bool.false

The long text is noisy but could be useful.

expect
    springs = [OperationalSpring, DamagedSpring, UnknownSpring, UnknownSpring]
    parseRow ".#?? 1,1" == { springs, counts: [1, 1] }

{counts: [1, 2], springs: [OperationalSpring, DamagedSpring, UnknownSpring, UnknownSpring]} == {counts: [1, 1], springs: [OperationalSpring, DamagedSpring, UnknownSpring, UnknownSpring]}

Bool.false

Longer lines can be a bit cumbersome to find the difference, but not bad. I know RSpec highlights the differences when doing equal assertions which is somewhat useful, but has its own issues.

expect
    tiles = parseTiles "S-7\n|.|\nL-J\n"
    expectedTiles = [
        ((0, 0), Start),
        ((1, 0), HorizontalPipe),
        ((2, 0), SouthWestPipe),
        ((0, 1), VerticalPipe),
        ((1, 1), Ground),
        ((2, 1), VerticalPipe),
        ((0, 2), NorthEastPipe),
        ((1, 2), HorizontalPipe),
        ((2, 2), NorthWestPipe),
    ]
    Dict.toList tiles == expectedTiles

Dict.toList {(0, 0): Start, (1, 0): HorizontalPipe, (2, 0): SouthWestPipe, (0, 1): VerticalPipe, (1, 1): Ground, (2, 1): VerticalPipe, (0, 2): NorthEastPipe, (0, 2): HorizontalPipe, (2, 2): NorthWestPipe} == [((0, 0), Start), ((1, 0), HorizontalPipe), ((2, 0), SouthWestPipe), ((0, 1), VerticalPipe), ((1, 1), Ground), ((2, 1), VerticalPipe), ((0, 2), NorthEastPipe), ((1, 2), HorizontalPipe), ((2, 2), NorthWestPipe)]

[((0, 0), Start), ((1, 0), HorizontalPipe), ((2, 0), SouthWestPipe), ((0, 1), VerticalPipe), ((1, 1), Ground), ((2, 1), VerticalPipe), ((0, 2), NorthEastPipe), ((0, 2), HorizontalPipe), ((2, 2), NorthWestPipe)] == [((0, 0), Start), ((1, 0), HorizontalPipe), ((2, 0), SouthWestPipe), ((0, 1), VerticalPipe), ((1, 1), Ground), ((2, 1), VerticalPipe), ((0, 2), NorthEastPipe), ((1, 2), HorizontalPipe), ((2, 2), NorthWestPipe)]

Bool.false

This output is a mess because there's so much data. It would be very difficult to find the issue. I think if the formatting is improved for long lines it could be really nice.

BTW, the output of dbg formatting is different from both roc test and roc format. Perhaps I'll make an issue for this.

view this post on Zulip Richard Feldman (Dec 21 2023 at 01:09):

please do! It's also different from the repl

view this post on Zulip Richard Feldman (Dec 21 2023 at 01:09):

we upgraded dbg to use the Inspect ability but haven't upgraded those others to use it yet

view this post on Zulip Kevin Gillette (Dec 22 2023 at 15:10):

Richard Feldman said:

we could have some heuristics like "don't bother printing it if it's entirely literals" e.g. [1, 2, 3] because you know what that is since it's right there in the source code

That seems magical to me. If someone is learning about expect, they may just try expect 1 == 2 to see what happens. I believe we should have that behave the same as any other expect, rather than a "go find the source to look at the values, because they're too simple to be worth printing here" outcome.

Not everyone uses sophisticated editor integrations, and aside from that, any reasonable opportunity to avoid forcing the user to do more jumping around in the code is a win. Imagine stack traces without function names: technically the user could work it all out from the filenames and line numbers, but their work is easily a few hundred times slower and more tedious in that scenario.


Last updated: Jun 16 2026 at 16:19 UTC