Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade examples to snake_case builtins and PNC #228

Merged
merged 44 commits into from
Jan 31, 2025
Merged
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
65ac721
update flake
lukewilliamboswell Jan 9, 2025
d854efc
upgrade Arithmetic example
lukewilliamboswell Jan 9, 2025
28580c9
update BaseDict example
lukewilliamboswell Jan 9, 2025
a269966
upgrade CommandLineArgs example
lukewilliamboswell Jan 9, 2025
175dad5
upgrade CommandLineArgsFile example
lukewilliamboswell Jan 9, 2025
25d693f
upgrade CustomInspect example
lukewilliamboswell Jan 9, 2025
4c6cb23
upgrade DesugaringTry example
lukewilliamboswell Jan 9, 2025
3b35581
upgrade ElmWebApp example
lukewilliamboswell Jan 9, 2025
41ec5fe
upgrade EncodeDecode example
lukewilliamboswell Jan 9, 2025
4ef372c
upgrade ErrorHandling example
lukewilliamboswell Jan 10, 2025
21e1778
upgrade FizzBuzz example
lukewilliamboswell Jan 10, 2025
21832b9
upgrade GraphTraversal example
lukewilliamboswell Jan 10, 2025
14fd1f8
upgrade HelloWeb example
lukewilliamboswell Jan 10, 2025
b6b537a
upgrade HelloWorld example
lukewilliamboswell Jan 10, 2025
0002342
upgrade ImportFromDirectory example
lukewilliamboswell Jan 10, 2025
ea97f2b
upgrade IngestFiles example
lukewilliamboswell Jan 10, 2025
495b6d1
upgrade Json example
lukewilliamboswell Jan 10, 2025
5cad4f1
upgrade LeastSquares example
lukewilliamboswell Jan 10, 2025
b52dfd7
upgrade LoopEffect example
lukewilliamboswell Jan 10, 2025
a46415c
upgrade MultipleRocFiles example
lukewilliamboswell Jan 10, 2025
d988ca1
upgrade Parser example
lukewilliamboswell Jan 10, 2025
ffdb27d
upgrade PatternMatching example
lukewilliamboswell Jan 10, 2025
ef6ada7
upgrade RandomNumbers example
lukewilliamboswell Jan 10, 2025
0ed2054
upgrade RecordBuilder example
lukewilliamboswell Jan 10, 2025
4b5145c
upgrade Results example
lukewilliamboswell Jan 10, 2025
89600f7
upgrade SafeMath example
lukewilliamboswell Jan 10, 2025
620dc41
upgrade TowersOfHanoi example
lukewilliamboswell Jan 10, 2025
76f9cd8
upgrade Tuples example
lukewilliamboswell Jan 10, 2025
4edf0d3
update .net and go examples
Anton-4 Jan 10, 2025
0982e9c
improved arithmetic example
Anton-4 Jan 10, 2025
ce1043a
update flake
lukewilliamboswell Jan 11, 2025
011430c
examples updates, improvements
Anton-4 Jan 11, 2025
29199af
improvements and updates
Anton-4 Jan 11, 2025
c97e12b
Merge branch 'snake_case_builtins' of github.com:roc-lang/examples in…
lukewilliamboswell Jan 12, 2025
0a4fb38
new string interpolation syntax
lukewilliamboswell Jan 12, 2025
3742337
Update lambdas and Result.map
smores56 Jan 24, 2025
bff5941
update
Anton-4 Jan 24, 2025
4e7393d
use roc-random url
Anton-4 Jan 24, 2025
9496ef3
Fixed DesugaringTry
Anton-4 Jan 24, 2025
8ab67f1
fix EncodeDecode
lukewilliamboswell Jan 25, 2025
8f42d20
re-enable EncodeDecode, add final printout for all_tests.sh
lukewilliamboswell Jan 25, 2025
40de926
basic-webserver link
Anton-4 Jan 25, 2025
58d7613
basic-cli link update
Anton-4 Jan 28, 2025
21c3cf9
basic-ws link update
Anton-4 Jan 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion ci_scripts/all_tests.sh
Original file line number Diff line number Diff line change
@@ -98,7 +98,7 @@ $ROC test ./examples/CustomInspect/OpaqueTypes.roc
if [[ "$(uname)" != "Darwin" ]]; then
$ROC build --lib ./examples/GoPlatform/main.roc --output examples/GoPlatform/platform/libapp.so
go build -C examples/GoPlatform/platform -buildmode=pie -o dynhost

$ROC preprocess-host ./examples/GoPlatform/platform/dynhost ./examples/GoPlatform/platform/main.roc ./examples/GoPlatform/platform/libapp.so
$ROC build ./examples/GoPlatform/main.roc

@@ -119,3 +119,5 @@ if [[ "$(uname)" != "Darwin" ]]; then
$ROC build ./examples/DotNetPlatform/main.roc --lib --output ./examples/DotNetPlatform/platform/interop
expect ci_scripts/expect_scripts/DotNetPlatform.exp
fi

echo "All tests passed!"
2 changes: 1 addition & 1 deletion ci_scripts/expect_scripts/MultipleRocFiles.exp
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ set timeout 7

spawn ./examples/MultipleRocFiles/main

expect "Hello World from interface!\r\n" {
expect "Hello World from module!\r\n" {
expect eof
exit 0
}
69 changes: 38 additions & 31 deletions examples/Arithmetic/main.roc
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br" }
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br" }

import pf.Stdout
import pf.Arg exposing [Arg]
import cli.Stdout
import cli.Arg exposing [Arg]

main! : List Arg.Arg => Result {} _
main! = \raw_args ->
main! = |raw_args|

args : { a : I32, b : I32 }
args = try read_args raw_args
args = read_args(raw_args)?

result =
[
@@ -16,44 +16,51 @@ main! = \raw_args ->
("product", args.a * args.b),
("integer quotient", args.a // args.b),
("remainder", args.a % args.b),
("exponentiation", Num.powInt args.a args.b),
("exponentiation", Num.pow_int(args.a, args.b)),
]
|> List.map \(operation, answer) ->
answer_str = Num.toStr answer
|> List.map(
|(operation, answer)| "${operation}: ${Num.to_str(answer)}",
)
|> Str.join_with("\n")

"$(operation): $(answer_str)"
|> Str.joinWith "\n"

Stdout.line! result
Stdout.line!(result)

## Reads two command-line arguments, attempts to parse them as `I32` numbers,
## and returns a task containing a record with two fields, `a` and `b`, holding
## the parsed `I32` values.
## and returns a [`Result`](https://www.roc-lang.org/builtins/Result).
##
## On success, the [`Result`](https://www.roc-lang.org/builtins/Result)
## will contain a record with two fields, `a` and `b`, holding the parsed `I32` values.
##
## If the arguments are missing, if there's an issue with parsing the arguments
## as `I32` numbers, or if the parsed numbers are outside the expected range
## (-1000 to 1000), the function will return a task that fails with an
## error `InvalidArg` or `InvalidNumStr`.
## This will fail if an argument is missing, if there's an issue with parsing
## the arguments as `I32` numbers, or if the parsed numbers are outside the
## expected range (-1000 to 1000). Then the [`Result`](https://www.roc-lang.org/builtins/Result) will contain
## an `Exit I32 Str` error.
read_args : List Arg -> Result { a : I32, b : I32 } [Exit I32 Str]
read_args = \raw_args ->
read_args = |raw_args|
arg_range_min = -1000
arg_range_max = 1000
expected_nr_of_args = 2 + 1 # +1 because first will be name or path of the program

invalid_args = Exit 1 "Error: Please provide two integers between -1000 and 1000 as arguments."
invalid_num_str = Exit 1 "Error: Invalid number format. Please provide integers between -1000 and 1000."
arg_range_min_str = Inspect.to_str(arg_range_min)
arg_range_max_str = Inspect.to_str(arg_range_max)
# TODO this function should not use Exit for modularity. Only perform this change after static dispatch has landed!
invalid_args = Exit(1, "Error: Please provide two integers between ${arg_range_min_str} and ${arg_range_max_str} as arguments.")
invalid_num_str = Exit(1, "Error: Invalid number format. Please provide integers between ${arg_range_min_str} and ${arg_range_max_str}.")

args =
if List.len raw_args != 3 then
return Err invalid_args
if List.len(raw_args) != expected_nr_of_args then
return Err(invalid_args)
else
List.map raw_args Arg.display
List.map(raw_args, Arg.display)

a_result = List.get args 1 |> Result.try Str.toI32
b_result = List.get args 2 |> Result.try Str.toI32
a_result = List.get(args, 1) |> Result.try(Str.to_i32)
b_result = List.get(args, 2) |> Result.try(Str.to_i32)

when (a_result, b_result) is
(Ok a, Ok b) ->
if a < -1000 || a > 1000 || b < -1000 || b > 1000 then
Err invalid_num_str
(Ok(a), Ok(b)) ->
if a < arg_range_min or a > arg_range_max or b < arg_range_min or b > arg_range_max then
Err(invalid_num_str)
else
Ok { a, b }
Ok({ a, b })

_ -> Err invalid_num_str
_ -> Err(invalid_num_str)
36 changes: 18 additions & 18 deletions examples/BasicDict/BasicDict.roc
Original file line number Diff line number Diff line change
@@ -6,56 +6,56 @@ module []
# Below we use a Str key for the fruit name, and a U64 value for the fruit count.
fruit_dict : Dict Str U64
fruit_dict =
Dict.empty {}
|> Dict.insert "Apple" 3
|> Dict.insert "Banana" 2
Dict.empty({})
|> Dict.insert("Apple", 3)
|> Dict.insert("Banana", 2)

expect
# get the value for a key
# Dict.get returns a Result with either `Ok value` or `Err KeyNotFound`
Dict.get fruit_dict "Apple" == (Ok 3)
Dict.get(fruit_dict, "Apple") == (Ok(3))

expect
# get the length (number of key-value pairs) of a Dict
Dict.len fruit_dict == 2
Dict.len(fruit_dict) == 2

expect
# convert Dict to a Str
Inspect.toStr fruit_dict == "{\"Apple\": 3, \"Banana\": 2}"
Inspect.to_str(fruit_dict) == "{\"Apple\": 3, \"Banana\": 2}"

expect
# get all the keys
Dict.keys fruit_dict == ["Apple", "Banana"]
Dict.keys(fruit_dict) == ["Apple", "Banana"]

expect
# get all the values
Dict.values fruit_dict == [3, 2]
Dict.values(fruit_dict) == [3, 2]

expect
# convert to a list of tuples
Dict.toList fruit_dict == [("Apple", 3), ("Banana", 2)]
Dict.to_list(fruit_dict) == [("Apple", 3), ("Banana", 2)]

expect
# remove a key-value pair
Dict.remove fruit_dict "Apple"
|> Dict.remove "Banana"
|> Dict.isEmpty
Dict.remove(fruit_dict, "Apple")
|> Dict.remove("Banana")
|> Dict.is_empty

expect
# update the value of a Dict
updated_dict =
Dict.update fruit_dict "Apple" add_fruit
Dict.update(fruit_dict, "Apple", add_fruit)

# We need to account for the case when a key (=fruit) is not in the Dict.
# So we need a function like this:
add_fruit : Result U64 [Missing] -> Result U64 [Missing]
add_fruit = \value_tag ->
add_fruit = |value_tag|
when value_tag is
# If the fruit is not in the dict (=missing), we set the count to 1
Err Missing -> Ok 1
# If the fruit is in the dict (=present), we increase the count
Ok count -> Ok (count + 1)
Err(Missing) -> Ok(1)
# If the fruit is in the dict, we increase the count
Ok(count) -> Ok((count + 1))

Dict.get updated_dict "Apple" == (Ok 4)
Dict.get(updated_dict, "Apple") == (Ok(4))

# see https://www.roc-lang.org/builtins/Dict for more
10 changes: 5 additions & 5 deletions examples/BasicDict/README.md
Original file line number Diff line number Diff line change
@@ -10,11 +10,11 @@ A `Dict` (dictionary) lets you save a value under a key, so that you end up with
For example, you can create a Dict to keep track of how much fruit you have:

```roc
fruitDict : Dict Str U64
fruitDict =
Dict.empty {}
|> Dict.insert "Apple" 3
|> Dict.insert "Banana" 2
fruit_dict : Dict Str U64
fruit_dict =
Dict.empty({})
|> Dict.insert("Apple", 3)
|> Dict.insert("Banana", 2)
```

## Basic Dict Examples
21 changes: 10 additions & 11 deletions examples/CommandLineArgs/main.roc
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
# Run with `roc ./examples/CommandLineArgs/main.roc some_argument`
# !! This currently does not work in combination with --linker=legacy, see https://github.com/roc-lang/basic-cli/issues/82
app [main!] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br",
cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br",
}

import pf.Stdout
import pf.Arg
import cli.Stdout
import cli.Arg

main! = \raw_args ->
args = List.map raw_args Arg.display
main! = |raw_args|
args = List.map(raw_args, Arg.display)

# get the second argument, the first is the executable's path
arg_result = List.get args 1 |> Result.mapErr (\_ -> ZeroArgsGiven)
arg_result = List.get(args, 1) |> Result.map_err(ZeroArgsGiven)

when arg_result is
Err ZeroArgsGiven ->
Err (Exit 1 "Error ZeroArgsGiven:\n\tI expected one argument, but I got none.\n\tRun the app like this: `roc main.roc -- input.txt`")
Err(ZeroArgsGiven(_)) ->
Err(Exit(1, "Error ZeroArgsGiven:\n\tI expected one argument, but I got none.\n\tRun the app like this: `roc main.roc -- input.txt`"))

Ok first_argument ->
Stdout.line! "received argument: $(first_argument)"
Ok(first_argument) ->
Stdout.line!("received argument: ${first_argument}")
45 changes: 22 additions & 23 deletions examples/CommandLineArgsFile/main.roc
Original file line number Diff line number Diff line change
@@ -1,44 +1,43 @@
# Run with `roc ./examples/CommandLineArgsFile/main.roc -- examples/CommandLineArgsFile/input.txt`
# This currently does not work in combination with --linker=legacy, see https://github.com/roc-lang/basic-cli/issues/82
app [main!] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br",
cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br",
}

