polar

Lists, Options and Results

Four types from the standard library show up in almost every program: List for sequences, Option for values that might be missing, Result for operations that might fail, and Map for lookups.

They're all ordinary variant types written in Polar, so everything from the last page applies to them. Each lives in its own module, which you import in uses.

Lists

uses
  Std.List

functions
  main() {
    let numbers = List.range(1, 6)
    let evens = List.filter(numbers, function(n) { n % 2 == 0 })
    let squares = List.map(numbers, function(n) { n * n })

    Log.info("sum of squares: #{List.fold(squares, 0, function(a, b) { a + b })}")
  }

Write a list with brackets: [1, 2, 3]. All items have the same type, so that's a List<Int>. .. spreads one list into another, so [0, ..numbers] puts a 0 in front.

Note: List syntax needs uses Std.List. Without it, Polar stops with "list syntax needs the List type" and tells you what to add.

The functions you'll use most:

Function Does
List.map(xs, f) A new list with f applied to each item
List.filter(xs, keep) Only the items where keep is true
List.fold(xs, start, f) Combines all items into one value, like reduce
List.find(xs, test) The first matching item, as an Option
List.any, List.all Whether some or every item passes a test
List.length, List.is_empty Size checks
List.join(xs, ", ") Joins a list of strings

The standard library reference lists the rest.

Option: a value that might be missing

Polar has no null. A value that might not be there has the type Option<a>, which is defined like this:

Option<a> = None | Some(a)

For example, List.head returns Option<a>, because an empty list has no first item. To use the value, you have to handle both cases, so you can't forget the empty one:

match List.head(numbers) {
  Some(first) -> "starts with #{first}",
  None -> "empty",
}

Often you just want a fallback. Option.with_default does that:

Option.with_default(List.nth(numbers, 10), -1)

Option.map changes the value inside a Some and leaves None alone. Option.and_then chains steps that might each come back empty.

Result: an operation that might fail

Result<e, a> is either Ok(a) with a value, or Err(e) with an error. Unlike Option, a failure carries information about what went wrong:

types
  ParseError = NotANumber(String)

functions
  parse_age(text: String) -> Result<ParseError, Int> {
    match Int.parse(text) {
      Some(n) -> Ok(n),
      None -> Err(NotANumber(text)),
    }
  }

  describe(result: Result<ParseError, Int>) -> String {
    match result {
      Ok(age) -> "age #{age}",
      Err(NotANumber(text)) -> "`#{text}` is not a number",
    }
  }

Result.map transforms the Ok value, and Result.map_err transforms the error.

Tip: Use Result when the caller is expected to deal with the failure, like bad user input. For errors that should travel up the call stack, see Handling Errors.

Map: looking things up by key

Map<k, v> stores values by key. Each change gives back a new map:

let ages = Map.empty() |> Map.insert("ann", 31) |> Map.insert("bob", 27)

Map.get(ages, "bob")   // Some(27)
Map.has(ages, "cat")   // false

Map.get returns an Option, because the key might not be there.

All together

module Lists

uses
  Std.List
  Std.Map
  Std.Option
  Std.Result

types
  ParseError = NotANumber(String)

functions
  parse_age(text: String) -> Result<ParseError, Int> {
    match Int.parse(text) {
      Some(n) -> Ok(n),
      None -> Err(NotANumber(text)),
    }
  }

  describe(result: Result<ParseError, Int>) -> String {
    match result {
      Ok(age) -> "age #{age}",
      Err(NotANumber(text)) -> "`#{text}` is not a number",
    }
  }

  main() {
    let numbers = List.range(1, 6)
    let evens = List.filter(numbers, function(n) { n % 2 == 0 })
    let squares = List.map(numbers, function(n) { n * n })

    Log.info("numbers: #{List.join(List.map(numbers, Int.to_string), ", ")}")
    Log.info("evens: #{List.join(List.map(evens, Int.to_string), ", ")}")
    Log.info("sum of squares: #{List.fold(squares, 0, function(a, b) { a + b })}")
    Log.info("any over 20? #{List.any(squares, function(n) { n > 20 })}")
    Log.info("first: #{Option.with_default(List.head(numbers), 0)}")
    Log.info("tenth: #{Option.with_default(List.nth(numbers, 10), -1)}")

    let ages = Map.empty() |> Map.insert("ann", 31) |> Map.insert("bob", 27)

    Log.info("bob is #{Option.with_default(Map.get(ages, "bob"), 0)}")
    Log.info("has cat? #{Map.has(ages, "cat")}")
    Log.info(describe(parse_age("42")))
    Log.info(describe(parse_age("forty")))
    Log.info(describe(Result.map(parse_age("20"), function(n) { n + 1 })))
  }

exports
  main
$ polar run lists.px
numbers: 1, 2, 3, 4, 5
evens: 2, 4
sum of squares: 55
any over 20? true
first: 1
tenth: -1
bob is 27
has cat? false
age 42
`forty` is not a number
age 21

Notice List.range(1, 6) stops before 6. Next, let's see how types share behavior with traits.