So I wanted to share a quick update now that #6634 is merged.
It's still early and there are definitely bugs we need to find and fix. But I wanted to give an example of how you can now use the proposed Chaining Syntax features in a script.
If you use the !
syntax and find any issues, I would really love it if you could make an issue and let me know. :pray:
Here is an example; this is our bash script from www/build-dev-local.sh
that builds and serves the roc website for local development. Let's re-write it in roc! :rock_on:
#!/usr/bin/env bash
# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
set -euxo pipefail
# Use this script to for testing the WIP site locally without downloading assets every time.
# NOTE run `bash www/build.sh` to cache local copy of fonts, and repl assets etc
## Get the directory of the currently executing script
DIR="$(dirname "$0")"
# Change to that directory
cd "$DIR" || exit
rm -rf dist/
cp -r build dist/
cp -r public/* dist/
roc run main.roc -- content/ dist/
simple-http-server -p 8080 --nocache --cors --index -- dist/
Using the new syntax sugar for !
, we can now write it like so.
app "roc-website-dev"
packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.9.0/oKWkaruh2zXxin_xfsYsCJobH1tO8_JvNkFzDwwzNUQ.tar.br" }
imports [pf.Stdout.{ line }, pf.Stderr, pf.Path, pf.Cmd, pf.Task.{ Task }]
provides [main] to pf
main = run |> Task.onErr handlErr
handlErr = \err ->
Stderr.line! "SCRIPT ERROR $(Inspect.toStr err)"
Task.err 1
run =
removeDir! "www/dist/"
copyFiles! "www/build/" "www/dist/"
copyFiles! "www/public/" "www/dist/"
generateSiteContent! "www/content/"
serveFiles!
verifyDirExists = \path ->
path
|> Path.fromStr
|> Path.isDir
|> Task.map \_ -> {}
|> Task.onErr \err -> Task.err (DirDoesntExist err path)
removeDir = \path ->
verifyDirExists! path
line! "Removing files from $(path)..."
Cmd.new "rm"
|> Cmd.arg "-rf"
|> Cmd.arg path
|> Cmd.status
|> Task.mapErr ErrRemovingDir
copyFiles = \from, to ->
verifyDirExists! from
line! "Copying files from $(from) to $(to)..."
Cmd.new "cp"
|> Cmd.args ["-r", from, to]
|> Cmd.status
|> Task.mapErr ErrCopyingFiles
generateSiteContent = \path ->
verifyDirExists! path
line! "Generating static site..."
Cmd.new "roc"
|> Cmd.args ["www/main.roc", "--", "www/content/", "www/dist/"]
|> Cmd.status
|> Task.mapErr ErrBuildingSite
serveFiles =
line! "Serving static site..."
Cmd.new "simple-http-server"
|> Cmd.args ["-p", "8080", "--nocache", "--cors", "--index", "--", "www/dist/"]
|> Cmd.status
|> Task.mapErr ErrServingFiles
Although the roc version is a little longer, I find it much easier to follow what is happening.
I think using helpers like this is much easier to compose, and gives nice error messages.
Let me know what you think?
Sweet! The new syntax looks very clean
Looks good! It could be nice to create a function like this to remove some boilerplate:
cmd
"roc"
["www/main.roc", "--", "www/content/", "www/dist/"]
ErrBuildingSite
Anton said:
It could be nice to create a function like this to remove some boilerplate:
see: this topic: basic-cli Cmd api
Luke Boswell said:
verifyDirExists = \path -> path |> Path.fromStr |> Path.isDir |> Task.map \_ -> {} |> Task.onErr \err -> Task.err (DirDoesntExist err path)
Does it mean you ignore the result of Path.isDir
? Should the function return Task.err
if is not a directory?
I was playing with this new syntax, trying to modify the verifyDirExists
, but I am not sure if this is correct:
verifyDirExists = \path ->
isDir =
Path.fromStr path
|> Path.isDir
|> Task.attempt! # <------ "!" goes here?
when isDir is
Ok true -> Task.succeed {}
Ok false -> Task.err (DirProblem path "not a directory")
Err err -> Task.err (DirProblem path err
or
verifyDirExists = \path ->
isDir =
Path.fromStr path
|> Path.isDir! # <------ or "!" goes here?
|> Task.attempt
when isDir is
Ok true -> Task.succeed {}
Ok false -> Task.err (DirProblem path "not a directory")
Err err -> Task.err (DirProblem path err
or maybe this is correct?
verifyDirExists = \path ->
isDir =
Path.fromStr path
|> Path.isDir
|> Task.attempt
when isDir! is # <------ "!" goes here?
Ok true -> Task.succeed {}
Ok false -> Task.err (DirProblem path "not a directory")
Err err -> Task.err (DirProblem path err
My question is where are the proper places for the !
?
Should be after the Task.attempt
Though. Technically should also be valid in the when isDir! is
Oh sorry, one edit
It can't be used with Task.attempt
at all. You need something like:
Task.toResult : Task ok err -> Task (Result ok err) []
Not sure if that exists yet. Probably needs to be added to basic cli
That would replace Task.attempt
and the bang would go after it
oh yeah we should add that!
If we are going all in on !
, should we actually steal Task.attempt
. Wondering if |> Task.attempt!
reads better than |> Task.toResult!
. Or maybe there is a better third name
You are so right, Task.attempt won't work with !
unless it returns a Task, the attempt was meant to work with regular callback/backpassing. Task.toResult
is not bad, but it can mislead, because, after all, it still returns a Task.
yeah, naming is hard.
Task.asResult
:thinking:
Silly/naive name or going after math/category theory to resolve the problem :upside_down:
I can't remember reading about "moniods, functors, monads" in here, so probably first option...
Personally, if we are planning to remove backpassing long term, Task.attempt
is no longer useful. So stealling Task.attempt
makes most sense to me. That said, these are all readable enough:
1)
isDir =
Path.fromStr path
|> Path.isDir
|> Task.attempt!
2)
isDir =
Path.fromStr path
|> Path.isDir
|> Task.asResult!
3)
isDir =
Path.fromStr path
|> Path.isDir
|> Task.toResult!
I like (1) the most.
I'll make a PR for basic-cli and basic-webserver
:thank_you:
I find the Task.attempt
can be somewhat confusing, especially for those not used to always check the type signatures first.
The problem with this word is that it implies that it is the thing that actually puts a task in motion. Like if you won't "attempt" nothing will ever happen.
The Task.asResult
or .toResult
are a little bit better, but they are not accurate, because they still return a Task
.
So, the question I am asking myself is what do we need that function for in the first place? Result
entanglement is just a mean to an end.
In my case (the verifyDirExists
function) I had to use it to be able to… inspect the result, so maybe this is the direction for a good, self describing name.
Brendan Hansknecht said:
yeah, naming is hard.
It is! :smile:
Putting another naming idea in the hat: how about Task.awaitResult
?
Task.resultify
:stuck_out_tongue:
Task.toResultTask
actually says what it does, but I'd still rather Task.toResult
for brevity. I agree that Task.attempt
is really confusing. Coming from other languages I would think that runs the task, waits for it to finish, and then returns it's result.
How does this look?
Screenshot-2024-04-17-at-21.36.32.png
I don’t love toResult
because it implies it would return a Result
instead of a Task (Result …) …
I've added this PR basic-cli#182
What about withResult
?
What about just Task.result
My inspiration for the shorter result
is just that it's shorter, though unfortunately just as vague. I think there is no way around the fact that users will have to get familiar with Tasks.
And another feature of the Chaining Syntax (that's not yet implemented) will enable us to do this
main =
when checkFile "bad" |> result! is
Ok Good -> Stdout.line "GOOD"
Ok Bad -> Stdout.line "BAD"
Err IOError -> Stdout.line "IOError"
Instead of
main =
result = checkFile "bad" |> Task.toResult!
when result is
Ok Good -> Stdout.line "GOOD"
Ok Bad -> Stdout.line "BAD"
Err IOError -> Stdout.line "IOError"
If I'm naming the intermediate variable here, I'll reach for result
, so maybe that's an indicator for a decent name?
Yeah, I like Task.result
I like Task.result
the best of the ones we've discussed too! :thumbs_up:
@Joshua Warner
Are you familiar with what changes we made to the formatter regarding the below? Is it just a switch to undo the indenting thing?
Richard Feldman said: I forget where we discussed this, but I think in the world of
!
we want to go back to indenting|>
(and other operators like+
for consistency) because otherwise they look weird in statements, e.g.
foo =
File.writeUtf8 foo bar
|> Task.onErr! Blah
File.writeUtf8 baz etc
|> Task.onErr! Baz
vs.
foo =
File.writeUtf8 foo bar
|> Task.onErr! Blah
File.writeUtf8 baz etc
|> Task.onErr! Baz
I guess I don't feel strongly on the formatting; I just remember Richard expressing the opposite opinion about how pizza operators should be formatted before.
I think I prefer the way it currently is. I've thought it might be nice to have the formatter auto add new lines between statements like is displayed here. But I don't feel strongly about it either.
I prefer the way it currently is too
interesting - you prefer it even in the "statement" case in the example above?
I definitely have grown to like consistent pipeline with no indentation. I even move things to new lines to avoid indenting pipeline
example
I would really dislike if we force indenting all pipeline even those that are unrelated to tasks.
example
As for the specific example given above. I honestly think that the |>
might be enough visual indentation for the reader. So I am inclined to say that we should leave it alone. I generally prefer flat over indentation, so clearly have a bias. As long as it is has a |>
on each following line and an empty line between each task, my gut feeling is that it will read fine.
cool, I'm game to try leaving it as-is and see how it feels! :+1:
Last updated: Jul 05 2025 at 12:14 UTC