import pf.Stdout
import pf.Path exposing [Path]
import pf.Arg
import cli.Stdout
import cli.Path exposing [Path]
import cli.Arg

main! = \raw_args ->
main! = |raw_args|

# read all command line arguments
args = List.map raw_args Arg.display
args = List.map(raw_args, Arg.display)

# get the second argument, the first is the executable's path
arg_result = List.get args 1 |> Result.mapErr \_ -> ZeroArgsGiven
arg_result = List.get(args, 1) |> Result.map_err(ZeroArgsGiven)

when arg_result is
Ok arg ->
file_content_str = try read_file_to_str! (Path.from_str arg)
Ok(arg) ->
file_content_str = read_file_to_str!(Path.from_str(arg))?

Stdout.line! "file content: $(file_content_str)"
Stdout.line!("file content: ${file_content_str}")
Err ZeroArgsGiven ->
Err (Exit 1 "Error ZeroArgsGiven:\n\tI expected one argument, but I got none.\n\tRun the app like this: `roc main.roc -- path/to/input.txt`")
Err(ZeroArgsGiven(_)) ->
Err(Exit(1, "Error ZeroArgsGiven:\n\tI expected one argument, but I got none.\n\tRun the app like this: `roc main.roc -- path/to/input.txt`"))
# reads a file and puts all lines in one Str
read_file_to_str! : Path => Result Str [ReadFileErr Str]_
read_file_to_str! = \path ->
read_file_to_str! = |path|
path
|> Path.read_utf8!
|> Result.mapErr \file_read_err ->
path_str = Path.display path
|> Result.map_err(
|file_read_err|
path_str = Path.display(path)
when file_read_err is
FileReadErr _ read_err ->
read_err_str = Inspect.toStr read_err
when file_read_err is
FileReadErr(_, read_err) ->
ReadFileErr("Failed to read file:\n\t${path_str}\nWith error:\n\t${Inspect.to_str(read_err)}")

ReadFileErr "Failed to read file:\n\t$(path_str)\nWith error:\n\t$(read_err_str)"

FileReadUtf8Err _ _ ->
ReadFileErr "I could not read the file:\n\t$(path_str)\nIt contains charcaters that are not valid UTF-8:\n\t- Check if the file is encoded using a different format and convert it to UTF-8.\n\t- Check if the file is corrupted.\n\t- Find the characters that are not valid UTF-8 and fix or remove them."
FileReadUtf8Err(_, _) ->
ReadFileErr("I could not read the file:\n\t${path_str}\nIt contains charcaters that are not valid UTF-8:\n\t- Check if the file is encoded using a different format and convert it to UTF-8.\n\t- Check if the file is corrupted.\n\t- Find the characters that are not valid UTF-8 and fix or remove them."),
)
26 changes: 13 additions & 13 deletions examples/CustomInspect/OpaqueTypes.roc
Original file line number Diff line number Diff line change
@@ -7,28 +7,28 @@ Color := [
Blue,
]
implements [
Inspect { toInspector: color_inspector },
Inspect { to_inspector: color_inspector },
]

color_inspector : Color -> Inspector f where f implements InspectFormatter
color_inspector = \@Color color ->
color_inspector : Color -> Inspector fmt where fmt implements InspectFormatter
color_inspector = |@Color(color)|
when color is
Red -> Inspect.str "_RED_"
Green -> Inspect.str "_GREEN_"
Blue -> Inspect.str "_BLUE_"
Red -> Inspect.str("_RED_")
Green -> Inspect.str("_GREEN_")
Blue -> Inspect.str("_BLUE_")

expect Inspect.toStr (@Color Red) == "\"_RED_\""
expect Inspect.toStr (@Color Green) == "\"_GREEN_\""
expect Inspect.toStr (@Color Blue) == "\"_BLUE_\""
expect Inspect.to_str(@Color(Red)) == "\"_RED_\""
expect Inspect.to_str(@Color(Green)) == "\"_GREEN_\""
expect Inspect.to_str(@Color(Blue)) == "\"_BLUE_\""
### end snippet color

### start snippet secret
MySecret := Str implements [
Inspect { toInspector: my_secret_inspector },
Inspect { to_inspector: my_secret_inspector },
]

my_secret_inspector : MySecret -> Inspector f where f implements InspectFormatter
my_secret_inspector = \@MySecret _ -> Inspect.str "******* REDACTED *******"
my_secret_inspector : MySecret -> Inspector fmt where fmt implements InspectFormatter
my_secret_inspector = |@MySecret(_)| Inspect.str("******* REDACTED *******")
expect Inspect.toStr (@MySecret "password1234") == "\"******* REDACTED *******\""
expect Inspect.to_str(@MySecret("password1234")) == "\"******* REDACTED *******\""
### end snippet secret
22 changes: 12 additions & 10 deletions examples/DesugaringTry/README.md
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@
convenient way without adding new functionality to the language itself.
</details>

Desugaring converts syntax sugar (like `x + 1`) into more fundamental operations (like `Num.add x 1`).
Desugaring converts syntax sugar (like `x + 1`) into more fundamental operations (like `Num.add(x, 1)`).

Let's see how `?` is desugared. In this example we will extract the name and birth year from a
string like `"Alice was born in 1990"`.
@@ -18,18 +18,20 @@ file:main.roc:snippet:question

After desugaring, this becomes:
```roc
file:main.roc:snippet:try
file:main.roc:snippet:desugared
```

