From 0f57673d880f2229fd4aee9b325498bfdbda6b3f Mon Sep 17 00:00:00 2001 From: Luke Boswell Date: Wed, 8 Jan 2025 16:54:33 +1100 Subject: [PATCH 01/11] update plans --- www/content/plans.md | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/www/content/plans.md b/www/content/plans.md index 6fdd1327ae2..25f5432097e 100644 --- a/www/content/plans.md +++ b/www/content/plans.md @@ -1,6 +1,8 @@ # Plans -This is not a roadmap, but it is a set of current plans for the language. Plans can change, of course, but the goal here is to lay out some of the current plans. It won't be an exhaustive list, but it should give you the highlights. +This is not a roadmap, but it is a set of current plans for the language. Plans can change, of course, but the goal here is to lay out some of the current plans. + +This is not an exhaustive list, it aims to highlight that there are significant changes to the language, tooling, and broader ecosystem ahead. ## [Planned Breaking Changes](#planned-breaking-changes) {#planned-breaking-changes} @@ -10,17 +12,25 @@ The best time to make breaking changes (that will benefit more and more people a ### [Builtins](#builtins) {#builtins} -Currently, [builtins](https://www.roc-lang.org/builtins) get breaking changes from time to time. There aren't any specific plans to make particular breaking changes to them, because typically when we decide a change is warranted, we discuss and implement the change pretty quickly. +Currently, [builtins](https://www.roc-lang.org/builtins) get breaking changes from time to time. + +The builtins are being migrated from `camelCase` to `snake_case`. This is a breaking change, and will likely take some time to be completed across the ecosystem of packages, platforms, and example code. + +### [Purity Inference](#purity-inference) {#purity-inference} + +Recently the changes outlined in the [design proposal for Purity Inference](https://docs.google.com/document/d/1ZVD3h5jLpQNFSDXTg2RkzPhNXz5EErUXBBjN8TuyiqQ/edit?usp=sharing) were released in the basic-cli and basic-webserver platforms (among others). + +Therefore `Task` is no longer required and has been deprecated. -As an example, we had [a discussion](https://roc.zulipchat.com/#narrow/stream/304641-ideas/topic/Drop.20n.20elements.20from.20the.20end.20of.20a.20list) about changing the API for how elements get dropped from a list, and then a week later [announced](https://roc.zulipchat.com/#narrow/stream/397893-announcements/topic/List.2Edrop.2C.20dropFirst.2C.20dropLast) that the change had shipped. +This is a significant upgrade to the language and will take some time for the ecosystem to upgrade. -This has been consistently happening a few times per year. It's hard to predict exactly what the next one will be, but it's a safe bet that it will happen again. +### [Static Dispatch](#static-dispatch) {#static-dispatch} -### [Import syntax](#import-syntax) {#import-syntax} +There are significant changes outlined in the [static dispatch design proposal](https://docs.google.com/document/d/1OUd0f4PQjH8jb6i1vEJ5DOnfpVBJbGTjnCakpXAYeT8/edit?usp=sharing). These are being discussed and worked on, but likely to affect other features like abilities, record default fields, and potentiall module params. -Implementing the very important [module params](https://docs.google.com/document/d/110MwQi7Dpo1Y69ECFXyyvDWzF4OYv1BLojIm08qDTvg/edit?usp=sharing) feature requires a breaking syntax change to how imports work. This plan is not at all tentative; there is a high degree of confidence that it will happen! +### [Syntax changes](#syntax-changes) {#syntax-changes} -Work has not started on this yet, but we'd like to have the project completed sometime in 2024. +There are a number of syntax changes that are planned, but not yet completed see [tracking issue](https://github.com/roc-lang/roc/issues/7106) for more information. ### [Shadowing](#shadowing) {#shadowing} From 3e7d1847ee7c12a33d42a43d9c13abb8889a6e03 Mon Sep 17 00:00:00 2001 From: Luke Boswell Date: Wed, 8 Jan 2025 17:56:53 +1100 Subject: [PATCH 02/11] some updates for snake_case and purity inference --- www/content/tutorial.md | 762 ++++++++++++++++++---------------------- 1 file changed, 349 insertions(+), 413 deletions(-) diff --git a/www/content/tutorial.md b/www/content/tutorial.md index 53142d67156..b8f4a88d932 100644 --- a/www/content/tutorial.md +++ b/www/content/tutorial.md @@ -20,6 +20,9 @@

Welcome to Roc!

This tutorial will teach you how to build Roc applications. Along the way, you'll learn how to write tests, use the REPL, and more!

+

Installation

Roc doesn’t have a numbered release or an installer yet, but you can follow the install instructions for your OS here . If you get stuck, friendly people will be happy to help if you open a topic in #beginners on Roc Zulip Chat and ask for assistance!

