It's great to be able to see all the values going through a pipeline with minimal effort, so I propose supporting dbg like this:
List.range { start: At 1, end: At 10 }
|> List.keepIf \x -> x % 3 == 0 || x % 5 == 0
|> List.sum
|> Num.toStr
|> dbg
Which would output:
[helloWorld.roc 7:9] List.range { start: At 1, end: At 10 } # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[helloWorld.roc 8:9] |> List.keepIf \x -> x % 3 == 0 || x % 5 == 0 # [3, 5, 6, 9, 10]
[helloWorld.roc 9:9] |> List.sum # 33
[helloWorld.roc 10:9] |> Num.toStr # "33"
The |> dbg is not super logical in the sense that it does not just dbg print the output of the previous step, but all alternatives I could think of are more cumbersome.
This proposal is extremely similar to how elixir does this:
__ENV__.file
|> String.split("/", trim: true)
|> List.last()
|> File.exists?()
|> dbg()
I would prefer it just print the previous step. Or it be a different keyword for the entire pipeline
A lot of the time, once don't with one part of a pipeline you don't want to see it anymore in debug. Would just clutter the output
Or it be a different keyword for the entire pipeline
Uhu, I was unsure about this as well, the behavior is different, so a different keyword may be better, could be dbgPipe or dbgPipeline.
I would prefer it just print the previous step.
This could be done with a dbg that both prints and returns its input which is how Elm does it, so I wonder why we diverged from it here.
in Elm it's common to do let _ = Debug.log ... in order to approximate the "statement" version of dbg
so I knew there was at least demand for that, and it would unblock people being able to do debug logging in general, so I figured we should start with that and see if there's demand for the expression version too
(turns out there is!)
personally I like the idea of "dbg can be used as a statement or as an expression, including in a pipeline"
but not actually having it be a function - e.g. you can't do List.map list dbg
but you can do List.map (dbg list) \elem -> ...
or |> dbg (which prints the previous answer in the pipeline)
It might be worth clarifying that dbg in elixir is not just a debugger function that works with pipelines. It's a customizable macro with multiple implementations. The default implementation does the pipeline thing, but it also prints the code getting inspected so that you don't have to write throwaway "got here" labels at each debug location. Roc already provides a file/line number, but it would be a bit friendlier to do something like [helloWorld.roc 10:9] foo # "foo value" for dbg foo. I think this is how the pipeline printing feature came about, because if you have
foo
|> Str.toUtf8
|> List.len
|> dbg
what would you print?
[helloWorld.roc 10:9] List.len # 9
[helloWorld.roc 10:9] |> List.len # 9
[helloWorld.roc 7:1 to 10:9] foo |> Str.toUtf8 |> List.len # 9
etc.
In Elixir's case the AST is a manipulable value so it's reasonable to print the whole thing. I'm not sure about Roc.
The other two implementations of dbg in elixir I'm aware of are dropping into a REPL at the point in the code where dbg is called, and manipulating pipeline outputs in LiveBook (scroll down to "Interactive user interface to visualize and edit Elixir pipelines").
That's really good background information, thanks! I had forgotten that Elixir had that LISP-like property where you can manipulate the AST. Roc doesn't have that.
We'd have to implement it as a special desugaring step during parsing, I think.
It would insert an implicit dbg expression at each level of the pipeline
Dropping into the REPL sounds like an excellent feature! We'd presumably need a different name like dbg_repl or something.
My guess is that the hard part of that would be performing effects from custom platforms. Today the REPL implementation uses its own special platform.
And also every new line of code you enter is a new program that we compile and run before printing the final value. So effects would be performed again every time.
Last updated: Jun 16 2026 at 16:19 UTC