[Result.try](https://www.roc-lang.org/builtins/Result#try) takes the success
value from a given Result and uses that to generate a new Result.
It's type is `Result a err, (a -> Result b err) -> Result b err`.
So `birth_year = Str.to_u16(birth_year_str)?` is converted to

`birthYear = Str.toU16? birthYearStr` is converted to `Str.toU16 birthYearStr |> Result.try \birthYear ->`.
```roc
when Str.to_u16(birth_year_str) is
Err(err2) -> return Err(err2)
Ok(birth_year) -> birth_year
```
As you can see, the first version is a lot nicer!

Thanks to `?`, you can write code in a mostly familiar way while also getting the benefits of Roc's
error handling.
Thanks to `?`, you can write code in a familiar way and you get the benefits of Roc's
error handling to drastically reduce the likelihood of crashes.

## Full Code
```roc
@@ -42,6 +44,6 @@ Run this from the directory that has `main.roc` in it:

```
$ roc main.roc
(Ok {birthYear: 1990, name: "Alice"})
(Ok {birthYear: 1990, name: "Alice"})
Ok({birth_year: 1990, name: "Alice"})
Ok({birth_year: 1990, name: "Alice"})
```
52 changes: 27 additions & 25 deletions examples/DesugaringTry/main.roc
Original file line number Diff line number Diff line change
@@ -1,33 +1,35 @@
app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br" }
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br" }

import pf.Stdout
import cli.Stdout

main! = \_args ->
try Stdout.line! (Inspect.toStr (parse_name_and_year "Alice was born in 1990"))
try Stdout.line! (Inspect.toStr (parse_name_and_year_try "Alice was born in 1990"))
main! = |_args|
Stdout.line!(Inspect.to_str(parse_name_and_year("Alice was born in 1990")))?
Stdout.line!(Inspect.to_str(parse_name_and_year_try("Alice was born in 1990")))?
Ok {}
Ok({})
### start snippet question
parse_name_and_year : Str -> Result { name : Str, birth_year : U16 } _
parse_name_and_year = \str ->
{ before: name, after: birth_year_str } = Str.splitFirst? str " was born in "
birth_year = Str.toU16? birth_year_str
Ok { name, birth_year }
parse_name_and_year = |str|
{ before: name, after: birth_year_str } = Str.split_first(str, " was born in ")?
birth_year = Str.to_u16(birth_year_str)?
Ok({ name, birth_year })
### end snippet question
parse_name_and_year_try = \str ->
### start snippet try
str
|> Str.splitFirst " was born in "
|> Result.try \{ before: name, after: birth_year_str } ->
Str.toU16 birth_year_str
|> Result.try \birth_year ->
Ok { name, birth_year }
### end snippet try

expect
parse_name_and_year "Alice was born in 1990" == Ok { name: "Alice", birth_year: 1990 }

expect
parse_name_and_year_try "Alice was born in 1990" == Ok { name: "Alice", birth_year: 1990 }
### start snippet desugared
parse_name_and_year_try = |str|
when Str.split_first(str, " was born in ") is
Err(err1) ->
return Err(err1)
Ok({ before: name, after: birth_year_str }) ->
when Str.to_u16(birth_year_str) is
Err(err2) ->
return Err(err2)
Ok(birth_year) ->
Ok({ name, birth_year })
### end snippet desugared
expect parse_name_and_year("Alice was born in 1990") == Ok({ name: "Alice", birth_year: 1990 })
expect parse_name_and_year_try("Alice was born in 1990") == Ok({ name: "Alice", birth_year: 1990 })
4 changes: 2 additions & 2 deletions examples/DotNetPlatform/README.md
Original file line number Diff line number Diff line change
@@ -41,12 +41,12 @@ To run:
$ cd platform
$ dotnet run
```
This should print "Hello from .NET".
This should print "Hello from .NET" and "Hi from roc! (in a .NET platform) 🔥🦅🔥".


## Build & Run Binary

If you want to build a binary for the app using native AOT:
To build a binary for the app using Ahead-Of-Time compilation:

1. Publish the dotnet app
```cli
2 changes: 1 addition & 1 deletion examples/DotNetPlatform/platform/Program.cs
Original file line number Diff line number Diff line change
@@ -30,7 +30,7 @@ static IntPtr CustomResolver(string libraryName, Assembly assembly, DllImportSea

public static partial class Platform
{
[LibraryImport("interop", EntryPoint = "roc__mainForHost_1_exposed_generic")]
[LibraryImport("interop", EntryPoint = "roc__main_for_host_1_exposed_generic")]
internal static partial void MainFromRoc(out RocStr rocStr);
}

6 changes: 3 additions & 3 deletions examples/DotNetPlatform/platform/main.roc
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ platform "dotnetplatform"
exposes []
packages {}
imports []
provides [mainForHost]
provides [main_for_host]

mainForHost : Str
mainForHost = main
main_for_host : Str
main_for_host = main
48 changes: 25 additions & 23 deletions examples/ElmWebApp/backend.roc
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
app [Model, server] {
pf: platform "https://github.com/roc-lang/basic-webserver/releases/download/0.8.0/jz2EfGAtz_y06nN7f8tU9AvmzhKK-jnluXQQGa9rZoQ.tar.br",
app [Model, init!, respond!] {
web: platform "https://github.com/roc-lang/basic-webserver/releases/download/0.12.0/Q4h_In-sz1BqAvlpmCsBHhEJnn_YvfRRMiNACB_fBbk.tar.br",
}

import pf.Stdout
import pf.Http exposing [Request, Response]
import pf.Utc
import web.Stdout
import web.Http exposing [Request, Response]
import web.Utc

# [backend](https://chatgpt.com/share/7ac35a32-dab5-46d0-bb17-9d584469556f) Roc server

@@ -13,24 +13,26 @@ Model : {}

# With `init` you can set up a database connection once at server startup,
# generate css by running `tailwindcss`,...
# In this case we don't have anything to initialize, so it is just `Task.ok {}`.
# In this case we don't have anything to initialize, so it is just `Ok({})`.

server = { init: Task.ok {}, respond }
init! = |{}| Ok({})

respond : Request, Model -> Task Response [ServerErr Str]_
respond = \req, _ ->
respond! : Request, Model => Result Response [ServerErr Str]_
respond! = |req, _|
# Log request datetime, method and url
datetime = Utc.now! |> Utc.toIso8601Str

Stdout.line! "$(datetime) $(Http.methodToStr req.method) $(req.url)"

Task.ok {
status: 200,
headers: [
# !!
# Change http://localhost:8001 to your domain for production usage
# !!
{ name: "Access-Control-Allow-Origin", value: "http://localhost:8001" },
],
body: Str.toUtf8 "Hi, Elm! This is from Roc: 🎁\n",
}
datetime = Utc.to_iso_8601(Utc.now!({}))

Stdout.line!("${datetime} ${Inspect.to_str(req.method)} ${req.uri}")?
Ok(
{
status: 200,
headers: [
# !!
# Change http://localhost:8001 to your domain for production usage
# !!
{ name: "Access-Control-Allow-Origin", value: "http://localhost:8001" },
],
body: Str.to_utf8("Hi, Elm! This is from Roc: 🎁\n"),
},
)
6 changes: 4 additions & 2 deletions examples/EncodeDecode/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Encoding & Decoding Abilities

An example for how to implement the builtin `Encoding` and `Decoding` abilities for an opaque type.
An example for how to implement the builtin `Encoding` and `Decoding` abilities for an opaque type (`ItemKind`).

Implementing these abilites for an opaque type like `ItemKind`, enables it to be used seamlessly within other data structures. This is useful when you would like to provide a custom mapping, such as in this example, between an integer and a tag union.
Implementing these abilites for an opaque type like `ItemKind`, enables it to be used seamlessly within other data structures.
This is useful when you would like to provide a custom mapping, such as in this example, between an integer and a [tag union](https://www.roc-lang.org/tutorial#tag-union-types).

## Implementation
```roc
@@ -29,6 +30,7 @@ $ roc dev
(@ItemKind Class)
(@ItemKind Interface)
(@ItemKind Module)
(@ItemKind Property)
```

You can also use `roc test` to run the tests.
144 changes: 75 additions & 69 deletions examples/EncodeDecode/main.roc
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
app [main!] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br",
json: "https://github.com/lukewilliamboswell/roc-json/releases/download/0.11.0/z45Wzc-J39TLNweQUoLw3IGZtkQiEN3lTBv3BXErRjQ.tar.br",
cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br",
json: "https://github.com/lukewilliamboswell/roc-json/releases/download/0.12.0/1trwx8sltQ-e9Y2rOB4LWUWLS_sFVyETK8Twl0i9qpw.tar.gz",
}

import json.Json
import pf.Stdout
import cli.Stdout
### start snippet impl

ItemKind := [
@@ -21,54 +21,59 @@ ItemKind := [
]
implements [
Decoding { decoder: decode_items },
Encoding { toEncoder: encode_items },
Encoding { to_encoder: encode_items },
Inspect,
Eq,
]

encode_items : ItemKind -> Encoder fmt where fmt implements EncoderFormatting
encode_items = |@ItemKind(kind)|
Encode.u32(
when kind is
Text -> 1
Method -> 2
Function -> 3
Constructor -> 4
Field -> 5
Variable -> 6
Class -> 7
Interface -> 8
Module -> 9
Property -> 10,
)

decode_items : Decoder ItemKind _
decode_items =
Decode.custom(
|bytes, fmt|
# Helper function to wrap our [tag](https://www.roc-lang.org/tutorial#tags)
ok = |tag| Ok(@ItemKind(tag))

bytes
|> Decode.from_bytes_partial(fmt)
|> try_map_result(
|num|
when num is
1 -> ok(Text)
2 -> ok(Method)
3 -> ok(Function)
4 -> ok(Constructor)
5 -> ok(Field)
6 -> ok(Variable)
7 -> ok(Class)
8 -> ok(Interface)
9 -> ok(Module)
10 -> ok(Property)
_ -> Err(TooShort),
),
)

# Converts `DecodeResult U32` to `DecodeResult ItemKind` using a given function
try_map_result : DecodeResult U32, (U32 -> Result ItemKind DecodeError) -> DecodeResult ItemKind
try_map_result = \decoded, mapper ->
try_map_result = |decoded, num_to_item_kind_fun|
when decoded.result is
Err e -> { result: Err e, rest: decoded.rest }
Ok res -> { result: mapper res, rest: decoded.rest }

decode_items : Decoder ItemKind fmt where fmt implements DecoderFormatting
decode_items = Decode.custom \bytes, fmt ->
# Helper function to wrap our tag
ok = \tag -> Ok (@ItemKind tag)

bytes
|> Decode.fromBytesPartial fmt
|> try_map_result \val ->
when val is
1 -> ok Text
2 -> ok Method
3 -> ok Function
4 -> ok Constructor
5 -> ok Field
6 -> ok Variable
7 -> ok Class
8 -> ok Interface
9 -> ok Module
10 -> ok Property
_ -> Err TooShort

encode_items : ItemKind -> Encoder fmt where fmt implements EncoderFormatting
encode_items = \@ItemKind val ->
Encode.u32
(
when val is
Text -> 1
Method -> 2
Function -> 3
Constructor -> 4
Field -> 5
Variable -> 6
Class -> 7
Interface -> 8
Module -> 9
Property -> 10
)
Err(e) -> { result: Err(e), rest: decoded.rest }
Ok(res) -> { result: num_to_item_kind_fun(res), rest: decoded.rest }

### end snippet impl

@@ -77,41 +82,42 @@ encode_items = \@ItemKind val ->
# make a list of ItemKind's
original_list : List ItemKind
original_list = [
@ItemKind Text,
@ItemKind Method,
@ItemKind Function,
@ItemKind Constructor,
@ItemKind Field,
@ItemKind Variable,
@ItemKind Class,
@ItemKind Interface,
@ItemKind Module,
@ItemKind Property,
@ItemKind(Text),
@ItemKind(Method),
@ItemKind(Function),
@ItemKind(Constructor),
@ItemKind(Field),
@ItemKind(Variable),
@ItemKind(Class),
@ItemKind(Interface),
@ItemKind(Module),
@ItemKind(Property),
]

# encode them into JSON
# encode them into JSON bytes
encoded_bytes : List U8
encoded_bytes = Encode.toBytes original_list Json.utf8
encoded_bytes = Encode.to_bytes(original_list, Json.utf8)

# test we have encoded correctly
expect encoded_bytes == original_bytes
# check that encoding is correct
expect
expected_bytes : List U8
expected_bytes = "[1,2,3,4,5,6,7,8,9,10]" |> Str.to_utf8

# take a JSON encoded list
original_bytes : List U8
original_bytes = "[1,2,3,4,5,6,7,8,9,10]" |> Str.toUtf8
encoded_bytes == expected_bytes

# decode into a list of ItemKind's
# decode back to a list of ItemKind's
decoded_list : List ItemKind
decoded_list = Decode.fromBytes original_bytes Json.utf8 |> Result.withDefault []
decoded_list = Decode.from_bytes(encoded_bytes, Json.utf8) |> Result.with_default([])
# don't use `Result.with_default([])` for professional applications; check https://www.roc-lang.org/examples/ErrorHandling/README.html

# test we have decoded correctly
# check that decoding is correct
expect decoded_list == original_list

main! = \_args ->
# debug print decoded items to stdio
main! = |_args|
# prints decoded items to stdout
decoded_list
|> List.map Inspect.toStr
|> Str.joinWith "\n"
|> List.map(Inspect.to_str)
|> Str.join_with("\n")
|> Stdout.line!

### end snippet demo
4 changes: 2 additions & 2 deletions examples/ErrorHandling/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Error handling with try and Result

A more complex "real world" example that demonstrates the use of `try`, `Result` and error handling in Roc.
A more complex "real world" example that demonstrates the use of `Result`, `?` and error handling in Roc.

## Full Code

@@ -13,7 +13,7 @@ file:main.roc
Run this from the directory that has `main.roc` in it:

```sh
$ HELLO=1 roc examples/Tasks/main.roc -- "https://www.roc-lang.org" roc.html
$ HELLO=1 roc examples/ErrorHandling/main.roc -- "https://www.roc-lang.org" roc.html
HELLO env var was set to 1
Fetching content from https://www.roc-lang.org...
Saving url HTML to roc.html...
99 changes: 54 additions & 45 deletions examples/ErrorHandling/main.roc
Original file line number Diff line number Diff line change
@@ -1,91 +1,100 @@
app [main!] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br",
cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br",
}

import pf.Stdout
import pf.Arg exposing [Arg]
import pf.Env
import pf.Http
import pf.Dir
import pf.Utc exposing [Utc]
import pf.Path exposing [Path]
import cli.Stdout
import cli.Arg exposing [Arg]
import cli.Env
import cli.Http
import cli.Dir
import cli.Utc exposing [Utc]
import cli.Path exposing [Path]

usage = "HELLO=1 roc main.roc -- \"https://www.roc-lang.org\" roc.html"

main! : List Arg => Result {} _
main! = \args ->
main! = |args|

# Get time since [Unix Epoch](https://en.wikipedia.org/wiki/Unix_time)
start_time : Utc
start_time = Utc.now! {}
start_time = Utc.now!({})

# Read the HELLO environment variable
hello_env : Str
hello_env =
read_env_var! "HELLO"
|> try
|> \msg -> if Str.isEmpty msg then "was empty" else "was set to $(msg)"
read_env_var!("HELLO")?
|> |env_var_content|
if Str.is_empty(env_var_content) then
"was empty"
else
"was set to ${env_var_content}"

try Stdout.line! "HELLO env var $(hello_env)"
Stdout.line!("HELLO env var ${hello_env}")?
# Read command line arguments
{ url, output_path } = try parse_args! args
{ url, output_path } = parse_args!(args)?
try Stdout.line! "Fetching content from $(url)..."
Stdout.line!("Fetching content from ${url}...")?
# Fetch the provided url using HTTP
html_str : Str
html_str = try fetch_html! url
html_str = fetch_html!(url)?
try Stdout.line! "Saving url HTML to $(Path.display output_path)..."
Stdout.line!("Saving url HTML to ${Path.display(output_path)}...")?

# Write HTML string to a file
Path.write_utf8! html_str output_path
|> Result.mapErr \_ -> FailedToWriteFile "Failed to write to file $(Path.display output_path), usage: $(usage)"
|> try
Result.map_err(
Path.write_utf8!(html_str, output_path),
|_| FailedToWriteFile("Failed to write to file ${Path.display(output_path)}, usage: ${usage}"),
)?

# Print contents of current working directory
try list_cwd_contents! {}
list_cwd_contents!({})?

end_time : Utc
end_time = Utc.now! {}
end_time = Utc.now!({})

run_duration = Utc.delta_as_millis start_time end_time
run_duration = Utc.delta_as_millis(start_time, end_time)

try Stdout.line! "Run time: $(Num.toStr run_duration) ms"
Stdout.line!("Run time: ${Num.to_str(run_duration)} ms")?
try Stdout.line! "Done"
Stdout.line!("Done")?
Ok {}
Ok({})
parse_args! : List Arg => Result { url : Str, output_path : Path } _
parse_args! = \args ->
when List.map args Arg.display is
parse_args! = |args|
when List.map(args, Arg.display) is
[_, first, second, ..] ->
Ok { url: first, output_path: Path.from_str second }
Ok({ url: first, output_path: Path.from_str(second) })
_ ->
Err (FailedToReadArgs "Failed to read command line arguments, usage: $(usage)")
Err(FailedToReadArgs("Failed to read command line arguments, usage: ${usage}"))
read_env_var! : Str => Result Str []
read_env_var! = \envVarName ->
when Env.var! envVarName is
Ok envVarStr if !(Str.isEmpty envVarStr) -> Ok envVarStr
_ -> Ok ""
read_env_var! = |env_var_name|
when Env.var!(env_var_name) is
Ok(env_var_str) if !Str.is_empty(env_var_str) -> Ok(env_var_str)
_ -> Ok("")
fetch_html! : Str => Result Str _
fetch_html! = \url ->
Http.get_utf8! url
|> Result.mapErr \err -> FailedToFetchHtml "Failed to fetch URL $(Inspect.toStr err), usage: $(usage)"
fetch_html! = |url|
Http.get_utf8!(url)
|> Result.map_err(|err| FailedToFetchHtml("Failed to fetch URL ${Inspect.to_str(err)}, usage: ${usage}"))
# effects need to be functions so we use the empty input type `{}`
list_cwd_contents! : {} => Result {} _
list_cwd_contents! = \_ ->
list_cwd_contents! = |_|
dirContents =
Dir.list! "."
|> Result.mapErr \_ -> FailedToListCwd "Failed to list contents of current directory, usage: $(usage)"
|> try
dir_contents =
Result.map_err(
Dir.list!("."),
|_| FailedToListCwd("Failed to list contents of current directory, usage: ${usage}"),
)?
contentsStr = List.map dirContents Path.display |> Str.joinWith ","
contents_str =
dir_contents
|> List.map(Path.display)
|> Str.join_with(",")
Stdout.line! "Contents of current directory: $(contentsStr)"
Stdout.line!("Contents of current directory: ${contents_str}")
34 changes: 17 additions & 17 deletions examples/FizzBuzz/main.roc
Original file line number Diff line number Diff line change
@@ -1,43 +1,43 @@
app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br" }
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br" }