@@ -158,17 +161,17 @@ Let's move out of the REPL and create our first Roc application! Make a file named `main.roc` and put this in it: ```roc -app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.17.0/lZFLstMUCUvd5bjnnpYromZJXkQUrdhbva4xdBInicE.tar.br" } +app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br" } import pf.Stdout -main = +main! = \_args -> Stdout.line! "Hi there, from inside a Roc app. 🎉" ``` Try running this with: -roc dev +roc main.roc You should see a message about a file being downloaded, followed by this: @@ -187,11 +190,11 @@ iguanas = 2 total = Num.toStr (birds + iguanas) -main = +main! = \_args -> Stdout.line! "There are $(total) animals." ``` -Now run `roc dev` again. This time the "Downloading ..." message won't appear; the file has been cached from last time, and won't need to be downloaded again. +Now run `roc main.roc` again. This time the "Downloading ..." message won't appear; the file has been cached from last time, and won't need to be downloaded again. You should see this: @@ -225,16 +228,16 @@ birds = 3 iguanas = 2 -total = addAndStringify birds iguanas +total = add_and_stringify birds iguanas -main = +main! = \_args -> Stdout.line! "There are $(total) animals." -addAndStringify = \num1, num2 -> +add_and_stringify = \num1, num2 -> Num.toStr (num1 + num2) ``` -This new `addAndStringify` function we've defined accepts two numbers, adds them, calls `Num.toStr` on the result, and returns that. +This new `add_and_stringify` function we've defined accepts two numbers, adds them, calls `Num.toStr` on the result, and returns that. The `\num1, num2 ->` syntax defines a function's arguments, and the expression after the `->` is the body of the function. Whenever a function gets called, its body expression gets evaluated and returned. @@ -243,7 +246,7 @@ The `\num1, num2 ->` syntax defines a function's arguments, and the expression a Let's modify this function to return an empty string if the numbers add to zero. ```roc -addAndStringify = \num1, num2 -> +add_and_stringify = \num1, num2 -> sum = num1 + num2 if sum == 0 then @@ -254,7 +257,7 @@ addAndStringify = \num1, num2 -> We did two things here: -- We introduced a _local def_ named `sum`, and set it equal to `num1 + num2`. Because we defined `sum` inside `addAndStringify`, it's _local_ to that scope and can't be accessed outside that function. +- We introduced a _local def_ named `sum`, and set it equal to `num1 + num2`. Because we defined `sum` inside `add_and_stringify`, it's _local_ to that scope and can't be accessed outside that function. - We added an `if`\-`then`\-`else` conditional to return either `""` or `Num.toStr sum` depending on whether `sum == 0`. Every `if` must be accompanied by both `then` and also `else`. Having an `if` without an `else` is an error, because `if` is an expression, and all expressions must evaluate to a value. If there were ever an `if` without an `else`, that would be an expression that might not evaluate to a value! @@ -264,7 +267,7 @@ Every `if` must be accompanied by both `then` and also `else`. Having an `if` wi We can combine `if` and `else` to get `else if`, like so: ```roc -addAndStringify = \num1, num2 -> +add_and_stringify = \num1, num2 -> sum = num1 + num2 if sum == 0 then @@ -278,7 +281,7 @@ addAndStringify = \num1, num2 -> Note that `else if` is not a separate language keyword! It's just an `if`/`else` where the `else` branch contains another `if`/`else`. This is easier to see with different indentation: ```roc -addAndStringify = \num1, num2 -> +add_and_stringify = \num1, num2 -> sum = num1 + num2 if sum == 0 then @@ -297,7 +300,7 @@ This differently-indented version is equivalent to writing `else if sum < 0 then This is a comment in Roc: ```roc -# The 'name' field is unused by addAndStringify +# The 'name' field is unused by add_and_stringify ``` Whenever you write `#` it means that the rest of the line is a comment, and will not affect the @@ -318,12 +321,12 @@ Like other comments, doc comments do not affect the running program. ### [Records](#records) {#records} -Currently our `addAndStringify` function takes two arguments. We can instead make it take one argument like so: +Currently our `add_and_stringify` function takes two arguments. We can instead make it take one argument like so: ```roc -total = addAndStringify { birds: 5, iguanas: 7 } +total = add_and_stringify { birds: 5, iguanas: 7 } -addAndStringify = \counts -> +add_and_stringify = \counts -> Num.toStr (counts.birds + counts.iguanas) ``` @@ -339,19 +342,19 @@ When we use [`==`](/builtins/Bool#isEq) on records, it compares all the fields i ### [Accepting extra fields](#accepting-extra-fields) {#accepting-extra-fields} -The `addAndStringify` function will accept any record with at least the fields `birds` and `iguanas`, but it will also accept records with more fields. For example: +The `add_and_stringify` function will accept any record with at least the fields `birds` and `iguanas`, but it will also accept records with more fields. For example: ```roc -total = addAndStringify { birds: 5, iguanas: 7 } +total = add_and_stringify { birds: 5, iguanas: 7 } -# The `note` field is unused by addAndStringify -totalWithNote = addAndStringify { birds: 4, iguanas: 3, note: "Whee!" } +# The `note` field is unused by add_and_stringify +total_with_note = add_and_stringify { birds: 4, iguanas: 3, note: "Whee!" } -addAndStringify = \counts -> +add_and_stringify = \counts -> Num.toStr (counts.birds + counts.iguanas) ``` -This works because `addAndStringify` only uses `counts.birds` and `counts.iguanas`. If we were to use `counts.note` inside `addAndStringify`, then we would get an error because `total` is calling `addAndStringify` passing a record that doesn't have a `note` field. +This works because `add_and_stringify` only uses `counts.birds` and `counts.iguanas`. If we were to use `counts.note` inside `add_and_stringify`, then we would get an error because `total` is calling `add_and_stringify` passing a record that doesn't have a `note` field. ### [Record shorthands](#record-shorthands) {#record-shorthands} @@ -381,14 +384,14 @@ In these cases, we shorten it to writing the name of the def alone—for example We can use _destructuring_ to avoid naming a record in a function argument, instead giving names to its individual fields: ```roc -addAndStringify = \{ birds, iguanas } -> +add_and_stringify = \{ birds, iguanas } -> Num.toStr (birds + iguanas) ``` Here, we've _destructured_ the record to create a `birds` def that's assigned to its `birds` field, and an `iguanas` def that's assigned to its `iguanas` field. We can customize this if we like: ```roc -addAndStringify = \{ birds, iguanas: lizards } -> +add_and_stringify = \{ birds, iguanas: lizards } -> Num.toStr (birds + lizards) ``` @@ -406,14 +409,14 @@ So far we've only constructed records from scratch, by specifying all of their f ```roc original = { birds: 5, zebras: 2, iguanas: 7, goats: 1 } -fromScratch = { birds: 4, zebras: 2, iguanas: 3, goats: 1 } -fromOriginal = { original & birds: 4, iguanas: 3 } +from_scratch = { birds: 4, zebras: 2, iguanas: 3, goats: 1 } +from_original = { original & birds: 4, iguanas: 3 } ``` -The `fromScratch` and `fromOriginal` records are equal, although they're defined in different ways. +The `from_scratch` and `from_original` records are equal, although they're defined in different ways. -- `fromScratch` was built using the same record syntax we've been using up to this point. -- `fromOriginal` created a new record using the contents of `original` as defaults for fields that it didn't specify after the `&`. +- `from_scratch` was built using the same record syntax we've been using up to this point. +- `from_original` created a new record using the contents of `original` as defaults for fields that it didn't specify after the `&`. Note that `&` can't introduce new fields to a record, or change the types of existing fields. (Trying to do either of these will result in an error at build time!) @@ -496,7 +499,7 @@ third = tuple.2 # ["list"] Sometimes we want to represent that something can have one of several values. For example: ```roc -stoplightColor = +stoplight_color = if something > 0 then Red else if something == 0 then @@ -505,19 +508,19 @@ stoplightColor = Green ``` -Here, `stoplightColor` can have one of three values: `Red`, `Yellow`, or `Green`. The capitalization is very important! If these were lowercase (`red`, `yellow`, `green`), then they would refer to defs. However, because they are capitalized, they instead refer to _tags_. +Here, `stoplight_color` can have one of three values: `Red`, `Yellow`, or `Green`. The capitalization is very important! If these were lowercase (`red`, `yellow`, `green`), then they would refer to defs. However, because they are capitalized, they instead refer to _tags_. ### [Tags](#tags) {#tags} A tag is a literal value just like a number or a string. Similarly to how I can write the number `42` or the string `"forty-two"` without defining them first, I can also write the tag `FortyTwo` without defining it first. Also, similarly to how `42 == 42` and `"forty-two" == "forty-two"`, it's also the case that `FortyTwo == FortyTwo`. -Let's say we wanted to turn `stoplightColor` from a `Red`, `Green`, or `Yellow` into a string. Here's one way we could do that: +Let's say we wanted to turn `stoplight_color` from a `Red`, `Green`, or `Yellow` into a string. Here's one way we could do that: ```roc -stoplightStr = - if stoplightColor == Red then +stoplight_str = + if stoplight_color == Red then "red" - else if stoplightColor == Green then + else if stoplight_color == Green then "green" else "yellow" @@ -526,58 +529,58 @@ stoplightStr = We can express this logic more concisely using `when`/`is` instead of `if`/`then`: ```roc -stoplightStr = - when stoplightColor is +stoplight_str = + when stoplight_color is Red -> "red" Green -> "green" Yellow -> "yellow" ``` -This results in the same value for `stoplightStr`. In both the `when` version and the `if` version, we have three conditional branches, and each of them evaluates to a string. The difference is how the conditions are specified; here, we specify between `when` and `is` that we're making comparisons against `stoplightColor`, and then we specify the different things we're comparing it to: `Red`, `Green`, and `Yellow`. +This results in the same value for `stoplight_str`. In both the `when` version and the `if` version, we have three conditional branches, and each of them evaluates to a string. The difference is how the conditions are specified; here, we specify between `when` and `is` that we're making comparisons against `stoplight_color`, and then we specify the different things we're comparing it to: `Red`, `Green`, and `Yellow`. Besides being more concise, there are other advantages to using `when` here. 1. We don't have to specify an `else` branch, so the code can be more self-documenting about exactly what all the options are. -2. We get more compiler help. If we try deleting any of these branches, we'll get a compile-time error saying that we forgot to cover a case that could come up. For example, if we delete the `Green ->` branch, the compiler will say that we didn't handle the possibility that `stoplightColor` could be `Green`. It knows this because `Green` is one of the possibilities in our `stoplightColor = if ...` definition. +2. We get more compiler help. If we try deleting any of these branches, we'll get a compile-time error saying that we forgot to cover a case that could come up. For example, if we delete the `Green ->` branch, the compiler will say that we didn't handle the possibility that `stoplight_color` could be `Green`. It knows this because `Green` is one of the possibilities in our `stoplight_color = if ...` definition. We can still have the equivalent of an `else` branch in our `when` if we like. Instead of writing `else`, we write `_ ->` like so: ```roc -stoplightStr = - when stoplightColor is +stoplight_str = + when stoplight_color is Red -> "red" _ -> "not red" ``` -This lets us more concisely handle multiple cases. However, it has the downside that if we add a new case - for example, if we introduce the possibility of `stoplightColor` being `Orange`, the compiler can no longer tell us we forgot to handle that possibility in our `when`. After all, we are handling it - just maybe not in the way we'd decide to if the compiler had drawn our attention to it! +This lets us more concisely handle multiple cases. However, it has the downside that if we add a new case - for example, if we introduce the possibility of `stoplight_color` being `Orange`, the compiler can no longer tell us we forgot to handle that possibility in our `when`. After all, we are handling it - just maybe not in the way we'd decide to if the compiler had drawn our attention to it! We can make this `when` _exhaustive_ (that is, covering all possibilities) without using `_ ->` by using `|` to specify multiple matching conditions for the same branch: ```roc -stoplightStr = - when stoplightColor is +stoplight_str = + when stoplight_color is Red -> "red" Green | Yellow -> "not red" ``` -You can read `Green | Yellow` as "either `Green` or `Yellow`". By writing it this way, if we introduce the possibility that `stoplightColor` can be `Orange`, we'll get a compiler error telling us we forgot to cover that case in this `when`, and then we can handle it however we think is best. +You can read `Green | Yellow` as "either `Green` or `Yellow`". By writing it this way, if we introduce the possibility that `stoplight_color` can be `Orange`, we'll get a compiler error telling us we forgot to cover that case in this `when`, and then we can handle it however we think is best. We can also combine `if` and `when` to make branches more specific: ```roc -stoplightStr = - when stoplightColor is +stoplight_str = + when stoplight_color is Red -> "red" Green | Yellow if contrast > 75 -> "not red, but very high contrast" Green | Yellow if contrast > 50 -> "not red, but high contrast" Green | Yellow -> "not red" ``` -This will give the same answer for `stoplightStr` as if we had written the following: +This will give the same answer for `stoplight_str` as if we had written the following: ```roc -stoplightStr = - when stoplightColor is +stoplight_str = + when stoplight_color is Red -> "red" Green | Yellow -> if contrast > 75 then @@ -595,7 +598,7 @@ Either style can be a reasonable choice depending on the circumstances. Tags can have _payloads_—that is, values inside them. For example: ```roc -stoplightColor = +stoplight_color = if something > 100 then Red else if something > 0 then @@ -605,16 +608,16 @@ stoplightColor = else Custom "some other color" -stoplightStr = - when stoplightColor is +stoplight_str = + when stoplight_color is Red -> "red" Green | Yellow -> "not red" Custom description -> description ``` -This makes two changes to our earlier `stoplightColor` / `stoplightStr` example. +This makes two changes to our earlier `stoplight_color` / `stoplight_str` example. -1. We sometimes chose to set `stoplightColor` to be `Custom "some other color"`. When we did this, we gave the `Custom` tag a _payload_ of the string `"some other color"`. +1. We sometimes chose to set `stoplight_color` to be `Custom "some other color"`. When we did this, we gave the `Custom` tag a _payload_ of the string `"some other color"`. 2. We added a `Custom` tag in our `when`, with a payload which we named `description`. Because we did this, we were able to refer to `description` in the body of the branch (that is, the part after the `->`) just like a def or a function argument. Any tag can be given a payload like this. A payload doesn't have to be a string; we could also have said (for example) `Custom { r: 40, g: 60, b: 80 }` to specify an RGB color instead of a string. Then in our `when` we could have written `Custom record ->` and then after the `->` used `record.r`, `record.g`, and `record.b` to access the `40`, `60`, `80` values. We could also have written `Custom { r, g, b } ->` to _destructure_ the record, and then accessed these `r`, `g`, and `b` defs after the `->` instead. @@ -849,6 +852,26 @@ listGet = \index -> `Result.try` is often used to chain two functions that return `Result` (as in the example above). This prevents you from needing to add error handling code at every intermediate step. +### [The `try` keyword](#the-try-keyword) {#the-try-keyword} + +Roc has a `try` keyword, which is convenient syntax sugar for `Result.try`. For example, consider the following `get_letter` function: + +```roc +get_letter : Str -> Result Str [OutOfBounds, InvalidNumStr] +get_letter = \index_str -> + index = try Str.toU64 index_str + List.get ["a", "b", "c", "d"] index +``` + +Here's what this does: + +- If the `Str.toU64` function returns an `Ok` value, then `try` will return what's inside the `Ok`. For example: + - If we call `get_letter "2"`, then `Str.toU64` returns `Ok 2`, and the `try` unwraps to the integer 2, so `index` is set to 2 (not `Ok 2`). Then the `List.get` function is called and returns `Ok "c"`. + - If the `Str.toU64` function returns an `Err` value, then the `try` keyword immediately interrupts the `get_letter` function and makes it return this error. + - For example, if we call `get_letter "abc"`, then the call to `Str.toU64` returns `Err InvalidNumStr`, and the `try` keyword ensures that the `get_letter` function returns this error immediately, without executing the rest of the function. + +Thanks to the `try` keyword, your code can focus on the "happy path" (where nothing fails) and simply bubble up to the caller any error that might occur. Your error handling code can be neatly separated, and you can rest assured that you won't forget to handle any errors, since the compiler will let you know. See this [code example](https://github.com/roc-lang/examples/blob/main/examples/Results/main.roc) for more details on error handling. + Now let's get back to lists! ### [Walking the elements in a list](#walking-the-elements-in-a-list) {#walking-the-elements-in-a-list} @@ -913,7 +936,7 @@ A helpful way to remember the argument order for `List.walk` is that that its ar You can also pattern match on lists, like so: ```roc -when myList is +when my_list is [] -> 0 # the list is empty [Foo, ..] -> 1 # it starts with a Foo tag [_, ..] -> 2 # it contains at least one element, which we ignore @@ -989,9 +1012,9 @@ All operators in Roc are syntax sugar for normal function calls. See the [Operat Sometimes you may want to document the type of a definition. For example, you might write: ```roc -# Takes a firstName string and a lastName string, and returns a string -fullName = \firstName, lastName -> - "$(firstName) $(lastName)" +# Takes a first_name string and a last_name string, and returns a string +full_name = \first_name, last_name -> + "$(first_name) $(last_name)" ``` Comments can be valuable documentation, but they can also get out of date and become misleading. If someone changes this function and forgets to update the comment, it will no longer be accurate. @@ -1001,35 +1024,35 @@ Comments can be valuable documentation, but they can also get out of date and be Here's another way to document this function's type, which doesn't have that problem: ```roc -fullName : Str, Str -> Str -fullName = \firstName, lastName -> - "$(firstName) $(lastName)" +full_name : Str, Str -> Str +full_name = \first_name, last_name -> + "$(first_name) $(last_name)" ``` -The `fullName :` line is a _type annotation_. It's a strictly optional piece of metadata we can add above a def to describe its type. Unlike a comment, the Roc compiler will check type annotations for accuracy. If the annotation ever doesn't fit with the implementation, we'll get a compile-time error. +The `full_name :` line is a _type annotation_. It's a strictly optional piece of metadata we can add above a def to describe its type. Unlike a comment, the Roc compiler will check type annotations for accuracy. If the annotation ever doesn't fit with the implementation, we'll get a compile-time error. -The annotation `fullName : Str, Str -> Str` says "`fullName` is a function that takes two strings as arguments and returns a string." +The annotation `full_name : Str, Str -> Str` says "`full_name` is a function that takes two strings as arguments and returns a string." We can give type annotations to any value, not just functions. For example: ```roc -firstName : Str -firstName = "Amy" +first_name : Str +first_name = "Amy" -lastName : Str -lastName = "Lee" +last_name : Str +last_name = "Lee" ``` -These annotations say that both `firstName` and `lastName` have the type `Str`. +These annotations say that both `first_name` and `last_name` have the type `Str`. -We can annotate records similarly. For example, we could move `firstName` and `lastName` into a record like so: +We can annotate records similarly. For example, we could move `first_name` and `last_name` into a record like so: ```roc -amy : { firstName : Str, lastName : Str } -amy = { firstName: "Amy", lastName: "Lee" } +amy : { first_name : Str, last_name : Str } +amy = { first_name: "Amy", last_name: "Lee" } -jen : { firstName : Str, lastName : Str } -jen = { firstName: "Jen", lastName: "Majura" } +jen : { first_name : Str, last_name : Str } +jen = { first_name: "Jen", last_name: "Majura" } ``` ### [Type Aliases](#type-aliases) {#type-aliases} @@ -1037,16 +1060,16 @@ jen = { firstName: "Jen", lastName: "Majura" } When we have a recurring type annotation like this, it can be nice to give it its own name. We do this like so: ```roc -Musician : { firstName : Str, lastName : Str } +Musician : { first_name : Str, last_name : Str } amy : Musician -amy = { firstName: "Amy", lastName: "Lee" } +amy = { first_name: "Amy", last_name: "Lee" } simone : Musician -simone = { firstName: "Simone", lastName: "Simons" } +simone = { first_name: "Simone", last_name: "Simons" } ``` -Here, `Musician` is a _type alias_. A type alias is like a def, except it gives a name to a type instead of to a value. Just like how you can read `name : Str` as "`name` has the type `Str`," you can also read `Musician : { firstName : Str, lastName : Str }` as "`Musician` has the type `{ firstName : Str, lastName : Str }`." +Here, `Musician` is a _type alias_. A type alias is like a def, except it gives a name to a type instead of to a value. Just like how you can read `name : Str` as "`name` has the type `Str`," you can also read `Musician : { first_name : Str, last_name : Str }` as "`Musician` has the type `{ first_name : Str, last_name : Str }`." ### [Type Parameters](#type-parameters) {#type-parameters} @@ -1057,7 +1080,7 @@ names : List Str names = ["Amy", "Simone", "Tarja"] ``` -You can read `List Str` as "a list of strings." Here, `Str` is a _type parameter_ that tells us what type of `List` we're dealing with. `List` is a _parameterized type_, which means it's a type that requires a type parameter. There's no way to give something a type of `List` without a type parameter. You have to specify what type of list it is, such as `List Str` or `List Bool` or `List { firstName : Str, lastName : Str }`. +You can read `List Str` as "a list of strings." Here, `Str` is a _type parameter_ that tells us what type of `List` we're dealing with. `List` is a _parameterized type_, which means it's a type that requires a type parameter. There's no way to give something a type of `List` without a type parameter. You have to specify what type of list it is, such as `List Str` or `List Bool` or `List { first_name : Str, last_name : Str }`. ### [Wildcard Types (\*)](#wildcard-type) {#wildcard-type} @@ -1114,8 +1137,8 @@ Similarly, the only way to have a function whose type is `a -> a` is if the func We can also annotate types that include tags: ```roc -colorFromStr : Str -> [Red, Green, Yellow] -colorFromStr = \string -> +color_from_str : Str -> [Red, Green, Yellow] +color_from_str = \string -> when string is "red" -> Red "green" -> Green @@ -1127,8 +1150,8 @@ You can read the type `[Red, Green, Yellow]` as "a tag union of the tags `Red`, Some tag unions have only one tag in them. For example: ```roc -redTag : [Red] -redTag = Red +red_tag : [Red] +red_tag = Red ``` ### [Accumulating Tag Types](#accumulating-tag-types) {#accumulating-tag-types} @@ -1175,16 +1198,16 @@ You can create an opaque type with the `:=` operator. Let's make one called `Use ```roc Username := Str -fromStr : Str -> Username -fromStr = \str -> +from_str : Str -> Username +from_str = \str -> @Username str -toStr : Username -> Str -toStr = \@Username str -> +to_str : Username -> Str +to_str = \@Username str -> str ``` -The `fromStr` function turns a string into a `Username` by calling `@Username` on that string. The `toStr` function turns a `Username` back into a string by pattern matching `@Username str` to unwrap the string from the `Username` opaque type. +The `from_str` function turns a string into a `Username` by calling `@Username` on that string. The `to_str` function turns a `Username` back into a string by pattern matching `@Username str` to unwrap the string from the `Username` opaque type. Now we can expose the `Username` opaque type so that other modules can use it in type annotations. However, other modules can't use the `@Username` syntax to wrap or unwrap `Username` values. That operation is only available in the same scope where `Username` itself was defined; trying to use it outside that scope will give an error. @@ -1348,7 +1371,7 @@ You can intentionally crash a Roc program, for example inside a conditional bran ```roc answer : Str answer = - when Str.fromUtf8 definitelyValidUtf8 is + when Str.fromUtf8 definitely_valid_utf8 is Ok str -> str Err _ -> crash "This should never happen!" ``` @@ -1384,12 +1407,12 @@ You can write automated tests for your Roc code like so: ```roc pluralize = \singular, plural, count -> - countStr = Num.toStr count + count_str = Num.toStr count if count == 1 then - "$(countStr) $(singular)" + "$(count_str) $(singular)" else - "$(countStr) $(plural)" + "$(count_str) $(plural)" expect pluralize "cactus" "cacti" 1 == "1 cactus" @@ -1413,14 +1436,14 @@ Expects do not have to be at the top level: ```roc pluralize = \singular, plural, count -> - countStr = Num.toStr count + count_str = Num.toStr count if count == 1 then - "$(countStr) $(singular)" + "$(count_str) $(singular)" else expect count > 0 - "$(countStr) $(plural)" + "$(count_str) $(plural)" ``` This `expect` will fail if you call `pluralize` passing a count of 0. @@ -1478,7 +1501,7 @@ There are several modules that are built into the Roc compiler, which are import 10. [Hash](https://www.roc-lang.org/builtins/Hash) 11. [Box](https://www.roc-lang.org/builtins/Box) 12. [Inspect](https://www.roc-lang.org/builtins/Inspect) -13. [Task](https://www.roc-lang.org/builtins/Task) +13. [Task](https://www.roc-lang.org/builtins/Task) (deprecated and will be remove soon) You may have noticed that we already used the first five. For example, when we wrote `Str.concat` and `Num.isEven`, we were referencing functions stored in the `Str` and `Num` modules. @@ -1494,14 +1517,14 @@ Besides being built into the compiler, the builtin modules are different from ot Let's take a closer look at the part of `main.roc` above the `main` def: ```roc -app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.17.0/lZFLstMUCUvd5bjnnpYromZJXkQUrdhbva4xdBInicE.tar.br" } +app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br" } import pf.Stdout ``` This is known as a _module header_. Every `.roc` file is a _module_, and there are different types of modules. We know this particular one is an _application module_ because it begins with the `app` keyword. -The line `app [main]` shows that this module is a Roc application and which [platform](https://github.com/roc-lang/roc/wiki/Roc-concepts-explained#platform) it is built on. +The line `app [main!]` shows that this module is a Roc application and which [platform](https://github.com/roc-lang/roc/wiki/Roc-concepts-explained#platform) it is built on. The `{ pf: platform "https://...tar.br" }` part says four things: @@ -1515,14 +1538,15 @@ The `import pf.Stdout` line says that we want to import the `Stdout` module from This import has a direct interaction with our definition of `main`. Let's look at that again: ```roc -main = Stdout.line! "Hi there, from inside a Roc app. 🎉" +main! = \_args -> + Stdout.line! "Hi there, from inside a Roc app. 🎉" ``` -Here, `main` is calling a function called `Stdout.line`. More specifically, it's calling a function named `line` which is exposed by a module named `Stdout`. +Here, `main!` is calling a function called `Stdout.line!`. More specifically, it's calling a function named `line!` which is exposed by a module named `Stdout`. When we write `import pf.Stdout`, it specifies that the `Stdout` module comes from the package we named `pf` in the `packages { pf: ... }` section. -You can find documentation for the `Stdout.line` function in the [Stdout](https://www.roc-lang.org/packages/basic-cli/Stdout#line) module documentation. +You can find documentation for the `Stdout.line!` function in the [Stdout](https://www.roc-lang.org/packages/basic-cli/Stdout#line) module documentation. If we would like to include other modules in our application, say `AdditionalModule.roc` and `AnotherModule.roc`, then they can be imported directly like this: @@ -1541,9 +1565,9 @@ import uuid.Generate as Uuid ...and the `exposing` keyword to bring values or functions into the current scope: ```roc -import pf.Stdout exposing [line] +import pf.Stdout exposing [line!] -main = +main! = \_args -> line! "Hi there, from inside a Roc app. 🎉" ``` @@ -1577,99 +1601,110 @@ Including the hash solves a number of problems: \[This part of the tutorial has not been written yet. Coming soon!\] -See [Html module](https://github.com/roc-lang/roc/blob/main/examples/virtual-dom-wip/platform/Html.roc) for an example. - ### [Platform Modules](#platform-modules) {#platform-modules} \[This part of the tutorial has not been written yet. Coming soon!\] -See [Platform Switching Rust](https://github.com/roc-lang/roc/blob/main/examples/platform-switching/rust-platform/main.roc) for an example. - ### [Importing Files](#importing-files) {#importing-files} You can import files directly into your module as a `Str` or a `List U8` at compile time. This is can be useful when working with data you would like to keep in a separate file, e.g. JSON or YAML configuration. ```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 ``` See the [Ingest Files Example](https://www.roc-lang.org/examples/IngestFiles/README.html) for a demonstration on using this feature. -## [Tasks](#tasks) {#tasks} +## [Effectful functions](#efffectful-functions) {#efffectful-functions} + +There are two types of functions in roc, "pure" and "effectful". Consider these two functions: + +```roc +with_extension : Str -> Str +with_extension = \filename -> + "$(filename).roc" + +read_file! : Str => Str +read_file! = \path -> + File.read_utf8! with_extension(path) |> Result.withDefault "" +``` + +Notice the subtle difference in these functions' types: -Note: [Tasks will soon dissolve entirely for app developers](#purity-inference), but today they're still critical. +- Str `->` Str is the type of the pure function `with_extension` +- Str `=>` Str is the type of the effectful function `read_file!` -Tasks are provided in a builtin `Task` module like the `List`, `Str` modules. They're an important part of building Roc applications, so let's continue using the [basic-cli](https://github.com/roc-lang/basic-cli) platform we've been using up to this point as an example! +The arrow in the type tells you whether a function is pure or might perform effects. Pure functions use `->` and effectful functions use `=>`. Effectful functions can call either pure functions or other effectful functions, but pure functions can only call other pure functions. (The fact that pure functions can only call pure functions is part of the definition of pure functions; Roc is just reflecting that fact in the type system.) -In the `basic-cli` platform, here are four operations we can do: +The `!` is a suffix character inside function names themselves, marking them as effectful. This is just a naming convention, though it is enforced by the compiler with a warning. + +Note: this was a previous change to the language, and `!` used to be an operator, so you may still find code that as a `Task` in it or an effectful function like `main` without a `!` suffix. This code should be upgraded to use `Result` and the naming convention for purity inference, e.g. `main!`. For more information about this change, see [this recent talk](https://www.youtube.com/watch?v=42TUAKhzlRI). + +Let's look at some examples. In the `basic-cli` platform, here are four operations we can do: - Write a string to the terminal - Read a string from user input - Write a string to a file - Read a string from a file -We'll use these four operations to learn about tasks. +We'll use these four operations to learn about effects. Let's revisit that first application. ```roc -app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.17.0/lZFLstMUCUvd5bjnnpYromZJXkQUrdhbva4xdBInicE.tar.br" } +app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br" } import pf.Stdout -main = +main! = \_args -> Stdout.line! "Hi there, from inside a Roc app. 🎉" ``` This code prints "Hi there, from inside a Roc app. 🎉" to the [standard output](). `Stdout.line` has this type: ```roc -Stdout.line : Str -> Task {} * +Stdout.line! : Str => Result {} [StdoutErr IOErr] ``` -A `Task` represents an _effect_; an interaction with state outside your Roc program, such as the terminal's standard output, or a file. - -The `!` after `Stdout.line` is an operator, similar to the [`?` "try" operator](https://www.roc-lang.org/tutorial#error-handling), but it is used on functions that return a `Task` instead of a `Result` (we'll discuss [the `!` operator in more depth](https://www.roc-lang.org/tutorial#the-!-"await"-operator) later in this tutorial). +An effectulful function is capable of interacting with state outside your Roc program, such as the terminal's standard output, or a file. -When we set `main` to be a `Task`, the task will get run when we run our program. Here, we've set `main` to be a task that writes `"Hi there, from inside a Roc app. 🎉"` to `stdout` when it gets run, so that's what our program does! +When we call `main!`, the host will provide the arguments passed from the cli `_args` (here we're just ignoring these), and then run. Here, we've set `main!` to be a task that writes `"Hi there, from inside a Roc app. 🎉"` to `stdout` when it gets run, so that's what our program does! -`Task` has two type parameters: the type of value it produces when it finishes running, and any errors that might happen when running it. `Stdout.line` has the type `Task {} *` because it doesn't produce any values when it finishes (hence the `{}`) and there aren't any errors that can happen when it runs (hence the `*`). +The `Stdout.line!` function here returns a `Result` when called. If it succeeds it returns the unit value `{}`, or if it fails it returns the tag `StdoutErr` with an `IOErr` payload. -In contrast, when `Stdin.line` finishes reading a line from [standard input](), it produces either a `Str` or else `End` if standard input reached its end (which can happen if the user types Ctrl+D on UNIX systems or Ctrl+Z on Windows). Those two possibilities are reflected in its type: +In contrast, when `Stdin.line! : {} => Result Str [EndOfFile, StdinErr IOErr]` finishes reading a line from [standard input](), it produces either a `Str` or else `EndOfFile` or a `StdinErr` error tag if standard input reached its end (which can happen if the user types Ctrl+D on UNIX systems or Ctrl+Z on Windows). These possibilities can all be seen just in the type definition of the function. -```roc -Stdin.line : Task [Input Str, End] * -``` +Note that to call this `Stdin.line!`, you need to provide the unit value `{}` as an argument. -Once this task runs, we'll end up with the [tag union](https://www.roc-lang.org/tutorial#tags-with-payloads) `[Input Str, End]`. Then we can check whether we got an `End` or some actual `Input`, and print out a message accordingly. +### [Reading values](#reading-values) {#reading-values} -### [Reading values from tasks](#task-input) {#task-input} - -Let's change `main` to read a line from `stdin`, and then print what we got: +Let's change `main!` to read a line from `stdin`, and then print what we got: ```roc -app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.17.0/lZFLstMUCUvd5bjnnpYromZJXkQUrdhbva4xdBInicE.tar.br" } +app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br" } import pf.Stdout import pf.Stdin -main = - Stdout.line! "Type in something and press Enter:" - input = Stdin.line! - Stdout.line! "Your input was: $(input)" +main! = \_args -> + try Stdout.line! "Type in something and press Enter:" + input = try Stdin.line! {} + try Stdout.line! "Your input was: $(input)" + + Ok {} ``` If you run this program, it will print "Type in something and press Enter:" and then pause. That's because it's waiting for you to type something in and press Enter! Once you do, it should print back out what you entered. -### [Task failure](#task-failure) {#task-failure} +### [Effectul failure](#failure) {#failure} -Sometimes, tasks can fail. For example, a task for reading from a file might fail if the file is not found. +Sometimes, effects can fail. For example, reading from a file might fail if the file is not found. Even reading from stdin and writing to stdout can fail! -For example, the `Stdin.line` task can fail if stdin is closed before it receives a line. You can try this out +For example, the `Stdin.line!` task can fail if stdin is closed before it receives a line. You can try this out by running the program and pressing Ctrl+Z on Windows, or Ctrl+D on macOS or Linux. (Press that key rather than typing in text and pressing Enter.) You'll see a default error message, which the `basic-cli` platform provides in case an error occurs that we didn't handle. @@ -1677,25 +1712,29 @@ in case an error occurs that we didn't handle. Although this default error handling behavior might be exactly what we want if we're writing a quick script, high-quality programs handle errors gracefully. Fortunately, we can do this nicely in Roc! -If we wanted to add the type annotation to `main` that Roc is inferring for it, we would add this annotation: +If we wanted to add the type annotation to `main!` that Roc is inferring for it, we would add this annotation: ```roc -main : Task {} [Exit I32 Str, StdoutErr Stdout.Err, StdinErr Stdin.Err] -main = +import pf.Stdout +import pf.Stdin +import pf.Arg exposing [Arg] + +main! : List Arg => Result {} [EndOfFile, StdinErr Stdin.IOErr, StdoutErr Stdout.IOErr] +main = #... ``` Let's break down what this type is saying: -- `Task` tells us this is a `Task` type. Its two type parameters are just like the ones we saw in `Result` earlier: the first type tells us what this task will produce if it succeeds, and the other one tells us what it will produce if it fails. -- `{}` tells us that this task always produces an empty record when it succeeds. (That is, it doesn't produce anything useful. Empty records don't have any information in them!) This is because the last task in `main` comes from `Stdout.line`, which doesn't produce anything. (In contrast, the `Stdin` task's first type parameter is a `Str`, because it produces a `Str` if it succeeds.) -- `[Exit I32 Str, StdoutErr Stdout.Err, StdinErr Stdin.Err]` tells us the different ways this task can fail. The `StdoutErr` and `StdinErr` tags are there because we used `Stdout.line` and `Stdin.line`. We'll talk about `Exit I32 Str` more in a moment. +- Both the `!` suffix in the name, and the `=>` in the type, tell us this is an effectful function. Its two type parameters are just like the ones we saw in `Result` earlier: the first type tells us what this task will produce if it succeeds, and the other one tells us what it will produce if it fails. +- `{}` tells us that this function always produces an empty record when it succeeds. (That is, it doesn't produce anything useful. Empty records don't have any information in them!) This is because the last task in `main!` comes from `Stdout.line!`, which doesn't produce anything. (In contrast, the `Stdin` task's first type parameter is a `Str`, because it produces a `Str` if it succeeds.) +- `[EndOfFile, StdinErr Stdin.IOErr, StdoutErr Stdout.IOErr]` tells us the different ways this task can fail. The `StdoutErr` and `StdinErr` tags are there because we used `Stdout.line!` and `Stdin.line!`. -To understand what the `Exit I32 Str` error means, let's try temporarily commenting out our current `main` and replacing +To understand the error a little more, let's try temporarily commenting out our current `main!` and replacing it with this one: ```roc -main : Task {} [Exit I32 Str] -main = Task.err (Exit 42 "An error happened!") +main! : _ => Result {} [Exit I32 Str] +main! = \_args -> Err (Exit 42 "An error happened!") ``` Now if we run the application, it will print the line "An error happened!" to stderr and exit with a status code of 42. (You can check the status code of the most recent terminal command that finished in Windows by running `echo %ERRORLEVEL%` (or `$LASTEXITCODE` in PowerShell), or by running `echo $?` in macOS or Linux.) @@ -1703,92 +1742,81 @@ Now if we run the application, it will print the line "An error happened!" to st Now let's try running it with this version of `main`: ```roc -main : Task {} [Exit I32 Str] -main = Task.ok {} +main! : _ => Result {} [Exit I32 Str] +main! = \_args -> Ok {} ``` This program won't print anything at all, but it will exit with a status code of `0`, indicating success. In summary: -- If the `main` task ends in a `Task.ok {}`, then it means the final task succeeded and the program will exit with status code 0. -- If the `main` task ends in a `Task.err (Exit 42 "…")`, then it means the final task failed, and the only information we got about the failure was that the program should exit with code 42 instead of 0, and that it should print a particular string to stderr to inform the user about what happened. +- If the `main!` task ends in a `Ok {}`, then it means the final task succeeded and the program will exit with status code 0. +- If the `main!` task ends in a `Err (Exit 42 "…")`, then it means it failed, and the only information we got about the failure was that the program should exit with code 42 instead of 0, and that it should print a particular string to stderr to inform the user about what happened. -### [Handling task failures](#handling-task-failures) {#handling-task-failures} +### [Handling task failures](#handling-failures) {#handling-failures} -If the `main` task ends up failing with any other errors besides `Exit` (such as `StdoutErr` or `StdinErr`), then the `basic-cli` platform's automatic error handling will handle them by printing out words taken from the source code (such as "StdoutErr" and "StdinErr"), which could lead to a bad experience for people using this program! +If `main!` ends up failing with any other errors besides `Exit` (such as `StdoutErr` or `StdinErr`), then the `basic-cli` platform's automatic error handling will handle them by printing out words taken from the source code (such as "StdoutErr" and "StdinErr"), which could lead to a bad experience for people using this program! -We can prevent that by gracefully handling the other error types, and then translating them into `Exit` errors so that they affect the program's exit code and don't result in the platform printing anything. A convenient way to make sure we've handled all the other errors is to keep our current type annotation for `main` but restore our old implementation: +We can prevent that by gracefully handling the other error types, and then translating them into `Exit` errors so that they affect the program's exit code and don't result in the platform printing anything. + +A convenient way to make sure we've handled all the other errors is to keep our current type annotation for `main!` but restore our old implementation: ```roc -main : Task {} [Exit I32 Str] -main = - Stdout.line! "Type in something and press Enter:" - input = Stdin.line! - Stdout.line! "Your input was: $(input)" +main! : List Arg => Result {} [Exit I32 Str] +main! = \_args -> + try Stdout.line! "Type in something and press Enter:" + input = try Stdin.line! {} + try Stdout.line! "Your input was: $(input)" + + Ok {} ``` -Adding this type annotation will give us a type mismatch - which is exactly what we want in this case! The type mismatch is telling us that we're claiming the `main` task will only ever fail with an `Exit` tag, but this implementation can _also_ fail with `StdoutErr` and `StdinErr` tags. +Adding this type annotation will give us a type mismatch - which is exactly what we want in this case! + +The type mismatch is telling us that we're claiming the `main!` task will only ever fail with an `Exit` tag, but this implementation can _also_ fail with `EndOfFile`, `StdoutErr` and `StdinErr` tags we saw earlier. -In other words, adding this annotation effectively opted us out of `basic-cli`'s default error handling. Now any potential task failures (now and in the future) will have to be handled somehow; if we forget to handle any, we'll get a type mismatch like this! For that reason, `basic-cli` applications that are intended to be high-quality (so, not things like quick scripts) will generally benefit from applying this type annotation to `main`. +In other words, adding this annotation effectively opted us out of `basic-cli`'s default error handling. Now any potential task failures (now and in the future) will have to be handled somehow; if we forget to handle any, we'll get a type mismatch like this! For that reason, `basic-cli` applications that are intended to be high-quality (so, not things like quick scripts) will generally benefit from applying this type annotation to `main!`. Here's one way we can handle those errors: ```roc -main : Task {} [Exit I32 Str] -main = - task = - Stdout.line! "Type in something and press Enter:" - input = Stdin.line! - Stdout.line! "Your input was: $(input)" - - Task.mapErr task \err -> - when err is - StdoutErr _ -> Exit 1 "Error writing to stdout." - StdinErr _ -> Exit 2 "Error writing to stdin." -``` +app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br" } -The `Task.mapErr` function translates one error into another. Here, we're translating the `StdoutErr` and `StdinErr` errors into `Exit` errors which include a different exit code plus a message that will print to stderr to explain what happened. - -We could also use `Task.onErr` instead, which is like `mapErr` except instead of returning a new error, it returns an entirely new `Task`. This means each branch of our `when` gets a bit more verbose, but it does mean we can run additional tasks before exiting. For example: +import pf.Stdout +import pf.Stdin +import pf.Arg exposing [Arg] -```roc - Task.onErr task \err -> +main! : List Arg => Result {} [Exit I32 Str] +main! = \_args -> + Result.mapErr (my_task! {}) \err -> when err is - StdoutErr _ -> Task.err (Exit 1 "Error writing to stdout.") - StdinErr _ -> - # Here we could now run some other tasks, - # e.g. to log the error to disk. + StdoutErr _ -> Exit 1i32 "Error writing to stdout." + StdinErr _ -> Exit 2i32 "Error writing to stdin." + EndOfFile -> Exit 3i32 "End of file reached." - Task.err (Exit 2 "Error writing to stdin.") +my_task! : {} => Result {} [EndOfFile, StdinErr _, StdoutErr _] +my_task! = \{} -> + try Stdout.line! "Type in something and press Enter:" + input = try Stdin.line! {} + try Stdout.line! "Your input was: $(input)" + + Ok {} ``` -Since we don't have any extra tasks to run, `mapErr` is more concise because we don't have to say `Task.err` at the end of each branch. +The `Result.mapErr` function translates one error into another. Here, we're translating the `EndOfFile`, `StdoutErr` and `StdinErr` errors into `Exit` errors which include a different exit code plus a message that will print to stderr to explain what happened. ### [The \_ type](#underscore) {#underscore} -In a larger program, we might want to split `main` into different pieces for logic and handling errors: +Note from our last example how we used `_` to create _partial_ type annotations. For example ```roc -main : Task {} [Exit I32 Str] -main = - run - |> Task.mapErr handleErr - -run : Task {} _ -run = - Stdout.line! "Type in something and press Enter:" - input = Stdin.line! - Stdout.line! "Your input was: $(input)" - -handleErr : _ -> [Exit I32 Str] -handleErr = \err -> - when err is - StdoutErr _ -> Exit 1 "Error writing to stdout." - StdinErr _ -> Exit 2 "Error writing to stdin." +when err is + StdoutErr _ -> Exit 1i32 "Error writing to stdout." + StdinErr _ -> Exit 2i32 "Error writing to stdin." + EndOfFile -> Exit 3i32 "End of file reached." ``` -Here, we used `_` to create _partial_ type annotations. Wherever a `_` appears in a type annotation, it essentially means "I'm choosing not to annotate this part right here" and it lets Roc use type inference to fill in the blank behind the scenes. Roc will still compile them and check their types as normal (just like it did before we had any annotations at all); the `_` is about which parts of the type we're choosing to annotate and which parts we're leaving to inference. +Wherever a `_` appears in a type annotation, it essentially means "I'm choosing not to annotate this part right here" and it lets Roc use type inference to fill in the blank behind the scenes. Roc will still compile them and check their types as normal (just like it did before we had any annotations at all); the `_` is about which parts of the type we're choosing to annotate and which parts we're leaving to inference. This is a useful technique to use when we don't want to write out a bunch of error types that we're going to handle anyway, and would otherwise have to keep updating every time a new error appeared. If we want to know the full list of errors, we can see it in a number of ways: @@ -1796,154 +1824,101 @@ This is a useful technique to use when we don't want to write out a bunch of err - If we're using an editor that supports it, hovering over the `_` might display the inferred type that goes there. - We can put an obviously wrong type in there (e.g. replace the `{}` with `Str`, which is totally wrong) and look at the compiler error to see what it inferred as the correct type. -### [The `!` postfix "await" operator](#the-postfix-await-operator) {#the-postfix-await-operator} - -The `!` postfix operator is syntax sugar for the `Task.await` function, which has this type: - -```roc -Task.await : Task a err, (a -> Task b err) -> Task b err -``` - -Basically, this function creates a task which runs one task, and then runs a second task which can use the output of the first task. (If the first task fails, the second task never gets run.) - -More specifically, `Task.await` returns a `Task` which: - -1. Runs the given `Task a err` -2. If it fails with some error, returns a `Task` which fails with that same error. (So if the given `Task a err` fails, the `a -> Task b err` function never gets called.) -3. If it succeeds (meaning it produces a success value which has the type `a`), pass the value it succeeded with to the `a -> Task b err` function. Whatever `Task b err` that function returns will be the `Task b err` the entire `Task.await` call returns. - -The `!` postfix operator is syntax sugar for connecting tasks using `Task.await`. Let's revisit our earlier example here: - -```roc -Stdout.line! "Type in something and press Enter:" -input = Stdin.line! -Stdout.line! "Your input was: $(input)" -``` - -This desugars to the following: - -```roc -Task.await (Stdout.line "Type in something and press Enter:") \_ -> - Task.await Stdin.line \input -> - Stdout.line "Your input was: $(input)" -``` - -Each of the `!` operators desugars to a `Task.await` call, except for the last one (which desugars to nothing because there's no task after it to connect to; if we wanted to, we could have left out that `!` without changing what the program does, but it looks more consistent to have both `Stdout.line!` calls end in a `!`). - -If you prefer, you can always... - -- call `Task.await` directly instead of using `!` -- leave out the last/only `!` in a block of expressions - -...without changing what the program does, but it's a stylistic convention in the Roc ecosystem to use `!` wherever possible. - ### [Ignoring informationless return values](#ignoring-informationless-return-values) {#ignoring-informationless-return-values} The compiler warns us about any unused variable, but as we saw before, if we don't intend to use a variable then we can name it `_` to clarify intent and silence the warning. Furthermore, if a variable can't possibly contain any useful information, then we can ignore it entirely. -Let's revisit our earlier example again: +Let's revisit an earlier example: ```roc -run = - Stdout.line! "Type in something and press Enter:" - input = Stdin.line! - Stdout.line! "Your input was: $(input)" +app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br" } + +import pf.Stdout +import pf.Stdin + +main! = \_args -> + try Stdout.line! "Type in something and press Enter:" + input = try Stdin.line! {} + try Stdout.line! "Your input was: $(input)" + Ok {} ``` It looks like this block goes "expression, assignment, expression", but expressions are only allowed on the last line of a block. What's happening here? -Since `Stdout.line : Str -> Task {} *`, the above is equivalent to: +Since `Stdout.line! : Str => Result {} [StdoutErr IOErr]`, in the above we are unwrapping the `Result` returned by calling these effects. -```roc -run = - alwaysAnEmptyRecord = Stdout.line! "Type in something and press Enter:" - input = Stdin.line! - Stdout.line! "Your input was: $(input)" -``` - -We don't intend to use `alwaysAnEmptyRecord`, so we can rename it to `_`. +An alternative option, is to just ignore the return value entirely: ```roc -run = +main! = \_args -> _ = Stdout.line! "Type in something and press Enter:" - input = Stdin.line! - Stdout.line! "Your input was: $(input)" + when Stdin.line! {} is + Ok input -> + _ = Stdout.line! "Your input was: $(input)" + Ok {} + Err _ -> + Ok {} ``` -Furthermore, since the type of `_` is `{}` (the only informationless type in Roc), the compiler allows us to delete the `_ =` entirely. `{}` is the only type for which Roc allows this ignoring/deletion. - ### [Tagging errors](#tagging-errors) {#tagging-errors} Although it's rare, it is possible that either of the `Stdout.line!` operations in our example could fail: ```roc -main = - Stdout.line! "Type something and press Enter." - input = Stdin.line! - Stdout.line! "You entered: $(input)" +main! = \_args -> + try Stdout.line! "Type something and press Enter." + input = try Stdin.line! {} + try Stdout.line! "You entered: $(input)" + Ok {} ``` -If that happens, we don't necessarily know which one was the cause of the failure. Our `handleErr` function runs when a `StdoutErr` happens, but once it receives a `StdoutErr` it has no way of knowing whether the first or second `Stdout.line` was the cause. +(In this particular example, it's very unlikely that this would come up at all, and even if it did, we might not care which one caused the problem. But you can imagine having multiple HTTP requests, or file writes, and wanting to know which of them was the one that failed.) -(In this particular example, it's not very likely that this would come up at all, and even if it did, we might not care which one caused the problem. But you can imagine having multiple HTTP requests, or file writes, and wanting to know which of them was the one that failed.) +If an error happended here, we wouldn't know which effectul function was the cause of the failure. -A quick way to do this is to "tag the error" using `Task.mapErr` to wrap the error in a [tag](#tags) like so: +One option is to "tag the error" using `Result.mapErr` to wrap the error in a [tag](#tags) like so: ```roc -main = - Stdout.line "Type something and press Enter." - |> Task.mapErr! PrintPrompt +app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br" } - input = Stdin.line! - - Stdout.line "You entered: $(input)" - |> Task.mapErr! PrintInput -``` +import pf.Stdout +import pf.Stdin -The `mapErr` function has this type: +main! = \_args -> -``` -Task.mapErr : Task ok a, (a -> b) -> Task ok b -``` + Stdout.line! "Type something and press Enter." + |> Result.mapErr UnableToPrintPrompt + |> try -Here we're passing in "tagging functions"—namely, `PrintPrompt` and `PrintInput`. (See [Using tags as functions](#using-tags-as-functions) for how this works.) + input = + Stdin.line! {} + |> Result.mapErr UnableToReadInput + |> try -Note that the `!` moves when we do this. It's no longer after `Stdout.line`, but rather after `Task.mapErr` instead: + Stdout.line! "You entered: $(input)" + |> Result.mapErr UnableToPrintInput + |> try -```roc - Stdout.line "Type something and press Enter." - |> Task.mapErr! PrintPrompt + Ok {} ``` -This code is doing three things: - -1. Call `Stdout.line "..."`, which returns a `Task` value -2. Transform that `Task` value into another `Task` value using `|> Task.mapErr` -3. Wait until that final `Task` value (returned by `mapErr`) successfully completes, using `!` - -It's easier to see why the `!` needs to move when considering a `Task` that produces a useful value, like `Stdin.line`. Compare these two: +The `mapErr` function has this type: -```roc - input = - Stdin.line - |> Task.mapErr! ReadStdin ``` - -This versions starts with the `Stdin.line` task, then passes it to `Task.mapErr` to tag its error with `ReadStdin`, and then finally uses `!` to await that final transformed task that `Task.mapErr` returned. - -```roc - input = - Stdin.line! - |> Task.mapErr ReadStdin +Result.mapErr : Result ok a, (a -> b) -> Result ok b ``` -This version starts with the `Stdin.line` task, awaits it with `!` to get its `Str` value if it succeded, and then tries to call `Task.mapErr` on that `Str` value. This will be a type mismatch! +Here we're passing in "tagging functions"—namely, `UnableToPrintPrompt` and `UnableToReadInput`. (See [Using tags as functions](#using-tags-as-functions) for how this works.) -In general, an easy rule to remember is "do the `!` last" - if you want to modify a `Task` before running it, use the `!` only after you've made all the modifications and ended up with the final `Task` you'd like to run. +This code is doing three things: + +1. Call `Stdout.line! "..."`, which returns a `Result` value +2. Transform that `Result` value into another `Result` value using `|> Result.mapErr` +3. Unwrap that final `Result` value (returned by `mapErr`) using `try` and return early if it's an error -See the [Task & Error Handling example](https://www.roc-lang.org/examples/Tasks/README.html) for a more detailed explanation of how to use tasks to help with error handling in a larger program. +See the [Task & Error Handling example](https://www.roc-lang.org/examples/ErrorHandling/README.html) for a more detailed explanation of how to use tasks to help with error handling in a larger program. ### [Displaying Roc values with `Inspect.toStr`](#inspect) {#inspect} @@ -1964,14 +1939,14 @@ This is rarely necessary, due to Roc's powerful syntax for expressions and patte For example: ```roc -stoplightStr : Str -stoplightStr = - stoplightColor = - if thisIsABadTime then +stoplight_str : Str +stoplight_str = + stoplight_color = + if this_ss_a_bad_time then return "Hey, listen, I just don't want to do this." else - previousStopLightColor - when stoplightColor is + previous_stop_light_color + when stoplight_color is Red -> "red" Green | Yellow if contrast > 75 -> "not red, but very high contrast" Green | Yellow if contrast > 50 -> "not red, but high contrast" @@ -1990,98 +1965,59 @@ You can continue reading through more-advanced topics below, or perhaps checkout Here are some features that are either coming soon or leaving soon. You may see these used in others' code, but for now you can choose to ignore them all! -### [Purity inference (PI)](#purity-inference) {#purity-inference} - -STATUS: Almost ready! For now, this may only work on supporting platforms and supported OSes. - -Soon, the `!` postfix "await" operator will evolve into a suffix character inside function names themselves, marking them as effectful. For example, a platform supporting PI may evolve from requiring `main` to requiring `main!`. - -Effectful functions are also identifiable from their type signature alone, using `a => b` instead of `a -> b`. - -Additionally, app developers will no longer learn or interact with `Task`s as a concept, as they shift to be a lower-level platform implementation detail. - -For more information, see [this recent talk](https://www.youtube.com/watch?v=42TUAKhzlRI) and [relevant Zulip threads](https://roc.zulipchat.com/#narrow/search/purity.20inference). - -### [The `try` keyword](#the-try-keyword) {#the-try-keyword} - -STATUS: Almost ready! For now, this may only work within PI contexts. - -Note: Right now, this can lead to confusing and unhelpful error messages. - -Roc has a `try` keyword, which is convenient syntax sugar for `Result.try`. For example, consider the following `getLetter` function: - -```roc -getLetter : Str -> Result Str [OutOfBounds, InvalidNumStr] -getLetter = \indexStr -> - index = try Str.toU64 indexStr - List.get ["a", "b", "c", "d"] index -``` - -Here's what this does: - -- If the `Str.toU64` function returns an `Ok` value, then `try` will return what's inside the `Ok`. For example: - - If we call `getLetter "2"`, then `Str.toU64` returns `Ok 2`, and the `try` unwraps to the integer 2, so `index` is set to 2 (not `Ok 2`). Then the `List.get` function is called and returns `Ok "c"`. -- If the `Str.toU64` function returns an `Err` value, then the `try` keyword immediately interrupts the `getLetter` function and makes it return this error. - - For example, if we call `getLetter "abc"`, then the call to `Str.toU64` returns `Err InvalidNumStr`, and the `try` keyword ensures that the `getLetter` function returns this error immediately, without executing the rest of the function. - -Thanks to the `try` keyword, your code can focus on the "happy path" (where nothing fails) and simply bubble up to the caller any error that might occur. Your error handling code can be neatly separated, and you can rest assured that you won't forget to handle any errors, since the compiler will let you know. See this [code example](https://github.com/roc-lang/examples/blob/main/examples/Results/main.roc) for more details on error handling. - ### [The `?` postfix "try" operator](#the-postfix-try-operator) {#the-postfix-try-operator} -STATUS: Deprecated! For now, this may only work within non-PI contexts. - -Note: Right now, this can lead to confusing and unhelpful error messages. +STATUS: Work in progress - being modified to support parens and commas calling convention. At this time it may lead to confusing and unhelpful error messages. Roc also has a `?` postfix operator, which behaves similar to the `try` keyword. An example: ```roc -getLetter : Str -> Result Str [OutOfBounds, InvalidNumStr] -getLetter = \indexStr -> - index = Str.toU64? indexStr +get_letter : Str -> Result Str [OutOfBounds, InvalidNumStr] +get_letter = \index_str -> + index = Str.toU64? index_str List.get ["a", "b", "c", "d"] index ``` -If you're working with [purity inference](#purity-inference) you'll want to use `try` and if your code is using `Task` you'll want to use `?.` ## [Advanced Concepts](#advanced-concepts) {#advanced-concepts} Here are some concepts you likely won't need as a beginner, but may want to know about eventually. This is listed as an appendix rather than the main tutorial, to emphasize that it's totally fine to stop reading here and go build things! ### [Open Records and Closed Records](#open-records-and-closed-records) {#open-records-and-closed-records} -Let's say I write a function which takes a record with a `firstName` and `lastName` field, and puts them together with a space in between: +Let's say I write a function which takes a record with a `first_name` and `last_name` field, and puts them together with a space in between: ```roc -fullName = \user -> - "$(user.firstName) $(user.lastName)" +full_name = \user -> + "$(user.first_name) $(user.last_name)" ``` -I can pass this function a record that has more fields than just `firstName` and `lastName`, as long as it has _at least_ both of those fields (and both of them are strings). So any of these calls would work: +I can pass this function a record that has more fields than just `first_name` and `last_name`, as long as it has _at least_ both of those fields (and both of them are strings). So any of these calls would work: -- `fullName { firstName: "Sam", lastName: "Sample" }` -- `fullName { firstName: "Sam", lastName: "Sample", email: "blah@example.com" }` -- `fullName { age: 5, firstName: "Sam", things: 3, lastName: "Sample", role: Admin }` +- `full_name { first_name: "Sam", last_name: "Sample" }` +- `full_name { first_name: "Sam", last_name: "Sample", email: "blah@example.com" }` +- `full_name { age: 5, first_name: "Sam", things: 3, last_name: "Sample", role: Admin }` This `user` argument is an _open record_ - that is, a description of a minimum set of fields on a record, and their types. When a function takes an open record as an argument, it's okay if you pass it a record with more fields than just the ones specified. In contrast, a _closed record_ is one that requires an exact set of fields (and their types), with no additional fields accepted. -If we add a type annotation to this `fullName` function, we can choose to have it accept either an open record or a closed record: +If we add a type annotation to this `full_name` function, we can choose to have it accept either an open record or a closed record: ```roc # Closed record -fullName : { firstName : Str, lastName : Str } -> Str -fullName = \user -> - "$(user.firstName) $(user.lastName)" +full_name : { first_name : Str, last_name : Str } -> Str +full_name = \user -> + "$(user.first_name) $(user.last_name)" ``` ```roc # Open record (because of the `*`) -fullName : { firstName : Str, lastName : Str }* -> Str -fullName = \user -> - "$(user.firstName) $(user.lastName)" +full_name : { first_name : Str, last_name : Str }* -> Str +full_name = \user -> + "$(user.first_name) $(user.last_name)" ``` -The `*` in the type `{ firstName : Str, lastName : Str }*` is what makes it an open record type. This `*` is the _wildcard type_ we saw earlier with empty lists. (An empty list has the type `List *`, in contrast to something like `List Str` which is a list of strings.) +The `*` in the type `{ first_name : Str, last_name : Str }*` is what makes it an open record type. This `*` is the _wildcard type_ we saw earlier with empty lists. (An empty list has the type `List *`, in contrast to something like `List Str` which is a list of strings.) This is because record types can optionally end in a type variable. Just like how we can have `List *` or `List a -> List a`, we can also have `{ first : Str, last : Str }*` or `{ first : Str, last : Str }a -> { first : Str, last : Str }a`. The differences are that in `List a`, the type variable is required and appears with a space after `List`; in a record, the type variable is optional, and appears (with no space) immediately after `}`. @@ -2092,8 +2028,8 @@ If the type variable in a record type is a `*` (such as in `{ first : Str, last The type variable can also be a named type variable, like so: ```roc -addHttps : { url : Str }a -> { url : Str }a -addHttps = \record -> +add_https : { url : Str }a -> { url : Str }a +add_https = \record -> { record & url: "https://$(record.url)" } ``` @@ -2126,8 +2062,8 @@ If you like, you can always annotate your functions as accepting open records. H ```roc User : { email : Str, - firstName : Str, - lastName : Str, + first_name : Str, + last_name : Str, } ``` @@ -2136,30 +2072,30 @@ This defines `User` to be a closed record, which in practice is the most common If you want to have a function take a `User`, you might write its type like so: ```roc -isValid : User -> Bool +is_valid : User -> Bool ``` If you want to have a function return a `User`, you might write its type like so: ```roc -userFromEmail : Str -> User +user_from_email : Str -> User ``` A function which takes a user and returns a user might look like this: ```roc -capitalizeNames : User -> User +capitalize_names : User -> User ``` -This is a perfectly reasonable way to write all of these functions. However, I might decide that I really want the `isValid` function to take an open record; a record with _at least_ the fields of this `User` record, but possibly others as well. +This is a perfectly reasonable way to write all of these functions. However, I might decide that I really want the `is_valid` function to take an open record; a record with _at least_ the fields of this `User` record, but possibly others as well. Since open records have a type variable (like `*` in `{ email : Str }*` or `a` in `{ email : Str }a -> { email : Str }a`), in order to do this I'd need to add a type variable to the `User` type alias: ```roc User a : { email : Str - firstName : Str - lastName : Str + first_name : Str + last_name : Str }a ``` @@ -2168,13 +2104,13 @@ Notice that the `a` type variable appears not only in `User a` but also in `}a` Using `User a` type alias, I can still write the same three functions, but now their types need to look different. This is what the first one would look like: ```roc -isValid : User * -> Bool +is_valid : User * -> Bool ``` Here, the `User *` type alias substitutes `*` for the type variable `a` in the type alias, which takes it from `{ email : Str, ... }a` to `{ email : Str, ... }*`. Now I can pass it any record that has at least the fields in `User`, and possibly others as well, which was my goal. ```roc -userFromEmail : Str -> User {} +user_from_email : Str -> User {} ``` Here, the `User {}` type alias substitutes `{}` for the type variable `a` in the type alias, which takes it from `{ email : Str, ... }a` to `{ email : Str, ... }{}`. As noted earlier, this is another way to specify a closed record: putting a `{}` after it, in the same place that you'd find a `*` in an open record. @@ -2186,20 +2122,20 @@ This function still returns the same record as it always did, it just needs to b The third function might need to use a named type variable: ```roc -capitalizeNames : User a -> User a +capitalize_names : User a -> User a ``` -If this function does a record update on the given user, and returns that - for example, if its definition were `capitalizeNames = \user -> { user & email: "blah" }` - then it needs to use the same named type variable for both the argument and return value. +If this function does a record update on the given user, and returns that - for example, if its definition were `capitalize_names = \user -> { user & email: "blah" }` - then it needs to use the same named type variable for both the argument and return value. However, if returns a new `User` that it created from scratch, then its type could instead be: ```roc -capitalizeNames : User * -> User {} +capitalize_names : User * -> User {} ``` This says that it takes a record with at least the fields specified in the `User` type alias, and possibly others...and then returns a record with exactly the fields specified in the `User` type alias, and no others. -These three examples illustrate why it's relatively uncommon to use open records for type aliases: it makes a lot of types need to incorporate a type variable that otherwise they could omit, all so that `isValid` can be given something that has not only the fields `User` has, but some others as well. (In the case of a `User` record in particular, it may be that the extra fields were included due to a mistake rather than on purpose, and accepting an open record could prevent the compiler from raising an error that would have revealed the mistake.) +These three examples illustrate why it's relatively uncommon to use open records for type aliases: it makes a lot of types need to incorporate a type variable that otherwise they could omit, all so that `is_valid` can be given something that has not only the fields `User` has, but some others as well. (In the case of a `User` record in particular, it may be that the extra fields were included due to a mistake rather than on purpose, and accepting an open record could prevent the compiler from raising an error that would have revealed the mistake.) That said, this is a useful technique to know about if you want to (for example) make a record type that accumulates more and more fields as it progresses through a series of operations. @@ -2352,58 +2288,58 @@ Record builders are a syntax sugar for sequencing actions and collecting the int For example, let's say we want a record builder to match URLs as follows: ```roc -combineMatchers : UrlMatcher a, UrlMatcher b, (a, b -> c) -> UrlMatcher c -combineMatchers = \matcherA, matcherB, combiner -> ... +combine_matchers : UrlMatcher a, UrlMatcher b, (a, b -> c) -> UrlMatcher c +combine_matchers = \matcherA, matcherB, combiner -> ... -userTabMatcher : UrlMatcher { users: {}, userId: U64, tab: Str } -userTabMatcher = - { combineMatchers <- - users: exactSegment "users", - userId: u64Segment, - tab: anySegment, +user_tab_matcher : UrlMatcher { users: {}, userId: U64, tab: Str } +user_tab_matcher = + { combine_matchers <- + users: exact_segment "users", + userId: u64_segment, + tab: any_segment, } expect - userTabMatcher + user_tab_matcher |> matchOnUrl "/users/123/account" == Ok { users: {}, userId: 123, tab: "account" } ``` -The `userTabMatcher` record builder desugars to the following: +The `user_tab_matcher` record builder desugars to the following: ```roc -userTabMatcher = - combineMatchers - (exactSegment "users") +user_tab_matcher = + combine_matchers + (exact_segment "users") ( - combineMatchers - u64Segment - anySegment + combine_matchers + u64_segment + any_segment \userId, tab -> (userId, tab) ) \users, (userId, tab) -> { users, userId, tab } ``` -You can see that the `combineMatchers` builder function is simply applied in sequence, pairing up all fields until a record is created. +You can see that the `combine_matchers` builder function is simply applied in sequence, pairing up all fields until a record is created. You'll notice that the `users` field above holds an empty record, and isn't a useful part of the result. If you want to ignore such a field in the record builder, prefix its name with an underscore as you would do to ignore a variable: ```roc -userTabMatcher : UrlMatcher { userId: U64 } -userTabMatcher = - { combineMatchers <- - _: exactSegment "users", - userId: u64Segment, - _tab: anySegment, +user_tab_matcher : UrlMatcher { userId: U64 } +user_tab_matcher = + { combine_matchers <- + _: exact_segment "users", + userId: u64_segment, + _tab: any_segment, } expect - userTabMatcher + user_tab_matcher |> matchOnUrl "/users/123/account" == Ok { userId: 123 } ``` -If you want to see other examples of using record builders, look at the [Record Builder Example](https://www.roc-lang.org/examples/RecordBuilder/README.html) for a moderately-sized example or the [Arg.Builder](https://github.com/roc-lang/basic-cli/blob/main/platform/Arg/Builder.roc) module in our `basic-cli` platform for a complex example. +If you want to see other examples of using record builders, look at the [Record Builder Example](https://www.roc-lang.org/examples/RecordBuilder/README.html). ### [Reserved Keywords](#reserved-keywords) {#reserved-keywords} From b4b44202a94fd15c8b8c6222e88b5cb30526dcd8 Mon Sep 17 00:00:00 2001 From: Luke Boswell Date: Wed, 8 Jan 2025 18:06:08 +1100 Subject: [PATCH 03/11] remove more references to Task --- www/content/tutorial.md | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/www/content/tutorial.md b/www/content/tutorial.md index b8f4a88d932..6ba1b868b5f 100644 --- a/www/content/tutorial.md +++ b/www/content/tutorial.md @@ -10,7 +10,6 @@
  • Crashing
  • Testing
  • Modules
  • -
  • Tasks
  • Advanced Concepts
  • @@ -200,13 +199,12 @@ You should see this: There are 5 animals. -`main.roc` now has four definitions (_defs_ for short) `birds`, `iguanas`, `total`, and `main`. +`main.roc` now has four definitions (_defs_ for short) `birds`, `iguanas`, `total`, and `main!`. A definition names an expression. - The first two defs assign the names `birds` and `iguanas` to the expressions `3` and `2`. - The next def assigns the name `total` to the expression `Num.toStr (birds + iguanas)`. -- The last def assigns the name `main` to an expression which returns a `Task`. We'll [discuss tasks later](#tasks). Once we have a def, we can use its name in other expressions. For example, the `total` expression refers to `birds` and `iguanas`, and `Stdout.line! "There are $(total) animals."` refers to `total`. @@ -929,7 +927,12 @@ Note that the state doesn't have to be a record; it can be anything you want. Fo A helpful way to remember the argument order for `List.walk` is that that its arguments follow the same pattern as what we've seen with `List.map`, `List.any`, `List.keepIf`, and `List.dropIf`: the first argument is a list, and the last argument is a function. The difference here is that `List.walk` has one more argument than those other functions; the only place it could go while preserving that pattern is in the middle! -> **Note:** Other languages give this operation different names, such as `fold`, `reduce`, `accumulate`, `aggregate`, `compress`, and `inject`. Some languages also have operations like `forEach` or `for...in` syntax, which walk across every element and perform potentially side-effecting operations on them; `List.walk` can be used to replace these too, if you include a `Task` in the state. We'll talk about tasks, and how to use them with `List.walk`, later on. +> **Note:** Other languages give this operation different names, such as `fold`, `reduce`, `accumulate`, `aggregate`, `compress`, and `inject`. Consider using one of the following if you would like to call an effectful function on a list of + +```roc +List.forEach! : List a, (a => {}) => {} +List.forEachTry! : List a, (a => Result {} err) => Result {} err +``` ### [Pattern Matching on Lists](#pattern-matching-on-lists) {#pattern-matching-on-lists} @@ -1501,7 +1504,6 @@ There are several modules that are built into the Roc compiler, which are import 10. [Hash](https://www.roc-lang.org/builtins/Hash) 11. [Box](https://www.roc-lang.org/builtins/Box) 12. [Inspect](https://www.roc-lang.org/builtins/Inspect) -13. [Task](https://www.roc-lang.org/builtins/Task) (deprecated and will be remove soon) You may have noticed that we already used the first five. For example, when we wrote `Str.concat` and `Num.isEven`, we were referencing functions stored in the `Str` and `Num` modules. @@ -1669,7 +1671,7 @@ Stdout.line! : Str => Result {} [StdoutErr IOErr] An effectulful function is capable of interacting with state outside your Roc program, such as the terminal's standard output, or a file. -When we call `main!`, the host will provide the arguments passed from the cli `_args` (here we're just ignoring these), and then run. Here, we've set `main!` to be a task that writes `"Hi there, from inside a Roc app. 🎉"` to `stdout` when it gets run, so that's what our program does! +When we call `main!`, the host will provide the arguments passed from the cli `_args` (here we're just ignoring these), and then run. Here, we've set `main!` to be an effectful function that writes `"Hi there, from inside a Roc app. 🎉"` to `stdout` when it gets run, so that's what our program does! The `Stdout.line!` function here returns a `Result` when called. If it succeeds it returns the unit value `{}`, or if it fails it returns the tag `StdoutErr` with an `IOErr` payload. @@ -1704,7 +1706,7 @@ it should print back out what you entered. Sometimes, effects can fail. For example, reading from a file might fail if the file is not found. Even reading from stdin and writing to stdout can fail! -For example, the `Stdin.line!` task can fail if stdin is closed before it receives a line. You can try this out +For example, the `Stdin.line!` function can fail if stdin is closed before it receives a line. You can try this out by running the program and pressing Ctrl+Z on Windows, or Ctrl+D on macOS or Linux. (Press that key rather than typing in text and pressing Enter.) You'll see a default error message, which the `basic-cli` platform provides in case an error occurs that we didn't handle. @@ -1725,9 +1727,9 @@ main = #... Let's break down what this type is saying: -- Both the `!` suffix in the name, and the `=>` in the type, tell us this is an effectful function. Its two type parameters are just like the ones we saw in `Result` earlier: the first type tells us what this task will produce if it succeeds, and the other one tells us what it will produce if it fails. -- `{}` tells us that this function always produces an empty record when it succeeds. (That is, it doesn't produce anything useful. Empty records don't have any information in them!) This is because the last task in `main!` comes from `Stdout.line!`, which doesn't produce anything. (In contrast, the `Stdin` task's first type parameter is a `Str`, because it produces a `Str` if it succeeds.) -- `[EndOfFile, StdinErr Stdin.IOErr, StdoutErr Stdout.IOErr]` tells us the different ways this task can fail. The `StdoutErr` and `StdinErr` tags are there because we used `Stdout.line!` and `Stdin.line!`. +- Both the `!` suffix in the name, and the `=>` in the type, tell us this is an effectful function. Its two type parameters are just like the ones we saw in `Result` earlier: the first type tells us what this function will produce if it succeeds, and the other one tells us what it will produce if it fails. +- `{}` tells us that this function always produces an empty record when it succeeds. (That is, it doesn't produce anything useful. Empty records don't have any information in them!) This is because the last expression in `main!` comes from `Stdout.line!`, which doesn't produce anything. (In contrast, the `Stdin` function's first type parameter is a `Str`, because it produces a `Str` if it succeeds.) +- `[EndOfFile, StdinErr Stdin.IOErr, StdoutErr Stdout.IOErr]` tells us the different ways this function can fail. The `StdoutErr` and `StdinErr` tags are there because we used `Stdout.line!` and `Stdin.line!`. To understand the error a little more, let's try temporarily commenting out our current `main!` and replacing it with this one: @@ -1750,10 +1752,10 @@ This program won't print anything at all, but it will exit with a status code of In summary: -- If the `main!` task ends in a `Ok {}`, then it means the final task succeeded and the program will exit with status code 0. -- If the `main!` task ends in a `Err (Exit 42 "…")`, then it means it failed, and the only information we got about the failure was that the program should exit with code 42 instead of 0, and that it should print a particular string to stderr to inform the user about what happened. +- If the `main!` function ends in a `Ok {}`, then it means the final expression succeeded and the program will exit with status code 0. +- If the `main!` function ends in a `Err (Exit 42 "…")`, then it means it failed, and the only information we got about the failure was that the program should exit with code 42 instead of 0, and that it should print a particular string to stderr to inform the user about what happened. -### [Handling task failures](#handling-failures) {#handling-failures} +### [Handling failure](#handling-failures) {#handling-failures} If `main!` ends up failing with any other errors besides `Exit` (such as `StdoutErr` or `StdinErr`), then the `basic-cli` platform's automatic error handling will handle them by printing out words taken from the source code (such as "StdoutErr" and "StdinErr"), which could lead to a bad experience for people using this program! @@ -1773,9 +1775,9 @@ main! = \_args -> Adding this type annotation will give us a type mismatch - which is exactly what we want in this case! -The type mismatch is telling us that we're claiming the `main!` task will only ever fail with an `Exit` tag, but this implementation can _also_ fail with `EndOfFile`, `StdoutErr` and `StdinErr` tags we saw earlier. +The type mismatch is telling us that we're claiming the `main!` function will only ever fail with an `Exit` tag, but this implementation can _also_ fail with `EndOfFile`, `StdoutErr` and `StdinErr` tags we saw earlier. -In other words, adding this annotation effectively opted us out of `basic-cli`'s default error handling. Now any potential task failures (now and in the future) will have to be handled somehow; if we forget to handle any, we'll get a type mismatch like this! For that reason, `basic-cli` applications that are intended to be high-quality (so, not things like quick scripts) will generally benefit from applying this type annotation to `main!`. +In other words, adding this annotation effectively opted us out of `basic-cli`'s default error handling. Now any potential failures (now and in the future) will have to be handled somehow; if we forget to handle any, we'll get a type mismatch like this! For that reason, `basic-cli` applications that are intended to be high-quality (so, not things like quick scripts) will generally benefit from applying this type annotation to `main!`. Here's one way we can handle those errors: @@ -1788,14 +1790,14 @@ import pf.Arg exposing [Arg] main! : List Arg => Result {} [Exit I32 Str] main! = \_args -> - Result.mapErr (my_task! {}) \err -> + Result.mapErr (my_function! {}) \err -> when err is StdoutErr _ -> Exit 1i32 "Error writing to stdout." StdinErr _ -> Exit 2i32 "Error writing to stdin." EndOfFile -> Exit 3i32 "End of file reached." -my_task! : {} => Result {} [EndOfFile, StdinErr _, StdoutErr _] -my_task! = \{} -> +my_function! : {} => Result {} [EndOfFile, StdinErr _, StdoutErr _] +my_function! = \{} -> try Stdout.line! "Type in something and press Enter:" input = try Stdin.line! {} try Stdout.line! "Your input was: $(input)" @@ -1918,7 +1920,7 @@ This code is doing three things: 2. Transform that `Result` value into another `Result` value using `|> Result.mapErr` 3. Unwrap that final `Result` value (returned by `mapErr`) using `try` and return early if it's an error -See the [Task & Error Handling example](https://www.roc-lang.org/examples/ErrorHandling/README.html) for a more detailed explanation of how to use tasks to help with error handling in a larger program. +See the [Error Handling example](https://www.roc-lang.org/examples/ErrorHandling/README.html) for a more detailed explanation of error handling in a larger program. ### [Displaying Roc values with `Inspect.toStr`](#inspect) {#inspect} From 841287cedbbe65c5a5c1ff525ab134c3e259b993 Mon Sep 17 00:00:00 2001 From: Luke Boswell Date: Thu, 9 Jan 2025 09:25:06 +1100 Subject: [PATCH 04/11] Update www/content/plans.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Kukieła <53443372+kukimik@users.noreply.github.com> Signed-off-by: Luke Boswell --- www/content/plans.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/content/plans.md b/www/content/plans.md index 25f5432097e..0e18c4f1a3d 100644 --- a/www/content/plans.md +++ b/www/content/plans.md @@ -26,7 +26,7 @@ This is a significant upgrade to the language and will take some time for the ec ### [Static Dispatch](#static-dispatch) {#static-dispatch} -There are significant changes outlined in the [static dispatch design proposal](https://docs.google.com/document/d/1OUd0f4PQjH8jb6i1vEJ5DOnfpVBJbGTjnCakpXAYeT8/edit?usp=sharing). These are being discussed and worked on, but likely to affect other features like abilities, record default fields, and potentiall module params. +There are significant changes outlined in the [static dispatch design proposal](https://docs.google.com/document/d/1OUd0f4PQjH8jb6i1vEJ5DOnfpVBJbGTjnCakpXAYeT8/edit?usp=sharing). These are being discussed and worked on, but likely to affect other features like abilities, record default fields, and potentially module params. ### [Syntax changes](#syntax-changes) {#syntax-changes} From 7cd9fae3349d7ffed07bd29f8350e91ec11b8c0e Mon Sep 17 00:00:00 2001 From: Luke Boswell Date: Thu, 9 Jan 2025 09:27:05 +1100 Subject: [PATCH 05/11] remove Task note from tutotial --- www/content/tutorial.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/www/content/tutorial.md b/www/content/tutorial.md index 6ba1b868b5f..2a0d8f90bb5 100644 --- a/www/content/tutorial.md +++ b/www/content/tutorial.md @@ -1639,9 +1639,7 @@ Notice the subtle difference in these functions' types: The arrow in the type tells you whether a function is pure or might perform effects. Pure functions use `->` and effectful functions use `=>`. Effectful functions can call either pure functions or other effectful functions, but pure functions can only call other pure functions. (The fact that pure functions can only call pure functions is part of the definition of pure functions; Roc is just reflecting that fact in the type system.) -The `!` is a suffix character inside function names themselves, marking them as effectful. This is just a naming convention, though it is enforced by the compiler with a warning. - -Note: this was a previous change to the language, and `!` used to be an operator, so you may still find code that as a `Task` in it or an effectful function like `main` without a `!` suffix. This code should be upgraded to use `Result` and the naming convention for purity inference, e.g. `main!`. For more information about this change, see [this recent talk](https://www.youtube.com/watch?v=42TUAKhzlRI). +The suffix character `!` at the end of a function name marks it as effectful. This is a naming convention, and is enforced by the compiler with a warning. Let's look at some examples. In the `basic-cli` platform, here are four operations we can do: From c197d77971ac4f745d9a8309cea634f199aed1d1 Mon Sep 17 00:00:00 2001 From: Luke Boswell Date: Thu, 9 Jan 2025 09:27:41 +1100 Subject: [PATCH 06/11] Update www/content/tutorial.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Kukieła <53443372+kukimik@users.noreply.github.com> Signed-off-by: Luke Boswell --- www/content/tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/content/tutorial.md b/www/content/tutorial.md index 6ba1b868b5f..be0322583f7 100644 --- a/www/content/tutorial.md +++ b/www/content/tutorial.md @@ -868,7 +868,7 @@ Here's what this does: - If the `Str.toU64` function returns an `Err` value, then the `try` keyword immediately interrupts the `get_letter` function and makes it return this error. - For example, if we call `get_letter "abc"`, then the call to `Str.toU64` returns `Err InvalidNumStr`, and the `try` keyword ensures that the `get_letter` function returns this error immediately, without executing the rest of the function. -Thanks to the `try` keyword, your code can focus on the "happy path" (where nothing fails) and simply bubble up to the caller any error that might occur. Your error handling code can be neatly separated, and you can rest assured that you won't forget to handle any errors, since the compiler will let you know. See this [code example](https://github.com/roc-lang/examples/blob/main/examples/Results/main.roc) for more details on error handling. +Thanks to the `try` keyword, your code can focus on the "happy path" (where nothing fails) and simply bubble up to the caller any error that might occur. Your error handling code can be neatly separated, and you can rest assured that you won't forget to handle any errors, since the compiler will let you know. See this [code example](https://www.roc-lang.org/examples/Results/README.html) for more details on error handling. Now let's get back to lists! From ca1f9c274ba44d60d52895aa269b692183a968ef Mon Sep 17 00:00:00 2001 From: Luke Boswell Date: Thu, 9 Jan 2025 09:27:52 +1100 Subject: [PATCH 07/11] Update www/content/tutorial.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Kukieła <53443372+kukimik@users.noreply.github.com> Signed-off-by: Luke Boswell --- www/content/tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/content/tutorial.md b/www/content/tutorial.md index be0322583f7..8918c1709d4 100644 --- a/www/content/tutorial.md +++ b/www/content/tutorial.md @@ -1701,7 +1701,7 @@ If you run this program, it will print "Type in something and press Enter:" and That's because it's waiting for you to type something in and press Enter! Once you do, it should print back out what you entered. -### [Effectul failure](#failure) {#failure} +### [Effectful failure](#failure) {#failure} Sometimes, effects can fail. For example, reading from a file might fail if the file is not found. Even reading from stdin and writing to stdout can fail! From 53f5fc3e7929e89feea27dd6d94b3a4c0478e285 Mon Sep 17 00:00:00 2001 From: Luke Boswell Date: Thu, 9 Jan 2025 09:28:20 +1100 Subject: [PATCH 08/11] review suggestion --- www/content/tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/content/tutorial.md b/www/content/tutorial.md index 2a0d8f90bb5..3ba30789670 100644 --- a/www/content/tutorial.md +++ b/www/content/tutorial.md @@ -1753,7 +1753,7 @@ In summary: - If the `main!` function ends in a `Ok {}`, then it means the final expression succeeded and the program will exit with status code 0. - If the `main!` function ends in a `Err (Exit 42 "…")`, then it means it failed, and the only information we got about the failure was that the program should exit with code 42 instead of 0, and that it should print a particular string to stderr to inform the user about what happened. -### [Handling failure](#handling-failures) {#handling-failures} +### [Handling failure](#handling-failure) {#handling-failure} If `main!` ends up failing with any other errors besides `Exit` (such as `StdoutErr` or `StdinErr`), then the `basic-cli` platform's automatic error handling will handle them by printing out words taken from the source code (such as "StdoutErr" and "StdinErr"), which could lead to a bad experience for people using this program! From d903312dff92007c4fbdd29e86dfc9d09b20aa1f Mon Sep 17 00:00:00 2001 From: Luke Boswell Date: Thu, 9 Jan 2025 09:28:36 +1100 Subject: [PATCH 09/11] Update www/content/tutorial.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Kukieła <53443372+kukimik@users.noreply.github.com> Signed-off-by: Luke Boswell --- www/content/tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/content/tutorial.md b/www/content/tutorial.md index 8918c1709d4..0befce8f653 100644 --- a/www/content/tutorial.md +++ b/www/content/tutorial.md @@ -1878,7 +1878,7 @@ main! = \_args -> (In this particular example, it's very unlikely that this would come up at all, and even if it did, we might not care which one caused the problem. But you can imagine having multiple HTTP requests, or file writes, and wanting to know which of them was the one that failed.) -If an error happended here, we wouldn't know which effectul function was the cause of the failure. +If an error happended here, we wouldn't know which effectful function was the cause of the failure. One option is to "tag the error" using `Result.mapErr` to wrap the error in a [tag](#tags) like so: From 0af27690ef0b6f5ffb3e8d774dedcc2b6ab31c47 Mon Sep 17 00:00:00 2001 From: Luke Boswell Date: Thu, 9 Jan 2025 09:28:44 +1100 Subject: [PATCH 10/11] Update www/content/tutorial.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Kukieła <53443372+kukimik@users.noreply.github.com> Signed-off-by: Luke Boswell --- www/content/tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/content/tutorial.md b/www/content/tutorial.md index 0befce8f653..703f223a041 100644 --- a/www/content/tutorial.md +++ b/www/content/tutorial.md @@ -1912,7 +1912,7 @@ The `mapErr` function has this type: Result.mapErr : Result ok a, (a -> b) -> Result ok b ``` -Here we're passing in "tagging functions"—namely, `UnableToPrintPrompt` and `UnableToReadInput`. (See [Using tags as functions](#using-tags-as-functions) for how this works.) +Here we're passing in "tagging functions" — namely, `UnableToPrintPrompt` and `UnableToReadInput`. (See [Using tags as functions](#using-tags-as-functions) for how this works.) This code is doing three things: From 285cf813283d2f09e5a4e5066f7aa7a7f870a352 Mon Sep 17 00:00:00 2001 From: Luke Boswell Date: Thu, 9 Jan 2025 09:28:55 +1100 Subject: [PATCH 11/11] Update www/content/tutorial.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Kukieła <53443372+kukimik@users.noreply.github.com> Signed-off-by: Luke Boswell --- www/content/tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/content/tutorial.md b/www/content/tutorial.md index 703f223a041..4c6a08f6bd4 100644 --- a/www/content/tutorial.md +++ b/www/content/tutorial.md @@ -1669,7 +1669,7 @@ This code prints "Hi there, from inside a Roc app. 🎉" to the [standard output Stdout.line! : Str => Result {} [StdoutErr IOErr] ``` -An effectulful function is capable of interacting with state outside your Roc program, such as the terminal's standard output, or a file. +An effectful function is capable of interacting with state outside your Roc program, such as the terminal's standard output, or a file. When we call `main!`, the host will provide the arguments passed from the cli `_args` (here we're just ignoring these), and then run. Here, we've set `main!` to be an effectful function that writes `"Hi there, from inside a Roc app. 🎉"` to `stdout` when it gets run, so that's what our program does!