Stream: show and tell

Topic: Initial Chaining Syntax


view this post on Zulip Luke Boswell (Apr 16 2024 at 03:38):

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?

view this post on Zulip Isaac Van Doren (Apr 16 2024 at 04:38):

Sweet! The new syntax looks very clean

view this post on Zulip Anton (Apr 16 2024 at 09:08):

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

view this post on Zulip witoldsz (Apr 16 2024 at 11:07):

Anton said:

It could be nice to create a function like this to remove some boilerplate:

see: this topic: basic-cli Cmd api

view this post on Zulip witoldsz (Apr 16 2024 at 11:13):

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?

view this post on Zulip witoldsz (Apr 16 2024 at 14:18):

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

view this post on Zulip Brendan Hansknecht (Apr 16 2024 at 16:08):

Should be after the Task.attempt

view this post on Zulip Brendan Hansknecht (Apr 16 2024 at 16:09):

Though. Technically should also be valid in the when isDir! is

view this post on Zulip Brendan Hansknecht (Apr 16 2024 at 16:09):

Oh sorry, one edit

view this post on Zulip Brendan Hansknecht (Apr 16 2024 at 16:11):

It can't be used with Task.attempt at all. You need something like:
Task.toResult : Task ok err -> Task (Result ok err) []

view this post on Zulip Brendan Hansknecht (Apr 16 2024 at 16:11):

Not sure if that exists yet. Probably needs to be added to basic cli

view this post on Zulip Brendan Hansknecht (Apr 16 2024 at 16:12):

That would replace Task.attempt and the bang would go after it

view this post on Zulip Richard Feldman (Apr 16 2024 at 16:14):

oh yeah we should add that!

view this post on Zulip Brendan Hansknecht (Apr 16 2024 at 16:44):

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

view this post on Zulip witoldsz (Apr 16 2024 at 19:48):

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.toResultis not bad, but it can mislead, because, after all, it still returns a Task.

view this post on Zulip Brendan Hansknecht (Apr 16 2024 at 19:49):

yeah, naming is hard.

view this post on Zulip witoldsz (Apr 16 2024 at 19:54):

Task.asResult :thinking:

view this post on Zulip witoldsz (Apr 16 2024 at 20:01):

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

view this post on Zulip Brendan Hansknecht (Apr 16 2024 at 20:06):

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!

view this post on Zulip Luke Boswell (Apr 17 2024 at 04:13):

I like (1) the most.

view this post on Zulip Luke Boswell (Apr 17 2024 at 04:25):

I'll make a PR for basic-cli and basic-webserver

view this post on Zulip Brendan Hansknecht (Apr 17 2024 at 04:51):

:thank_you:

view this post on Zulip witoldsz (Apr 17 2024 at 08:25):

I find the Task.attemptcan 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:

view this post on Zulip Jasper Woudenberg (Apr 17 2024 at 09:26):

Putting another naming idea in the hat: how about Task.awaitResult?

view this post on Zulip Richard Feldman (Apr 17 2024 at 10:35):

Task.resultify :stuck_out_tongue:

view this post on Zulip Eli Dowling (Apr 17 2024 at 11:02):

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.

view this post on Zulip Luke Boswell (Apr 17 2024 at 11:36):

How does this look?

Screenshot-2024-04-17-at-21.36.32.png

view this post on Zulip Agus Zubiaga (Apr 17 2024 at 11:47):

I don’t love toResult because it implies it would return a Result instead of a Task (Result …) …

view this post on Zulip Luke Boswell (Apr 17 2024 at 11:47):

I've added this PR basic-cli#182

view this post on Zulip Agus Zubiaga (Apr 17 2024 at 11:47):

What about withResult?

view this post on Zulip Luke Boswell (Apr 17 2024 at 11:49):

What about just Task.result

view this post on Zulip Luke Boswell (Apr 17 2024 at 11:51):

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.

view this post on Zulip Luke Boswell (Apr 17 2024 at 11:53):

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"

view this post on Zulip Luke Boswell (Apr 17 2024 at 11:54):

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"

view this post on Zulip Luke Boswell (Apr 17 2024 at 11:56):

If I'm naming the intermediate variable here, I'll reach for result, so maybe that's an indicator for a decent name?

view this post on Zulip Agus Zubiaga (Apr 17 2024 at 12:02):

Yeah, I like Task.result

view this post on Zulip Richard Feldman (Apr 17 2024 at 12:52):

I like Task.result the best of the ones we've discussed too! :thumbs_up:

view this post on Zulip Luke Boswell (Apr 20 2024 at 03:21):

@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

view this post on Zulip Joshua Warner (Apr 20 2024 at 03:25):

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.

view this post on Zulip Luke Boswell (Apr 20 2024 at 03:31):

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.

view this post on Zulip Isaac Van Doren (Apr 20 2024 at 04:08):

I prefer the way it currently is too

view this post on Zulip Richard Feldman (Apr 20 2024 at 10:58):

interesting - you prefer it even in the "statement" case in the example above?

view this post on Zulip Brendan Hansknecht (Apr 20 2024 at 14:38):

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.

view this post on Zulip Richard Feldman (Apr 20 2024 at 15:18):

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