import pf.Stdout
import cli.Stdout

main! = \_args ->
List.range { start: At 1, end: At 100 }
|> List.map fizz_buzz
|> Str.joinWith ","
main! = |_args|
List.range({ start: At(1), end: At(100) })
|> List.map(fizz_buzz)
|> Str.join_with(",")
|> Stdout.line!

## Determine the FizzBuzz value for a given integer.
## Returns "Fizz" for multiples of 3, "Buzz" for
## multiples of 5, "FizzBuzz" for multiples of both
## 3 and 5, and the original number for anything else.
fizz_buzz : I32 -> Str
fizz_buzz = \n ->
fizz_buzz = |n|
fizz = n % 3 == 0
buzz = n % 5 == 0

if fizz && buzz then
if fizz and buzz then
"FizzBuzz"
else if fizz then
"Fizz"
else if buzz then
"Buzz"
else
Num.toStr n
Num.to_str(n)

## Test Case 1: not a multiple of 3 or 5
expect fizz_buzz 1 == "1"
expect fizz_buzz 7 == "7"
expect fizz_buzz(1) == "1"
expect fizz_buzz(7) == "7"

## Test Case 2: multiple of 3
expect fizz_buzz 3 == "Fizz"
expect fizz_buzz 9 == "Fizz"
expect fizz_buzz(3) == "Fizz"
expect fizz_buzz(9) == "Fizz"

## Test Case 3: multiple of 5
expect fizz_buzz 5 == "Buzz"
expect fizz_buzz 20 == "Buzz"
expect fizz_buzz(5) == "Buzz"
expect fizz_buzz(20) == "Buzz"

## Test Case 4: multiple of both 3 and 5
expect fizz_buzz 15 == "FizzBuzz"
expect fizz_buzz 45 == "FizzBuzz"
expect fizz_buzz(15) == "FizzBuzz"
expect fizz_buzz(45) == "FizzBuzz"
2 changes: 1 addition & 1 deletion examples/GoPlatform/README.md
Original file line number Diff line number Diff line change
@@ -72,7 +72,7 @@ $ roc build --bundle .tar.br platform/main.roc

4. Now you can use the platform from inside a Roc file with:
```roc
app [goUsingRocApp] { pf: platform "YOUR_URL" }
app [main] { pf: platform "YOUR_URL" }
```

‼ This build procedure only builds the platform for your kind of operating system and architecture. If you want to support users on all Roc supported operating systems and architectures, you'll need [this kind of setup](https://github.com/roc-lang/roc/blob/main/.github/workflows/basic_cli_build_release.yml).
2 changes: 1 addition & 1 deletion examples/GoPlatform/platform/host.h
Original file line number Diff line number Diff line change
@@ -6,4 +6,4 @@ struct RocStr {
size_t capacity;
};

extern void roc__mainForHost_1_exposed_generic(const struct RocStr *data);
extern void roc__main_for_host_1_exposed_generic(const struct RocStr *data);
2 changes: 1 addition & 1 deletion examples/GoPlatform/platform/main.go
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ import (

func main() {
var str C.struct_RocStr
C.roc__mainForHost_1_exposed_generic(&str)
C.roc__main_for_host_1_exposed_generic(&str)
fmt.Print(rocStrRead(str))
}

6 changes: 3 additions & 3 deletions examples/GoPlatform/platform/main.roc
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ platform "go-platform"
exposes []
packages {}
imports []
provides [mainForHost]
provides [main_for_host]

mainForHost : Str
mainForHost = main
main_for_host : Str
main_for_host = main
116 changes: 58 additions & 58 deletions examples/GraphTraversal/Graph.roc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## The Graph interface represents a [graph](https://en.wikipedia.org/wiki/Graph_(discrete_mathematics))
## The Graph module represents a [graph](https://en.wikipedia.org/wiki/Graph_(discrete_mathematics))
## using an [adjacency list](https://en.wikipedia.org/wiki/Adjacency_list)
## and exposes functions for working with graphs, such as creating one from a list and
## performing a depth-first or breadth-first search.
@@ -16,13 +16,13 @@ Graph a := Dict a (List a) where a implements Eq

## Create a Graph from an adjacency list.
from_list : List (a, List a) -> Graph a
from_list = \adjacency_list ->
empty_dict = Dict.withCapacity (List.len adjacency_list)
from_list = |adjacency_list|
empty_dict = Dict.with_capacity(List.len(adjacency_list))

update = \dict, (vertex, edges) ->
Dict.insert dict vertex edges
update = |dict, (vertex, edges)|
Dict.insert(dict, vertex, edges)

@Graph (List.walk adjacency_list empty_dict update)
@Graph(List.walk(adjacency_list, empty_dict, update))

## Create a Graph from an adjacency list.
from_dict : Dict a (List a) -> Graph a
@@ -31,148 +31,148 @@ from_dict = @Graph
## Perform a depth-first search on a graph to find a target vertex.
## [Algorithm animation](https://en.wikipedia.org/wiki/Depth-first_search#/media/File:Depth-First-Search.gif)
##
## - `isTarget` : A function that returns true if a vertex is the target.
## - `is_target` : A function that returns true if a vertex is the target.
## - `root` : The starting vertex for the search.
## - `graph` : The graph to perform the search on.
dfs : (a -> Bool), a, Graph a -> Result a [NotFound]
dfs = \is_target, root, @Graph graph ->
dfs_helper is_target [root] (Set.empty {}) graph
dfs = |is_target, root, @Graph(graph)|
dfs_helper(is_target, [root], Set.empty({}), graph)

# A helper function for performing the depth-first search.
#
# `isTarget` : A function that returns true if a vertex is the target.
# `is_target` : A function that returns true if a vertex is the target.
# `stack` : A List of vertices to visit.
# `visited` : A Set of visited vertices.
# `graph` : The graph to perform the search on.
dfs_helper : (a -> Bool), List a, Set a, Dict a (List a) -> Result a [NotFound]
dfs_helper = \is_target, stack, visited, graph ->
dfs_helper = |is_target, stack, visited, graph|
when stack is
[] ->
Err NotFound
Err(NotFound)

[.., current] ->
rest = List.dropLast stack 1
rest = List.drop_last(stack, 1)

if is_target current then
Ok current
else if Set.contains visited current then
dfs_helper is_target rest visited graph
if is_target(current) then
Ok(current)
else if Set.contains(visited, current) then
dfs_helper(is_target, rest, visited, graph)
else
new_visited = Set.insert visited current
new_visited = Set.insert(visited, current)

when Dict.get graph current is
Ok neighbors ->
when Dict.get(graph, current) is
Ok(neighbors) ->
# filter out all visited neighbors
filtered =
neighbors
|> List.keepIf (\n -> !(Set.contains new_visited n))
|> List.keep_if(|n| !(Set.contains(new_visited, n)))
|> List.reverse

# newly explored nodes are added to LIFO stack
new_stack = List.concat rest filtered
new_stack = List.concat(rest, filtered)

dfs_helper is_target new_stack new_visited graph
dfs_helper(is_target, new_stack, new_visited, graph)

Err KeyNotFound ->
dfs_helper is_target rest new_visited graph
Err(KeyNotFound) ->
dfs_helper(is_target, rest, new_visited, graph)

## Perform a breadth-first search on a graph to find a target vertex.
## [Algorithm animation](https://en.wikipedia.org/wiki/Breadth-first_search#/media/File:Animated_BFS.gif)
##
## - `isTarget` : A function that returns true if a vertex is the target.
## - `is_target` : A function that returns true if a vertex is the target.
## - `root` : The starting vertex for the search.
## - `graph` : The graph to perform the search on.
bfs : (a -> Bool), a, Graph a -> Result a [NotFound]
bfs = \is_target, root, @Graph graph ->
bfs_helper is_target [root] (Set.single root) graph
bfs = |is_target, root, @Graph(graph)|
bfs_helper(is_target, [root], Set.single(root), graph)

# A helper function for performing the breadth-first search.
#
# `isTarget` : A function that returns true if a vertex is the target.
# `is_target` : A function that returns true if a vertex is the target.
# `queue` : A List of vertices to visit.
# `seen` : A Set of all seen vertices.
# `graph` : The graph to perform the search on.
bfs_helper : (a -> Bool), List a, Set a, Dict a (List a) -> Result a [NotFound]
bfs_helper = \is_target, queue, seen, graph ->
bfs_helper = |is_target, queue, seen, graph|
when queue is
[] ->
Err NotFound
Err(NotFound)

[current, ..] ->
rest = List.dropFirst queue 1
rest = List.drop_first(queue, 1)

if is_target current then
Ok current
if is_target(current) then
Ok(current)
else
when Dict.get graph current is
Ok neighbors ->
when Dict.get(graph, current) is
Ok(neighbors) ->
# filter out all seen neighbors
filtered = List.keepIf neighbors (\n -> !(Set.contains seen n))
filtered = List.keep_if(neighbors, |n| !(Set.contains(seen, n)))

# newly explored nodes are added to the FIFO queue
new_queue = List.concat rest filtered
new_queue = List.concat(rest, filtered)

# the new nodes are also added to the seen set
new_seen = List.walk filtered seen Set.insert
new_seen = List.walk(filtered, seen, Set.insert)

bfs_helper is_target new_queue new_seen graph
bfs_helper(is_target, new_queue, new_seen, graph)

Err KeyNotFound ->
bfs_helper is_target rest seen graph
Err(KeyNotFound) ->
bfs_helper(is_target, rest, seen, graph)

# Test DFS with multiple paths
expect
actual = dfs (\v -> Str.startsWith v "C") "A" test_graph_multipath
expected = Ok "Ccorrect"
actual = dfs(|v| Str.starts_with(v, "C"), "A", test_graph_multipath)
expected = Ok("Ccorrect")

actual == expected

# Test BFS with multiple paths
expect
actual = bfs (\v -> Str.startsWith v "C") "A" test_graph_multipath
expected = Ok "Ccorrect"
actual = bfs(|v| Str.starts_with(v, "C"), "A", test_graph_multipath)
expected = Ok("Ccorrect")

actual == expected

# Test DFS
expect
actual = dfs (\v -> Str.startsWith v "F") "A" test_graph_small
expected = Ok "F-DFS"
actual = dfs(|v| Str.starts_with(v, "F"), "A", test_graph_small)
expected = Ok("F-DFS")

actual == expected

## Test BFS
expect
actual = bfs (\v -> Str.startsWith v "F") "A" test_graph_small
expected = Ok "F-BFS"
actual = bfs(|v| Str.starts_with(v, "F"), "A", test_graph_small)
expected = Ok("F-BFS")

actual == expected

# Test NotFound DFS
expect
actual = dfs (\v -> v == "not a node") "A" test_graph_small
expected = Err NotFound
actual = dfs(|v| v == "not a node", "A", test_graph_small)
expected = Err(NotFound)

actual == expected

# Test NotFound BFS
expect
actual = dfs (\v -> v == "not a node") "A" test_graph_small
expected = Err NotFound
actual = dfs(|v| v == "not a node", "A", test_graph_small)
expected = Err(NotFound)

actual == expected

# Test DFS large
expect
actual = dfs (\v -> v == "AE") "A" test_graph_large
expected = Ok "AE"
actual = dfs(|v| v == "AE", "A", test_graph_large)
expected = Ok("AE")

actual == expected

## Test BFS large
expect
actual = bfs (\v -> v == "AE") "A" test_graph_large
expected = Ok "AE"
actual = bfs(|v| v == "AE", "A", test_graph_large)
expected = Ok("AE")

actual == expected

2 changes: 1 addition & 1 deletion examples/GraphTraversal/README.md
Original file line number Diff line number Diff line change
@@ -14,5 +14,5 @@ Run this from the directory that has `Graph.roc` in it:
```
$ roc test Graph.roc
0 failed and 4 passed in 653 ms.
0 failed and 8 passed in 200 ms.
```
30 changes: 16 additions & 14 deletions examples/HelloWeb/main.roc
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
app [Model, init!, respond!] { pf: platform "https://github.com/roc-lang/basic-webserver/releases/download/0.11.0/yWHkcVUt_WydE1VswxKFmKFM5Tlu9uMn6ctPVYaas7I.tar.br" }
app [Model, init!, respond!] { web: platform "https://github.com/roc-lang/basic-webserver/releases/download/0.12.0/Q4h_In-sz1BqAvlpmCsBHhEJnn_YvfRRMiNACB_fBbk.tar.br" }

import pf.Stdout
import pf.Http exposing [Request, Response]
import pf.Utc
import web.Stdout
import web.Http exposing [Request, Response]
import web.Utc

# Model is produced by `init`.
Model : {}

# With `init` you can set up a database connection once at server startup,
# generate css by running `tailwindcss`,...
# In this case we don't have anything to initialize, so it is just `Task.ok {}`.
# In this case we don't have anything to initialize, so it is just `Ok({})`.

init! : {} => Result Model []
init! = \_ -> Ok {}
init! = |_| Ok({})

respond! : Request, Model => Result Response [ServerErr Str]_
respond! = \req, _ ->
respond! = |req, _|
# Log request datetime, method and url
datetime = Utc.to_iso_8601 (Utc.now! {})
datetime = Utc.to_iso_8601(Utc.now!({}))

try Stdout.line! "$(datetime) $(Inspect.toStr req.method) $(req.uri)"
Stdout.line!("${datetime} ${Inspect.to_str(req.method)} ${req.uri}")?
Ok {
status: 200,
headers: [],
body: Str.toUtf8 "<b>Hello, web!</b></br>",
}
Ok(
{
status: 200,
headers: [],
body: Str.to_utf8("<b>Hello, web!</b></br>"),
},
)
8 changes: 4 additions & 4 deletions examples/HelloWorld/main.roc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br" }
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br" }

import pf.Stdout
import cli.Stdout

main! = \_args ->
Stdout.line! "Hello, World!"
main! = |_args|
Stdout.line!("Hello, World!")
4 changes: 2 additions & 2 deletions examples/ImportFromDirectory/Dir/Hello.roc
Original file line number Diff line number Diff line change
@@ -2,5 +2,5 @@
module [hello]

hello : Str -> Str
hello = \name ->
"Hello $(name) from inside Dir!"
hello = |name|
"Hello ${name} from inside Dir!"
8 changes: 4 additions & 4 deletions examples/ImportFromDirectory/main.roc
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br" }
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br" }

import pf.Stdout
import cli.Stdout
import Dir.Hello exposing [hello]

main! = \_args ->
main! = |_args|
# here we're calling the `hello` function from the Hello module
Stdout.line! (hello "World")
Stdout.line!(hello("World"))
2 changes: 1 addition & 1 deletion examples/ImportPackageFromModule/Module.roc
Original file line number Diff line number Diff line change
@@ -2,4 +2,4 @@ module [split_graphemes]

import unicode.Grapheme

split_graphemes = \string -> Grapheme.split string
split_graphemes = |string| Grapheme.split(string)
12 changes: 7 additions & 5 deletions examples/ImportPackageFromModule/main.roc
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
### start snippet header
app [main!] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br",
unicode: "https://github.com/roc-lang/unicode/releases/download/0.1.2/vH5iqn04ShmqP-pNemgF773f86COePSqMWHzVGrAKNo.tar.br",
cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br",
unicode: "https://github.com/roc-lang/unicode/releases/download/0.3.0/9KKFsA4CdOz0JIOL7iBSI_2jGIXQ6TsFBXgd086idpY.tar.br",
}
### end snippet header

