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.