Skip to content

0.0.0-alpha2-*

Latest
Compare
Choose a tag to compare
@Anton-4 Anton-4 released this 29 Jan 18:45
· 76 commits to main since this release
689c58f

Note: we're starting with a new release approach. Each time a breaking change happens, the alpha number will be incremented in 0.0.0-alpha. The * indicates a rolling release, the tar.gz archives in assets will be updated if the changes are non-breaking. So upgrading to the latest tar.gz with the same alpha should not create any problems.
We recommend these alpha releases for most users instead of the nightly releases.

❗ if your project used a latest nightly url like:

https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-linux_x86_64-latest.tar.gz

You should probably switch it to:

https://github.com/roc-lang/roc/releases/download/0.0.0-alpha2-rolling/roc-linux_x86_64-0-alpha2-rolling.tar.gz

Migration guide

TL;DR

  • Run roc format --migrate to upgrade most syntax changes
  • Replace usages of Task with effectful! functions
  • Use Result.map_ok instead of Result.map
  • Str.BadUtf8 has changed from BadUtf8 Utf8ByteProblem U64 to BadUtf8 { problem: Utf8Problem, index: U64}

What changed?

Since the last update, Roc has gone through lots of syntax changes! We want
to make Roc a friendly language for new programmers, functional programming wizards,
and everyone in between. To that end, we've made Roc look more like mainstream
languages while still keeping things clean and concise.

Let's look at a sample application to see what changed:

app [main] { pf: platform "<basic-cli>" }

import pf.File
import pf.Stdout

main =
    readmePath = "./README.md"
    licensePath = "./LICENSE"

    when checkContentsOfFiles readmePath licensePath |> Task.result! is
        Ok {} -> Task.ok {}
        Err err ->
            msg =
                when err is
                    FileReadErr _ -> "Error reading README"
                    FailedToReadLicense -> "Failed to read LICENSE file"

            Task.err (Exit 1 "unable to read file: $(msg)")

checkContentsOfFiles : Str, Str -> Task {} _
checkContentsOfFiles = \readmePath, licensePath ->
    readmeContents = File.readUtf8! readmePath
    licenseContents = File.readUtf8 licensePath
        |> Task.mapErr! \_ -> FailedToReadLicense

    readmeLines = Str.split readmeContents "\n"
    firstReadmeLine = List.first lines |> Result.withDefault "<empty README>"

    licenseLength = Str.split license "\n" |> List.len

    Stdout.line! "First line of $(readmePath): $(firstReadmeLine)"
    Stdout.line! "Line count for $(licensePath): $(Num.toStr licenseLength)"

This example reads the README and the LICENSE of a repository and prints some info on
it, namely the first line of the README and the number of lines in the LICENSE. Let's
look at its modern counterpart!

app [main] { pf: platform "<basic-cli>" }

import pf.File
import pf.Stdout

main! = |_args|
    readme_path = "./README.md"
    license_path = "./LICENSE"

    check_contents_of_files(readme_path, license_path)
    |> Result.map_err!(|err|
        msg =
            when err is
                FileReadErr(_) -> "Error reading README"
                FailedToReadLicense -> "Failed to read LICENSE file"

        Err(Exit(1, "unable to read file: ${msg}")))

check_contents_of_files! : Str, Str => Result {} _
check_contents_of_files! = |readme_path, license_path|
    readme_contents = File.read_utf8!(readme_path)?
    license_contents = File.read_utf8(license_path) ? |_| FailedToReadLicense

    readme_lines = Str.split(readme_contents, "\n")
    first_readme_line = List.first(lines) ?? "<empty README>"
    license_length = Str.split(license, "\n") |> List.len

    Stdout.line!("First line of ${readme_path}: ${first_readme_line}")?
    Stdout.line!("Line count for ${license_path}: ${Num.to_str(license_length)}")

Lots has changed, even if it works the same under the hood! Let's list the changes:

  • Functions get called with ( and ) now instead of whitespace (also tags like Ok(val) instead of Ok val)
  • Function arguments are now surrounded with | pipes, just like Rust or Ruby
  • All variable and function names are in snake_case instead of camelCase, which makes individual words in variables easier to read
  • Functions that might have side effects (like writing to a file) must have a ! at the end of their name, which means ! is no longer an operator but just a suffix
  • String interpolation now uses ${ and } instead of $( and )
  • We now use and and or for boolean operators instead of the old && and || operators to make them distinct from the new |args| function syntax

In addition to the stylistic changes, there are some additional syntax features for error handling:

  • ? right after an expression that returns a Result will either early return the Err or unwrap the Ok value, just like Rust
  • If you put a space between the expression and ?, it will use the function after the ? to map the Err value first before the early return happens
    • This is really useful for giving additional useful for giving context
  • ?? unwraps a Result with a default value just like Result.with_default(), but with the benefit of only calculating the provided default expression if necessary

Most of the work to change existing Roc code to the new format can be done for you by the Roc compiler; just run

$ roc format --migrate

and the above stylistic changes will get converted for you. The main thing you'll have to do is replace any usage of Task with an effectful function, AKA one with a ! at the end of its name. Task has now been completely removed from Roc, which we think is a great improvement to the simplicity and readability of Roc. Also, Result.map has been renamed to Result.map_ok for better compatability with future plans for Roc.

This has been a whirlwind of changes! Don't worry, our hope has been to do the majority of the changes needed for Roc to look how it will in the long-term, so you won't have to do much migration after this feature push.

Some other APIs we've changed/added:

  • Renamed Result.map with Result.map_ok
  • Introduce some new Str operations:
    • Str.with_ascii_lowercased: make ASCII characters in a string lowercase and leave everything else the same
    • Str.with_ascii_uppercased: make ASCII characters in a string uppercase and leave everything else the same
    • Str.caseless_ascii_equals: compare the contents of two strings, ignoring case for ASCII characters
    • Str.from_utf8: convert a List U16 to a string, failing on invalid codepoints
      • We already had this, but it changed error types:
      • Old: from_utf8 : List U8 -> Result Str [BadUtf8 Utf8ByteProblem U64]
      • New: from_utf8 : List U8 -> Result Str [BadUtf8 { problem : Utf8Problem, index : U64 }]
    • Str.from_utf8_lossy: convert a List U8 to a string, replacing invalid codepoints with the "�" character
    • Str.from_utf16: convert a List U16 to a string, failing on invalid codepoints
    • Str.from_utf16_lossy: convert a List U16 to a string, replacing invalid codepoints with the "�" character
    • Str.from_utf32: convert a List U16 to a string, failing on invalid codepoints
    • Str.from_utf32_lossy: convert a List U32 to a string, replacing invalid codepoints with the "�" character
  • Added some effectful List walking functions:
    • for_each!: Run an effectful operation on every element in a list
    • for_each_try!: Run an effectful operation on every element in a list, returning early on an Err
    • walk!: Effectfully walk through the elements of a list, building up state as you go
    • walk_try!: Effectfully walk through the elements of a list, building up state as you go, returning early on an Err

That's it for now! Let us know if you run into any issues in the Zulip chat, we're here to help!