import pf.Stdout
import cli.Stdout
import Module

main! = \_args ->
Stdout.line! (Inspect.toStr (Module.split_graphemes "hello"))
main! = |_args|
Module.split_graphemes("hello")
|> Inspect.to_str
|> Stdout.line!
6 changes: 3 additions & 3 deletions examples/IngestFiles/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Ingest Files

Statically importing files as a `Str` or a `List U8` (list of bytes):
To statically import files as a `Str` or a `List U8` (list of bytes):

```roc
import "some-file" as someStr : Str
import "some-file" as someBytes : List U8
import "some-file" as some_str : Str
import "some-file" as some_bytes : List U8
```

## Code
8 changes: 4 additions & 4 deletions examples/IngestFiles/main.roc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br" }
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br" }

import pf.Stdout
import cli.Stdout
import "sample.txt" as sample : Str

main! = \_args ->
Stdout.line! "$(sample)"
main! = |_args|
Stdout.line!("${sample}")
2 changes: 1 addition & 1 deletion examples/Json/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# JSON

Decode JSON data.
Decode JSON data into a Roc record.

## Code
```roc
21 changes: 10 additions & 11 deletions examples/Json/main.roc
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
app [main!] {
cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br",
json: "https://github.com/lukewilliamboswell/roc-json/releases/download/0.11.0/z45Wzc-J39TLNweQUoLw3IGZtkQiEN3lTBv3BXErRjQ.tar.br",
cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br",
json: "https://github.com/lukewilliamboswell/roc-json/releases/download/0.12.0/1trwx8sltQ-e9Y2rOB4LWUWLS_sFVyETK8Twl0i9qpw.tar.gz",
}

import cli.Stdout
import json.Json

main! = \_args ->
request_body = Str.toUtf8 "{\"Image\":{\"Animated\":false,\"Height\":600,\"Ids\":[116,943,234,38793],\"Thumbnail\":{\"Height\":125,\"Url\":\"http:\\/\\/www.example.com\\/image\\/481989943\",\"Width\":100},\"Title\":\"View from 15th Floor\",\"Width\":800}}"
main! = |_args|
request_body = Str.to_utf8("{\"Image\":{\"Animated\":false,\"Height\":600,\"Ids\":[116,943,234,38793],\"Thumbnail\":{\"Height\":125,\"Url\":\"http:\\/\\/www.example.com\\/image\\/481989943\",\"Width\":100},\"Title\":\"View from 15th Floor\",\"Width\":800}}")

# This { fieldNameMapping: PascalCase } setting translates
# This { field_name_mapping: PascalCase } setting translates
# incoming JSON fields from PascalCase (first letter capitalized)
# to camelCase (first letter uncapitalized), which is what
# Roc field names always use.
decoder = Json.utf8With { fieldNameMapping: PascalCase }
# to snake_case, which is what Roc field names always use.
decoder = Json.utf8_with({ field_name_mapping: PascalCase })

decoded : DecodeResult ImageRequest
decoded = Decode.fromBytesPartial request_body decoder
decoded = Decode.from_bytes_partial(request_body, decoder)

when decoded.result is
Ok record -> Stdout.line! "Successfully decoded image, title:\"$(record.image.title)\""
Err _ -> Err (Exit 1 "Error, failed to decode image")
Ok(record) -> Stdout.line!("Successfully decoded image, title:\"${record.image.title}\"")
Err(_) -> Err(Exit(1, "Error, failed to decode image"))

ImageRequest : {
image : {
32 changes: 14 additions & 18 deletions examples/LeastSquares/main.roc
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br" }
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br" }

import pf.Stdout
import cli.Stdout

main! = \_args ->
nStr = Num.toStr (least_square_difference {})
main! = |_args|
n_str = Num.to_str(least_square_difference)

Stdout.line! "The least positive integer n, where the difference of n*n and (n-1)*(n-1) is greater than 1000, is $(nStr)"
Stdout.line!("The least positive integer n, where the difference of n*n and (n-1)*(n-1) is greater than 1000, is ${n_str}")
## A recursive function that takes an `U32` as its input and returns the least
## positive integer number `n`, where the difference of `n*n` and `(n-1)*(n-1)`
## is greater than 1000.
## The smallest positive integer number `n`, where the difference
## of `n*n` and `(n-1)*(n-1)` is greater than 1000.
##
## The input `n` should be a positive integer, and the function will return an
## `U32`representing the least positive integer that satisfies the condition.
##
least_square_difference : {} -> U32
least_square_difference = \_ ->
find_number = \n ->
difference = (Num.powInt n 2) - (Num.powInt (n - 1) 2)
least_square_difference : U32
least_square_difference =
find_number = |n|
difference = (Num.pow_int(n, 2)) - (Num.pow_int((n - 1), 2))
if difference > 1000 then
n
else
find_number (n + 1)
find_number((n + 1))
find_number 1
find_number(1)
expect least_square_difference {} == 501
expect least_square_difference == 501
2 changes: 1 addition & 1 deletion examples/LoopEffect/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Loop Effects
Sometimes, you need to repeat an [effectful](https://en.wikipedia.org/wiki/Side_effect_(computer_science)) function, multiple times until a particular event occurs. In roc, you can use a [recursive function](https://en.wikipedia.org/wiki/Recursion_(computer_science)) to do this.
Sometimes, you need to repeat an [effectful](https://en.wikipedia.org/wiki/Side_effect_(computer_science)) function, multiple times until a particular event occurs. In Roc, you can use a [recursive function](https://en.wikipedia.org/wiki/Recursion_(computer_science)) to do this.

We'll demonstrate this by adding numbers read from stdin until the end of input (Ctrl-D or [end of file](https://en.wikipedia.org/wiki/End-of-file)).

47 changes: 24 additions & 23 deletions examples/LoopEffect/main.roc
Original file line number Diff line number Diff line change
@@ -1,37 +1,38 @@
app [main!] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br",
cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br",
}

import pf.Stdin
import pf.Stdout
import pf.Stderr
import cli.Stdin
import cli.Stdout
import cli.Stderr

main! = \_args ->
when run! {} is
Ok {} -> Ok {}
Err err -> print_err! err
main! = |_args|
when run!({}) is
Ok({}) -> Ok({})
Err(err) -> print_err!(err)

run! : {} => Result {} _
run! = \_ ->
try Stdout.line! "Enter some numbers on different lines, then press Ctrl-D to sum them up."
run! = |_|
Stdout.line!("Enter some numbers on different lines, then press Ctrl-D to sum them up.")?
sum = try add_number_from_stdin! 0
sum = add_number_from_stdin!(0)?
Stdout.line! "Sum: $(Num.toStr sum)"
Stdout.line!("Sum: ${Num.to_str(sum)}")

## recursive function that sums every number that is provided through stdin
add_number_from_stdin! : I64 => Result I64 _
add_number_from_stdin! = \sum ->
when Stdin.line! {} is
Ok input ->
when Str.toI64 input is
Ok num -> add_number_from_stdin! (sum + num)
Err _ -> Err (NotNum input)
add_number_from_stdin! = |sum|
when Stdin.line!({}) is
Ok(input) ->
when Str.to_i64(input) is
Ok(num) -> add_number_from_stdin!((sum + num))
Err(_) -> Err(NotNum(input))

Err EndOfFile -> Ok sum
Err err -> err |> Inspect.toStr |> NotNum |> Err
Err(EndOfFile) -> Ok(sum)
Err(err) -> err |> Inspect.to_str |> NotNum |> Err

print_err! : _ => Result {} _
print_err! = \err ->
print_err! = |err|
when err is
NotNum text -> Stderr.line! "Error: \"$(text)\" is not a valid I64 number."
_ -> Stderr.line! "Error: $(Inspect.toStr err)"
NotNum(text) -> Stderr.line!("Error: \"${text}\" is not a valid I64 number.")
_ -> Stderr.line!("Error: ${Inspect.to_str(err)}")
2 changes: 1 addition & 1 deletion examples/MultiLineComments/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Multi-line comments

Roc does not support native multi-line comments like `/*...*/` in other languages.
Roc does not support native multi-line comments like `/*...*/` in other languages (see below for why).
It's `#` all the way:

```roc
4 changes: 2 additions & 2 deletions examples/MultipleRocFiles/Hello.roc
Original file line number Diff line number Diff line change
@@ -2,5 +2,5 @@
module [hello]

hello : Str -> Str
hello = \name ->
"Hello $(name) from interface!"
hello = |name|
"Hello ${name} from module!"
2 changes: 1 addition & 1 deletion examples/MultipleRocFiles/README.md
Original file line number Diff line number Diff line change
@@ -22,5 +22,5 @@ Run this from the directory that has `main.roc` in it:

```
$ roc main.roc
Hello World from interface!
Hello World from module!
```
8 changes: 4 additions & 4 deletions examples/MultipleRocFiles/main.roc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br" }
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br" }

import pf.Stdout
import cli.Stdout
import Hello

main! = \_args ->
Stdout.line! (Hello.hello "World")
main! = |_args|
Stdout.line!(Hello.hello("World"))
57 changes: 28 additions & 29 deletions examples/Parser/main.roc
Original file line number Diff line number Diff line change
@@ -1,57 +1,56 @@
app [main!] {
cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br",
parser: "https://github.com/lukewilliamboswell/roc-parser/releases/download/0.9.0/w8YKp2YAgQt5REYk912HfKAHBjcXsrnvtjI0CBzoAT4.tar.br",
cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br",
parser: "https://github.com/lukewilliamboswell/roc-parser/releases/download/0.10.0/6eZYaXkrakq9fJ4oUc0VfdxU1Fap2iTuAN18q9OgQss.tar.br",
}

import cli.Stdout
import parser.Parser exposing [Parser, many, oneOf, map]
import parser.String exposing [parseStr, codeunit, anyCodeunit]
import parser.Parser exposing [Parser, many, one_of, map]
import parser.String exposing [parse_str, codeunit, any_codeunit]

main! = \_args ->
main! = |_args|

letters = try parseStr (many letter_parser) input_str
letters = parse_str(many(letter_parser), input_str)?

msg =
letters
|> count_letter_a
|> \count -> "I counted $(count) letter A's!"
|> |count| "I counted ${count} letter A's!"

Stdout.line! msg
Stdout.line!(msg)

Letter : [A, B, C, Other]

input_str = "AAAiBByAABBwBtCCCiAyArBBx"

# Helper to check if a letter is an A tag
isA = \l -> l == A

# Count the number of Letter A's
count_letter_a : List Letter -> Str
count_letter_a = \letters ->
count_letter_a = |letters|
letters
|> List.countIf isA
|> Num.toStr
|> List.count_if(|l| l == A)
|> Num.to_str

# Parser to convert utf8 input into Letter tags
# Parser to convert utf8 input into Letter [tags](https://www.roc-lang.org/tutorial#tags)
letter_parser : Parser (List U8) Letter
letter_parser =
oneOf [
codeunit 'A' |> map \_ -> A,
codeunit 'B' |> map \_ -> B,
codeunit 'C' |> map \_ -> C,
anyCodeunit |> map \_ -> Other,
]

# Test we can parse a single B letter
one_of(
[
codeunit('A') |> map(|_| A),
codeunit('B') |> map(|_| B),
codeunit('C') |> map(|_| C),
any_codeunit |> map(|_| Other),
],
)

# Test parsing a single letter B
expect
input = "B"
parser = letter_parser
result = parseStr parser input
result == Ok B
result = parse_str(parser, input)
result == Ok(B)

# Test we can parse a number of different letters
# Test parsing a number of different letters
expect
input = "BCXA"
parser = many letter_parser
result = parseStr parser input
result == Ok [B, C, Other, A]
parser = many(letter_parser)
result = parse_str(parser, input)
result == Ok([B, C, Other, A])
82 changes: 51 additions & 31 deletions examples/PatternMatching/PatternMatching.roc
Original file line number Diff line number Diff line change
@@ -2,88 +2,108 @@ module []

# Match an empty list
expect
match = \input ->
match = |input|
when input is
[] -> EmptyList
_ -> Other

(match [] == EmptyList)
&& (match [A, B, C] != EmptyList)
match([])
== EmptyList
and match([A, B, C])
!= EmptyList

# Match a non-empty list
expect
match = \input ->
match = |input|
when input is
[_, ..] -> NonEmptyList
_ -> Other

(match [A, B, C] == NonEmptyList)
&& (match [] != NonEmptyList)
match([A, B, C])
== NonEmptyList
and match([])
!= NonEmptyList

# Match a list whose first element is the string "Hi"
expect
match = \input ->
match = |input|
when input is
["Hi", ..] -> StartsWithHi
_ -> Other

(match ["Hi", "Hello", "Yo"] == StartsWithHi)
&& (match ["Hello", "Yo", "Hi"] != StartsWithHi)
match(["Hi", "Hello", "Yo"])
== StartsWithHi
and match(["Hello", "Yo", "Hi"])
!= StartsWithHi

# Match a list whose last element is the number 42
expect
match = \input ->
match = |input|
when input is
[.., 42] -> EndsWith42
_ -> Other

(match [24, 64, 42] == EndsWith42)
&& (match [42, 1, 5] != EndsWith42)
match([24, 64, 42])
== EndsWith42
and match([42, 1, 5])
!= EndsWith42

# Match a list that starts with a Foo tag
# followed by a Bar tag
expect
match = \input ->
match = |input|
when input is
[Foo, Bar, ..] -> StartsWithFooBar
_ -> Other

(match [Foo, Bar, Bar] == StartsWithFooBar)
&& (match [Bar, Bar, Foo] != StartsWithFooBar)
match([Foo, Bar, Bar])
== StartsWithFooBar
and match([Bar, Bar, Foo])
!= StartsWithFooBar

# Match a list with these exact elements:
# Foo, Bar, and then (Baz "Hi")
expect
match = \input ->
match = |input|
when input is
[Foo, Bar, Baz "Hi"] -> FooBarBazStr
[Foo, Bar, Baz("Hi")] -> FooBarBazStr
_ -> Other

(match [Foo, Bar, Baz "Hi"] == FooBarBazStr)
&& (match [Foo, Bar] != FooBarBazStr)
&& (match [Foo, Bar, Baz "Hi", Blah] != FooBarBazStr)
match([Foo, Bar, Baz("Hi")])
== FooBarBazStr
and match([Foo, Bar])
!= FooBarBazStr
and match([Foo, Bar, Baz("Hi"), Blah])
!= FooBarBazStr

# Match a list with Foo as its first element, and
# Count for its second element. Count holds a number,
# and we only match if that number is greater than 0.
expect
match = \input ->
match = |input|
when input is
[Foo, Count num, ..] if num > 0 -> FooCountIf
[Foo, Count(num), ..] if num > 0 -> FooCountIf
_ -> Other

(match [Foo, Count 1] == FooCountIf)
&& (match [Foo, Count 0] != FooCountIf)
&& (match [Baz, Count 1] != FooCountIf)
match([Foo, Count(1)])
== FooCountIf
and match([Foo, Count(0)])
!= FooCountIf
and match([Baz, Count(1)])
!= FooCountIf

# Use `as` to create a variable equal to the part of the list that matches `..`
expect
match = \input ->
match = |input|
when input is
[head, .. as tail] -> HeadAndTail head tail
[head, .. as tail] -> HeadAndTail(head, tail)
_ -> Other

(match [1, 2, 3] == HeadAndTail 1 [2, 3])
&& (match [1, 2] == HeadAndTail 1 [2])
&& (match [1] == HeadAndTail 1 [])
&& (match [] == Other)
match([1, 2, 3])
== HeadAndTail(1, [2, 3])
and match([1, 2])
== HeadAndTail(1, [2])
and match([1])
== HeadAndTail(1, [])
and match([])
== Other
4 changes: 2 additions & 2 deletions examples/PatternMatching/README.md
Original file line number Diff line number Diff line change
@@ -13,9 +13,9 @@ when input is
[Foo, Bar, ..] -> StartsWithFooBar
[Foo, Bar, Baz "Hi"] -> FooBarBazStr
[Foo, Bar, Baz("Hi")] -> FooBarBazStr
[Foo, Count num, ..] if num > 0 -> FooCountIf
[Foo, Count(num), ..] if num > 0 -> FooCountIf
[head, .. as tail] -> HeadAndTail head tail
12 changes: 9 additions & 3 deletions examples/RandomNumbers/README.md
Original file line number Diff line number Diff line change
@@ -4,9 +4,15 @@ Generate a list of random numbers using the [roc-random package](https://github.

## Random `Generators`

Some languages provide a function like JavaScript’s `Math.random()` which can return a different number each time you call it. However, functions in Roc are guaranteed to return the same answer when given the same arguments, so something like `Math.random` couldn’t possibly be a valid Roc function! As such, we use a different approach to generate random numbers in Roc.

This example uses a `Generator` which generates pseudorandom numbers using an initial seed value and the [PCG algorithm](https://www.pcg-random.org/). Note that if the same seed is provided, then the same number sequence will be generated every time! The appearance of randomness comes entirely from deterministic math being done on that initial seed. (The same is true of `Math.random()`, except that `Math.random()` silently chooses a seed for you at runtime.)
Some languages provide a function like JavaScript’s `Math.random()` which can return a different number each time you call it.
However, functions in Roc are guaranteed to return the same answer when given the same arguments, this has many benefits.
But this also means something like `Math.random` couldn’t possibly be a valid Roc function!
So, we use a different approach to generate random numbers in Roc.

This example uses a `Generator` which generates pseudorandom numbers using an initial seed value and the [PCG algorithm](https://www.pcg-random.org/).
If the same seed is provided, then the same number sequence will be generated every time!
The appearance of randomness comes entirely from deterministic math being done on that initial seed.
The same is true of `Math.random()`, except that `Math.random()` silently chooses a seed for you at runtime.

## Code
```roc
37 changes: 19 additions & 18 deletions examples/RandomNumbers/main.roc
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
app [main!] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br",
rand: "https://github.com/lukewilliamboswell/roc-random/releases/download/0.4.0/Ai2KfHOqOYXZmwdHX3g3ytbOUjTmZQmy0G2R9NuPBP0.tar.br",
cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br",
rand: "https://github.com/lukewilliamboswell/roc-random/releases/download/0.5.0/yDUoWipuyNeJ-euaij4w_ozQCWtxCsywj68H0PlJAdE.tar.br",
}

import pf.Stdout
import cli.Stdout
import rand.Random

main! = \_args ->
main! = |_args|

# Print a list of 10 random numbers in the range 25-75 inclusive.
# Print a list of 10 random numbers.
numbers_str =
randomNumbers
|> List.map Num.toStr
|> Str.joinWith "\n"
random_numbers
|> List.map(Num.to_str)
|> Str.join_with("\n")

Stdout.line! numbers_str
Stdout.line!(numbers_str)

# Generate a list random numbers using the seed `1234`.
# Generate a list of random numbers using the seed `1234`.
# This is NOT cryptograhpically secure!
randomNumbers : List U32
randomNumbers =
{ value: numbers } = Random.step (Random.seed 1234) numbersGenerator
random_numbers : List U32
random_numbers =
{ value: numbers } = Random.step(Random.seed(1234), numbers_generator)

numbers

# A generator that will produce a list of 10 random numbers in the range 25-75 inclusive.
# A generator that will produce a list of 10 random numbers in the range 25-75.
# This includes the boundaries, so the numbers can be 25 or 75.
# This is NOT cryptograhpically secure!
numbersGenerator : Random.Generator (List U32)
numbersGenerator =
Random.list (Random.boundedU32 25 75) 10
numbers_generator : Random.Generator (List U32)
numbers_generator =
Random.list(Random.bounded_u32(25, 75), 10)

expect
actual = randomNumbers
actual = random_numbers
actual == [52, 34, 26, 69, 34, 35, 51, 74, 70, 39]
63 changes: 41 additions & 22 deletions examples/RecordBuilder/DateParser.roc
Original file line number Diff line number Diff line change
@@ -6,44 +6,63 @@ module [
build_segment_parser,
]

# see README.md for explanation of code
ParserErr : [InvalidNumStr, OutOfSegments]

ParserGroup a := List Str -> Result (a, List Str) ParserErr

parse_with : (Str -> Result a ParserErr) -> ParserGroup a
parse_with = \parser ->
@ParserGroup \segments ->
when segments is
[] -> Err OutOfSegments
[first, .. as rest] ->
parsed = parser? first
Ok (parsed, rest)
parse_with = |parser|
@ParserGroup(
|segments|
when segments is
[] -> Err(OutOfSegments)
[first, .. as rest] ->
parsed = parser(first)?
Ok((parsed, rest)),
)

chain_parsers : ParserGroup a, ParserGroup b, (a, b -> c) -> ParserGroup c
chain_parsers = \@ParserGroup first, @ParserGroup second, combiner ->
@ParserGroup \segments ->
(a, after_first) = first? segments
(b, after_second) = second? after_first
chain_parsers = |@ParserGroup(first), @ParserGroup(second), combiner|
@ParserGroup(
|segments|
(a, after_first) = first(segments)?
(b, after_second) = second(after_first)?

Ok (combiner a b, after_second)
Ok((combiner(a, b), after_second)),
)

build_segment_parser : ParserGroup a -> (Str -> Result a ParserErr)
build_segment_parser = \@ParserGroup parser_group ->
\text ->
segments = Str.splitOn text "-"
(date, _remaining) = parser_group? segments
build_segment_parser = |@ParserGroup(parser_group)|
|text|
segments = Str.split_on(text, "-")
(date, _remaining) = parser_group(segments)?

Ok date
Ok(date)

expect
date_parser =
{ chain_parsers <-
month: parse_with Ok,
day: parse_with Str.toU64,
year: parse_with Str.toU64,
month: parse_with(Ok),
day: parse_with(Str.to_u64),
year: parse_with(Str.to_u64),
}
|> build_segment_parser

date = date_parser "Mar-10-2015"
date_parser("Mar-10-2015") == Ok({ month: "Mar", day: 10, year: 2015 })

date == Ok { month: "Mar", day: 10, year: 2015 }
expect
date_parser =
build_segment_parser(
chain_parsers(
parse_with(Ok),
chain_parsers(
parse_with(Str.to_u64),
parse_with(Str.to_u64),
|day, year| (day, year),
),
|month, (day, year)| { month, day, year },
),
)

date_parser("Mar-10-2015") == Ok({ month: "Mar", day: 10, year: 2015 })
122 changes: 67 additions & 55 deletions examples/RecordBuilder/README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
# Record Builder
# Record Builder

Record builders are a syntax sugar for sequencing actions and collecting the intermediate results as fields in a record. All you need to build a record is a `map2`-style function that takes two values of the same type and combines them using a provided combiner fnuction. There are many convenient APIs we can build with this simple syntax.
Record builders are a syntax sugar for sequencing actions and collecting the intermediate results as fields in a record.
All you need to build a record is a `map2`-style function that takes two values of the same type and combines them using a provided combiner fnuction. There are many convenient APIs we can build with this simple syntax.

## The Basics

Let's assume we want to develop a module that parses any text with segments delimited by dashes. The record builder pattern can help us here to parse each segment in their own way, and short circuit on the first failure.
Let's assume we want to develop a module that parses any text with segments delimited by dashes, like "Mar-10-2015".
The record builder pattern can help us here to parse each segment in their own way, and [short circuit](https://en.wikipedia.org/wiki/Short-circuit_evaluation) on the first failure.

> Note: it is possible to parse dash-delimited text in a specific format with simpler code. However, generic APIs built with record builders can be much simpler and readable than any such specific implementation.
> Note: it is possible to parse dash-delimited text in a specific format with simpler code.
However, generic APIs built with record builders can be much simpler and readable.

## Defining Types

```roc
ParserGroup a := List Str -> Result (a, List Str) ParserErr
```

We start by defining a `ParserGroup`, which is a [parser combinator](https://en.wikipedia.org/wiki/Parser_combinator) that takes in a list of string segments to parse, and returns parsed data as well as the remaining, unparsed segments. All of the parsers that render to our builder's fields are `ParserGroup` values, and get chained together into one big `ParserGroup`.
We start by defining a `ParserGroup`, which is a [parser combinator](https://en.wikipedia.org/wiki/Parser_combinator)
that takes a list of string segments to parse, and returns parsed data, as well as the remaining, unparsed segments.
All of the parsers that render to our builder's fields are `ParserGroup` values, and get chained together into one big `ParserGroup`.

You'll notice that record builders all tend to deal with a single wrapping type, as we can only combine said values with our `map2`-style function if all combined values are the same type. On the plus side, this allows record builders to work with a single value, two fields, or ten, allowing for great composability.

@@ -24,83 +29,90 @@ It's useful to visualize our desired result. The record builder pattern we're ai

```roc
expect
dateParser : ParserGroup Date
dateParser =
{ chainParsers <-
month: parseWith Ok,
day: parseWith Str.toU64,
year: parseWith Str.toU64,
date_parser =
{ chain_parsers <-
month: parse_with(Ok),
day: parse_with(Str.to_u64),
year: parse_with(Str.to_u64),
}
|> buildSegmentParser
|> build_segment_parser
date = dateParser "Mar-10-2015"
date == Ok { month: "Mar", day: 10, year: 2015 }
date_parser("Mar-10-2015") == Ok({ month: "Mar", day: 10, year: 2015 })
```

This generates a record with fields `month`, `day`, and `year`, all possessing specific parts of the provided date. Note the slight deviation from the conventional record syntax, with the `chainParsers <-` at the top, which is our `map2`-style function.
This generates a record with fields `month`, `day`, and `year`, all possessing specific parts of the provided date.
Note the slight deviation from the conventional record syntax, with the `chain_parsers <-` at the top, which is our `map2`-style function.

## Under the Hood

The record builder pattern is syntax sugar which converts the preceding into:
The record builder pattern is [syntax sugar](https://en.wikipedia.org/wiki/Syntactic_sugar) which converts the previous code block into:

```roc
expect
dateParser : ParserGroup Date
dateParser =
chainParsers
(parseWith Ok)
(chainParsers
(parseWith Str.toU64)
(parseWith Str.toU64)
(\day, year -> (day, year))
)
(\month, (day, year) -> { month, day, year })
date_parser =
build_segment_parser(chain_parsers(
parse_with(Ok),
chain_parsers(
parse_with(Str.to_u64),
parse_with(Str.to_u64),
|day, year| (day, year),
),
|month, (day, year)| { month, day, year },
))
date_parser("Mar-10-2015") == Ok({ month: "Mar", day: 10, year: 2015 })
```

In short, we chain together all pairs of field values with the `map2` combining function, pairing them into tuples until the final grouping of values is structured as a record.

To make the above possible, we'll need to define the `parseWith` function that turns a parser into a `ParserGroup`, and the `chainParsers` function that acts as our `map2` combining function.
To make the above possible, we'll need to define the `parse_with` function that turns a parser into a `ParserGroup`, and the `chain_parsers` function that acts as our `map2` combining function.

## Defining Our Functions

Let's start with `parseWith`:
Let's start with `parse_with`:

```roc
parseWith : (Str -> Result a ParserErr) -> ParserGroup a
parseWith = \parser ->
@ParserGroup \segments ->
when segments is
[] -> Err OutOfSegments
[first, .. as rest] ->
parsed = parser? first
Ok (parsed, rest)
parse_with : (Str -> Result a ParserErr) -> ParserGroup a
parse_with = |parser|
@ParserGroup(
|segments|
when segments is
[] -> Err(OutOfSegments)
[first, .. as rest] ->
parsed = parser(first)?
Ok((parsed, rest)),
)
```

This parses the first segment available, and returns the parsed data along with all remaining segments not yet parsed. We could already use this to parse a single-segment string without even using a record builder, but that wouldn't be very useful. Let's see how our `chainParsers` function will manage combining two `ParserGroup`s in serial:
This parses the first segment available, and returns the parsed data along with all remaining segments not yet parsed.
We could already use this to parse a single-segment string without even using a record builder, but that wouldn't be very useful.
Let's see how our `chain_parsers` function will manage combining two `ParserGroup`s in serial:

```roc
chainParsers : ParserGroup a, ParserGroup b, (a, b -> c) -> ParserGroup c
chainParsers = \@ParserGroup first, @ParserGroup second, combiner ->
@ParserGroup \segments ->
(a, afterFirst) = first? segments
(b, afterSecond) = second? afterFirst
Ok (combiner a b, afterSecond)
chain_parsers : ParserGroup a, ParserGroup b, (a, b -> c) -> ParserGroup c
chain_parsers = |@ParserGroup(first), @ParserGroup(second), combiner|
@ParserGroup(
|segments|
(a, after_first) = first(segments)?
(b, after_second) = second(after_first)?
Ok((combiner(a, b), after_second)),
)
```

Just parse the two groups, and then combine their results? That was easy!
We parse the two groups (see `first` and `second`), and then combine their results.

Finally, we'll need to wrap up our parsers into one that breaks a string into segments and then applies our parsers on said segments. We can call it `buildSegmentParser`:
Finally, we'll need to wrap up our parsers into one that breaks a string into segments and then applies our parsers on those segments.
We can call it `build_segment_parser`:

```roc
buildSegmentParser : ParserGroup a -> (Str -> Result a ParserErr)
buildSegmentParser = \@ParserGroup parserGroup ->
\text ->
segments = Str.splitOn text "-"
(date, _remaining) = parserGroup? segments
build_segment_parser : ParserGroup a -> (Str -> Result a ParserErr)
build_segment_parser = |@ParserGroup(parser_group)|
|text|
segments = Str.split_on(text, "-")
(date, _remaining) = parser_group(segments)?
Ok date
Ok(date)
```

Now we're ready to use our parser as much as we want on any input text!
@@ -116,7 +128,7 @@ file:DateParser.roc
Code for the above example is available in `DateParser.roc` which you can run like this:

```sh
% roc test IDCounter.roc
% roc test DateParser.roc

0 failed and 1 passed in 190 ms.
0 failed and 2 passed in 190 ms.
```
4 changes: 1 addition & 3 deletions examples/Results/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# Results & Error Handling

TODO update this example with a snippet using the `try` keyword, see issue #227

This example shows how to use [`Result`](https://www.roc-lang.org/builtins/Result) in functions that can return errors. We will see how to use `Result.try` or the try operator `?` to chain functions and return the first error if any occurs.
This example shows how to use [`Result`](https://www.roc-lang.org/builtins/Result) in functions that can return errors. We will see how to use `Result.try` or the `?` operator to chain functions and return the first error if any occurs.

## Code

170 changes: 96 additions & 74 deletions examples/Results/main.roc
Original file line number Diff line number Diff line change
@@ -1,105 +1,127 @@
app [main!] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br",
cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br",
}

import pf.Stdout
import cli.Stdout

Person : { first_name : Str, last_name : Str, birth_year : U16 }

## This function parses strings like "{FirstName} {LastName} was born in {Year}"
## and if successful returns `Ok {firstName, lastName, birthYear}`. Otherwise
## and if successful returns `Ok {first_name, last_name, birth_year}`. Otherwise
## it returns an `Err` containing a descriptive tag.
## This is the most verbose version, we will do better below.
parse_verbose = \line ->
when line |> Str.splitFirst " was born in " is
Ok { before: full_name, after: birth_year_str } ->
when full_name |> Str.splitFirst " " is
Ok { before: first_name, after: last_name } ->
when Str.toU16 birth_year_str is
Ok birth_year ->
Ok { first_name, last_name, birth_year }
parse_verbose : Str -> Result Person [InvalidRecordFormat, InvalidNameFormat, InvalidBirthYearFormat]
parse_verbose = |line|
when line |> Str.split_first(" was born in ") is
Ok({ before: full_name, after: birth_year_str }) ->
when full_name |> Str.split_first(" ") is
Ok({ before: first_name, after: last_name }) ->
when Str.to_u16(birth_year_str) is
Ok(birth_year) ->
Ok({ first_name, last_name, birth_year })
Err _ -> Err InvalidBirthYearFormat
Err(_) -> Err(InvalidBirthYearFormat)
_ -> Err InvalidNameFormat
_ -> Err(InvalidNameFormat)
_ -> Err InvalidRecordFormat
_ -> Err(InvalidRecordFormat)
## Here's a very slightly shorter version using `Result.try` to chain multiple
## functions that each could return an error. It's a bit nicer, don't you think?
## Note: this version returns "raw" errors (`Err NotFound` or `Err InvalidNumStr`).
parse_with_try = \line ->
parse_with_try : Str -> Result Person [InvalidNumStr, NotFound]
parse_with_try = |line|
line
|> Str.splitFirst " was born in "
|> Result.try \{ before: full_name, after: birth_year_str } ->
full_name
|> Str.splitFirst " "
|> Result.try \{ before: first_name, after: last_name } ->
Str.toU16 birth_year_str
|> Result.try \birth_year ->
Ok { first_name, last_name, birth_year }

## This version is like `parseWithTry`, except it uses `Result.mapErr`
## to return more informative errors, just like the ones in `parseVerbose`.
parse_with_try_v2 = \line ->
|> Str.split_first(" was born in ")
|> Result.try(
|{ before: full_name, after: birth_year_str }|
full_name
|> Str.split_first(" ")
|> Result.try(
|{ before: first_name, after: last_name }|
Str.to_u16(birth_year_str)
|> Result.try(
|birth_year|
Ok({ first_name, last_name, birth_year }),
),
),
)
## This version is like `parse_with_try`, except it uses `Result.map_err`
## to return more informative errors, just like the ones in `parse_verbose`.
parse_with_try_v2 : Str -> Result Person [InvalidRecordFormat, InvalidNameFormat, InvalidBirthYearFormat]
parse_with_try_v2 = |line|
line
|> Str.splitFirst " was born in "
|> Result.mapErr \_ -> Err InvalidRecordFormat
|> Result.try \{ before: full_name, after: birth_year_str } ->
full_name
|> Str.splitFirst " "
|> Result.mapErr \_ -> Err InvalidNameFormat
|> Result.try \{ before: first_name, after: last_name } ->
Str.toU16 birth_year_str
|> Result.mapErr \_ -> Err InvalidBirthYearFormat
|> Result.try \birth_year ->
Ok { first_name, last_name, birth_year }
|> Str.split_first(" was born in ")
|> Result.map_err(|_| InvalidRecordFormat)
|> Result.try(
|{ before: full_name, after: birth_year_str }|
full_name
|> Str.split_first(" ")
|> Result.map_err(|_| InvalidNameFormat)
|> Result.try(
|{ before: first_name, after: last_name }|
Str.to_u16(birth_year_str)
|> Result.map_err(|_| InvalidBirthYearFormat)
|> Result.try(
|birth_year|
Ok({ first_name, last_name, birth_year }),
),
),
)
## The `?` operator, called the "try operator", is
## [syntactic sugar](en.wikipedia.org/wiki/Syntactic_sugar) for `Result.try`.
## It makes the code much less nested and easier to read.
## The following function is equivalent to `parseWithTry`:
parse_with_try_op = \line ->
{ before: full_name, after: birth_year_str } = Str.splitFirst? line " was born in "
{ before: first_name, after: last_name } = Str.splitFirst? full_name " "
birth_year = Str.toU16? birth_year_str
Ok { first_name, last_name, birth_year }

## And lastly the following function is equivalent to `parseWithTryV2`.
## Note that the `?` operator has moved from `splitFirst` & `toU16` to `mapErr`:
parse_with_try_op_v2 = \line ->
## The following function is equivalent to `parse_with_try`:
parse_with_try_op : Str -> Result Person [NotFound, InvalidNumStr]
parse_with_try_op = |line|
{ before: full_name, after: birth_year_str } = Str.split_first(line, " was born in ")?
{ before: first_name, after: last_name } = Str.split_first(full_name, " ")?
birth_year = Str.to_u16(birth_year_str)?
Ok({ first_name, last_name, birth_year })
## And lastly the following function is equivalent to `parse_with_try_v2`.
## Note that the `?` operator has moved from `split_first` & `to_u16` to `map_err`:
parse_with_try_op_v2 : Str -> Result Person [InvalidRecordFormat, InvalidNameFormat, InvalidBirthYearFormat]
parse_with_try_op_v2 = |line|
{ before: full_name, after: birth_year_str } =
line
|> Str.splitFirst " was born in "
|> Result.mapErr? \_ -> Err InvalidRecordFormat
(Str.split_first(line, " was born in ") |> Result.map_err(|_| InvalidRecordFormat))?
{ before: first_name, after: last_name } =
full_name
|> Str.splitFirst " "
|> Result.mapErr? \_ -> Err InvalidNameFormat
birth_year =
Str.toU16 birth_year_str
|> Result.mapErr? \_ -> Err InvalidBirthYearFormat
Ok { first_name, last_name, birth_year }
(Str.split_first(full_name, " ") |> Result.map_err(|_| InvalidNameFormat))?
birth_year = Result.map_err(Str.to_u16(birth_year_str), |_| InvalidBirthYearFormat)?
Ok({ first_name, last_name, birth_year })
## This function parses a string using a given parser and returns a string to
## display to the user. Note how we can handle errors individually or in bulk.
parse = \line, parser ->
when parser line is
Ok { first_name, last_name, birth_year } ->
parse = |line, parser|
when parser(line) is
Ok({ first_name, last_name, birth_year }) ->
"""
Name: $(last_name), $(first_name)
Born: $(birth_year |> Num.toStr)
Name: ${last_name}, ${first_name}
Born: ${Num.to_str(birth_year)}

"""
Err InvalidNameFormat -> "What kind of a name is this?"
Err InvalidBirthYearFormat -> "That birth year looks fishy."
Err InvalidRecordFormat -> "Oh wow, that's a weird looking record!"
Err(InvalidNameFormat) -> "What kind of a name is this?"
Err(InvalidBirthYearFormat) -> "That birth year looks fishy."
Err(InvalidRecordFormat) -> "Oh wow, that's a weird looking record!"
_ -> "Something unexpected happened" # Err NotFound or Err InvalidNumStr
main! = \_args ->
try Stdout.line! (parse "George Harrison was born in 1943" parse_verbose)
try Stdout.line! (parse "John Lennon was born in 1940" parse_with_try)
try Stdout.line! (parse "Paul McCartney was born in 1942" parse_with_try_v2)
try Stdout.line! (parse "Ringo Starr was born in 1940" parse_with_try_op)
try Stdout.line! (parse "Stuart Sutcliffe was born in 1940" parse_with_try_op_v2)
main! = |_args|
Stdout.line!(parse("George Harrison was born in 1943", parse_verbose))?
Stdout.line!(parse("John Lennon was born in 1940", parse_with_try))?
Stdout.line!(parse("Paul McCartney was born in 1942", parse_with_try_v2))?
Stdout.line!(parse("Ringo Starr was born in 1940", parse_with_try_op))?
Stdout.line!(parse("Stuart Sutcliffe was born in 1940", parse_with_try_op_v2))?
Ok({})
Ok {}
expect parse("George Harrison was born in 1943", parse_verbose) == "Name: Harrison, George\nBorn: 1943\n"
expect parse("John Lennon was born in 1940", parse_with_try) == "Name: Lennon, John\nBorn: 1940\n"
expect parse("Paul McCartney was born in 1942", parse_with_try_v2) == "Name: McCartney, Paul\nBorn: 1942\n"
expect parse("Ringo Starr was born in 1940", parse_with_try_op) == "Name: Starr, Ringo\nBorn: 1940\n"
expect parse("Stuart Sutcliffe was born in 1940", parse_with_try_op_v2) == "Name: Sutcliffe, Stuart\nBorn: 1940\n"
8 changes: 4 additions & 4 deletions examples/SafeMath/README.md
Original file line number Diff line number Diff line change
@@ -3,21 +3,21 @@
This example shows how to perform calculations while avoiding overflows.
For example; `+` actually uses `Num.add`, which can crash if the bytes of the result can not fit in the provided type:
```cli
» Num.maxU64 + Num.maxU64
» Num.max_u64 + Num.max_u64
This Roc code crashed with: "Integer addition overflowed!"
* : U64
```
If you want to avoid a program-ending crash, you can instead use:
```
» Num.addChecked Num.maxU64 Num.maxU64
» Num.add_checked Num.max_u64 Num.max_u64
Err Overflow : Result U64 [Overflow]
```
That would allow you to display a clean error to the user or handle the failure in an intelligent way.
Use `Checked` math functions if [reliability is important for your application](https://arstechnica.com/information-technology/2015/05/boeing-787-dreamliners-contain-a-potentially-catastrophic-software-bug/).
Use `checked` math functions if [reliability is important for your application](https://arstechnica.com/information-technology/2015/05/boeing-787-dreamliners-contain-a-potentially-catastrophic-software-bug/).

For a realistic demonstration, we will use `Checked` math functions to calculate the variance of a population.
For a realistic demonstration, we will use `checked` math functions to calculate the variance of a population.

The variance formula is: `σ² = ∑(X - µ)² / N` where:
- `σ²` = variance
55 changes: 28 additions & 27 deletions examples/SafeMath/main.roc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br" }
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br" }

import cli.Stdout

@@ -14,49 +14,50 @@ import cli.Stdout
## Performance note: safe or checked math prevents crashes but also runs slower.
##
safe_variance : List (Frac a) -> Result (Frac a) [EmptyInputList, Overflow]
safe_variance = \maybe_empty_list ->
safe_variance = |maybe_empty_list|

# Check length to prevent DivByZero
when List.len maybe_empty_list is
0 -> Err EmptyInputList
# Check length to prevent division by zero
when List.len(maybe_empty_list) is
0 -> Err(EmptyInputList)
_ ->
non_empty_list = maybe_empty_list

n = non_empty_list |> List.len |> Num.toFrac
n = non_empty_list |> List.len |> Num.to_frac

mean =
non_empty_list # sum of all elements:
|> List.walkTry 0.0 (\state, elem -> Num.addChecked state elem)
|> Result.map (\x -> x / n)
|> List.walk_try(0.0, |state, elem| Num.add_checked(state, elem))
|> Result.map_ok(|x| x / n)

non_empty_list
|> List.walkTry
0.0
(\state, elem ->
|> List.walk_try(
0.0,
|state, elem|
mean
|> Result.try (\m -> Num.subChecked elem m) # X - µ
|> Result.try (\y -> Num.mulChecked y y) # ²
|> Result.try (\z -> Num.addChecked z state)) # ∑
|> Result.map (\x -> x / n)
|> Result.try(|m| Num.sub_checked(elem, m)) # X - µ
|> Result.try(|y| Num.mul_checked(y, y)) # ²
|> Result.try(|z| Num.add_checked(z, state)), # ∑
)
|> Result.map_ok(|x| x / n)

main! = \_args ->
main! = |_args|

variance_result =
[46, 69, 32, 60, 52, 41]
|> safe_variance
|> Result.map Num.toStr
|> Result.map (\v -> "σ² = $(v)")
|> Result.map_ok(Num.to_str)
|> Result.map_ok(|v| "σ² = ${v}")

output_str =
when variance_result is
Ok str -> str
Err EmptyInputList -> "Error: EmptyInputList: I can't calculate the variance over an empty list."
Err Overflow -> "Error: Overflow: When calculating the variance, a number got too large to store in the available memory for the type."
Ok(str) -> str
Err(EmptyInputList) -> "Error: EmptyInputList: I can't calculate the variance over an empty list."
Err(Overflow) -> "Error: Overflow: When calculating the variance, a number got too large to store in the available memory for the type."

Stdout.line! output_str
Stdout.line!(output_str)

expect (safe_variance []) == Err EmptyInputList
expect (safe_variance [0]) == Ok 0
expect (safe_variance [100]) == Ok 0
expect (safe_variance [4, 22, 99, 204, 18, 20]) == Ok 5032.138888888888888888
expect (safe_variance [46, 69, 32, 60, 52, 41]) == Ok 147.666666666666666666
expect safe_variance([]) == Err(EmptyInputList)
expect safe_variance([0]) == Ok(0)
expect safe_variance([100]) == Ok(0)
expect safe_variance([4, 22, 99, 204, 18, 20]) == Ok(5032.138888888888888888)
expect safe_variance([46, 69, 32, 60, 52, 41]) == Ok(147.666666666666666666)
46 changes: 25 additions & 21 deletions examples/TowersOfHanoi/Hanoi.roc
Original file line number Diff line number Diff line change
@@ -7,47 +7,51 @@ State : {
from : Str, # identifier of the source rod
to : Str, # identifier of the target rod
using : Str, # identifier of the auxiliary rod
moves : List (Str, Str), # list of moves accumulated so far
moves : List (Str, Str), # list of moves done so far
}

## Solves the Tower of Hanoi problem using recursion. Returns a list of moves
## which represent the solution.
hanoi : State -> List (Str, Str)
hanoi = \{ num_disks, from, to, using, moves } ->
hanoi = |{ num_disks, from, to, using, moves }|
if num_disks == 1 then
List.concat moves [(from, to)]
List.concat(moves, [(from, to)])
else
moves1 = hanoi {
num_disks: (num_disks - 1),
from,
to: using,
using: to,
moves,
}
moves1 = hanoi(
{
num_disks: (num_disks - 1),
from,
to: using,
using: to,
moves,
},
)

moves2 = List.concat moves1 [(from, to)]
moves2 = List.concat(moves1, [(from, to)])

hanoi {
num_disks: (num_disks - 1),
from: using,
to,
using: from,
moves: moves2,
}
hanoi(
{
num_disks: num_disks - 1,
from: using,
to,
using: from,
moves: moves2,
},
)

start = { num_disks: 0, from: "A", to: "B", using: "C", moves: [] }

## Test Case 1: Tower of Hanoi with 1 disk
expect
actual = hanoi { start & num_disks: 1 }
actual = hanoi({ start & num_disks: 1 })
actual
== [
("A", "B"),
]

## Test Case 2: Tower of Hanoi with 2 disks
expect
actual = hanoi { start & num_disks: 2 }
actual = hanoi({ start & num_disks: 2 })
actual
== [
("A", "C"),
@@ -57,7 +61,7 @@ expect

## Test Case 3: Tower of Hanoi with 3 disks
expect
actual = hanoi { start & num_disks: 3 }
actual = hanoi({ start & num_disks: 3 })
actual
== [
("A", "B"),
28 changes: 14 additions & 14 deletions examples/Tuples/main.roc
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br" }
app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br" }

import pf.Stdout

main! = \_args ->
main! = |_args|

# a tuple that contains different types
# a tuple that contains three different types
simple_tuple : (Str, Bool, I64)
simple_tuple = ("A String", Bool.true, 15_000_000)

# access the items in a tuple by index (starts at 0)
first_item = simple_tuple.0
second_item = if simple_tuple.1 then "true" else "false"
third_item = Num.toStr simple_tuple.2
third_item = Num.to_str(simple_tuple.2)

try
Stdout.line!
"""
First is: $(first_item),
Second is: $(second_item),
Third is: $(third_item).
Stdout.line!(
"""
First is: ${first_item},
Second is: ${second_item},
Third is: ${third_item}.
""",
)?

# You can also use tuples with `when`:
fruit_selection : [Apple, Pear, Banana]
@@ -29,7 +29,7 @@ main! = \_args ->

when (fruit_selection, quantity) is
# TODO re-enable when github.com/roc-lang/roc/issues/5530 is fixed.
# (_, qty) if qty == 0 -> Stdout.line! "You also have no fruit."
(Apple, _) -> Stdout.line! "You also have some apples."
(Pear, _) -> Stdout.line! "You also have some pears."
(Banana, _) -> Stdout.line! "You also have some bananas."
# (_, qty) if qty == 0 -> Stdout.line! "You have no fruit."
(Apple, _) -> Stdout.line!("You also have some apples.")
(Pear, _) -> Stdout.line!("You also have some pears.")
(Banana, _) -> Stdout.line!("You also have some bananas.")
18 changes: 9 additions & 9 deletions flake.lock