Tutorial

Learn Par in 10 short programs.

This tutorial assumes prior programming experience. Ideally, you've been writing code for a few years, and you're comfortable with at least one statically-typed language.

1. Basics

Add two numbers and print the sum.

  • Line 1 — All Par programs begin with a module declaration. A module is simply a namespace that can contain functions, constants, types, etc.

  • Line 3 — We define an add() function that accepts two parameters, x and y, and returns their sum. All standard arithmetic and comparison operators (+, -, *, /, ==, >, <, etc.) work in Par. You can find all possible expressions in the reference manual.

  • Lines 5–7 — A program begins by executing the main() function, which accepts no arguments. In our main(), we call add(3, 4) and use let to assign the result to the variable sum. We then call the builtin function print(), which will print sum to stdout.

  • Note: You can never re-assign or modify a variable; you can only introduce a new binding via let. This immutability makes it easy to reason about your code, but takes time to get used to.

File basics.par:

1
2
3
4
5
6
7
module Basics

add(x, y) = x + y

main() =
  let sum = add(3, 4)
  print(sum)
# compile and run
$ par basics.par
7

2. Price checker

Check if we can buy a phone with $300.

  • Lines 3, 11, 14 — We provide type signatures for each global constant/function definition. Although not required, this is good practice; it ensures the type checker affirms our understanding of the code, and prevents unexpected types from propagating. You can find all possible types in the reference manual.

  • Lines 3–9 — We define a global hash map, prices, that maps item names to item prices. Its type is Map<String, Float>, meaning there are String keys and Float values. Note that because all data structures are immutable, it's implied that prices is a constant and cannot be changed.

  • Lines 11–12 — The type signature for can_buy?() indicates that it accepts two parameters, a String and a Float, and then returns a Bool. can_buy?() uses the builtin function get() to find the value associated with the key item in the prices map. It then returns whether the resulting price is within the provided budget.

  • Line 14main : () -> () means that main() accepts no arguments, and returns a value of type unit, or (). The unit type () is used to indicate that there's no useful return value; that is, this function effectively returns nothing.

File price-checker.par:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module PriceChecker

prices : Map<String, Float>
prices = {
  "Candy" => 0.25
  "Phone" => 599.99
  "Coffee" => 4.50
  "Juice" => 9
}

can_buy? : (String, Float) -> Bool
can_buy?(item, budget) = get(prices, item) <= budget

main : () -> ()
main() = print(can_buy?("Phone", 300))
# compile and run
$ par price-checker.par
false

3. Digitizer

Convert a number into an array of its digits.

  • Line 3 — The type [Int] means a list of integers. to_digits() accepts two parameters: Int and [Int], and returns [Int]. The [Int] passed into to_digits(), accum, is the accumulated list of digits so far. to_digits() is defined recursively; in the first call, accum will be the empty list, [], and in recursive calls, it'll contain digits.

  • Lines 6, 9 — The expression [h | t] returns a new list with the first element h followed by all elements in the existing list t. h is called the head of the new list, and t is the tail. This is an O(1) operation and is the most efficient way to put elements into a list. Appending an element at the end of a list, by contrast, copies all prior elements, making it O(n).

  • Line 8% is the remainder operator. num % 10 finds the remainder when num is divided by 10, giving us the last digit.

  • Line 9trunc() converts a Float to an Int by truncating anything after the decimal point. So trunc(num / 10) returns num with the last digit removed. e.g. trunc(137 / 10) == trunc(13.7) == 13.

  • Line 12to_digits(137, []) calls to_digits(13, [7]), which calls to_digits(1, [3, 7]), which returns [1, 3, 7].

File digitizer.par:

1
2
3
4
5
6
7
8
9
10
11
12
module Digitizer

to_digits : (Int, [Int]) -> [Int]
to_digits(num, accum) =
  if num < 10 then
    [num | accum]
  else
    let digit = num % 10
    to_digits(trunc(num / 10), [digit | accum])

main : () -> ()
main() = print(to_digits(137, []))
# compile and run
$ par digitizer.par
[1, 3, 7]

4. Undigitizer

Convert an array of digits back into a number.

  • Line 3to_num() accepts a list of integers, [Int] as the sole parameter and returns an Int.

  • Lines 4–8 — The match keyword lets us pattern match the list of digits. It goes through each case pat => expr on lines 5-7 in order. If the pattern pat matches digits, then expr will be executed and returned; otherwise, match will try the next case. You can find all possible patterns in the reference manual.

  • Line 5 — If digits is precisely an empty list, [], then we return 0 as the corresponding number.

  • Line 6 — The pattern [d] checks if digits has exactly one element, which we'll call d. d is accessible to the right-hand side expression, and we simply return d as the corresponding number.

  • Line 7 — The pattern [d1, d2 | t] checks if digits has at least two elements d1 and d2, followed by any remaining elements in the tail list t. t may be empty. In this case, we combine d1 and d2 into one number, prepend it to t, and recursively call ourselves.

  • Line 11to_num([1, 3, 7]) calls to_num([13, 7]), which calls to_num([137]), which returns 137.

File undigitizer.par:

1
2
3
4
5
6
7
8
9
10
11
module Undigitizer

to_num : [Int] -> Int
to_num(digits) = match digits {
  [] => 0
  [d] => d
  [d1, d2 | t] => to_num([d1 * 10 + d2 | t])
}

main : () -> ()
main() = print(to_num([1, 3, 7]))
# compile and run
$ par undigitizer.par
137

5. Word analyzer

Find the number of consonants and vowels in a given word.

  • Line 3–4vowels_set is a hash set containing characters, or a Set<Char>. A literal set is created with the #[elem1, elem2, ...] syntax. A character is denoted by single quotes.

  • Line 6analyze() accepts a String word argument and returns a tuple containing two integers, (Int, Int).

  • Line 8String is a standard library module that contains a function to_chars(), which converts a String to a list of characters, [Char].

  • Line 10, 14 — The builtin filter() accepts two arguments: a function that, given an element, returns a Bool indicating whether to keep that element, and a collection of elements, which could be a List, Map, or Set. It returns a new collection containing only the elements for which the function returns true.

  • Line 12 — The syntax |arg1, arg2, ...| expr creates an anonymous function that accepts arguments arg1, arg2, etc. and executes the given expr expression. In this case, our function accepts a character ch and returns true if ch isn't a vowel. You can refer to the documentation for contains?().

  • Line 16 — The syntax contains?(vowels_set, _) creates a new function that accepts one argument, which we'll call ch, and returns contains?(vowels_set, ch). This is a shorthand for |ch| contains?(vowels_set, ch).

  • Line 19(a, b) is the syntax for creating a tuple. A tuple may contain two or more elements.

  • Line 23 — The left-hand side of a let binding can be any valid pattern, as per the reference manual. In this case, we're matching a tuple that has two elements.

  • Line 25, 27++ is used to concatenate two strings, lists, sets, or maps. to_pretty() converts any type into a pretty String.

File word-analyzer.par:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
module WordAnalyzer

vowels_set : Set<Char>
vowels_set = #['a', 'e', 'i', 'o', 'u']

analyze : String -> (Int, Int)
analyze(word) =
  let chars = String.to_chars(word)

  let consonants = filter(
    chars,
    |ch| !contains?(vowels_set, ch)
  )
  let vowels = filter(
    chars,
    contains?(vowels_set, _)
  )

  (length(consonants), length(vowels))

main : () -> ()
main() =
  let (num_consonants, num_vowels) =
    analyze("arithmetic")
  print("Num consonants: " ++
    to_pretty(num_consonants))
  print("Num vowels: " ++ to_pretty(num_vowels))
# compile and run
$ par word-analyzer.par
Num consonants: 6
Num vowels: 4

6. People

Identify people who are tall and print their names.

  • Lines 3–7A and B are type variables, which represent any two types. For any types A and B, map_list() requires two arguments:

    • A list containing elements of type A.
    • A function that accepts some value of type A and returns some value of type B.

    map_list() returns a new list containg elements of type B. It does so by applying the given function to each element of the given list, creating a new list of mapped elements.

    map_list() works for any types A and B, and is said to be polymorphic over A and B. For example, in map_list([1.5, 2.7, 3], |x| x < 2), A is Float and B is Bool, with the result [true, false, false].

  • Lines 9–14people is a list of anonymous records. A record is a collection of fields, each with a name and associated value. In this case, each record has two fields: name, whose value is a String, and height, whose value is a Float.

  • Lines 19, 20 — The pipe operator |> takes the left-hand side and makes it the first argument of the right-hand side. a |> f(b) is equivalent to f(a, b). p.height and p.name access the height and name fields, respectively, of the records created on lines 11-13.

  • Note: The builtin function map() does exactly what map_list does, but it also works for sets and maps. We're implementing map_list here for the sake of learning.

File people.par:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module People

map_list : ([A], A -> B) -> [B]
map_list(l, f) = match l {
  [h | t] => [f(h) | map_list(t, f)]
  [] => []
}

people : [{ name : String, height : Float }]
people = [
  { name = "Jill", height = 70.6 }
  { name = "Bob", height = 68 }
  { name = "Laura", height = 73.25 }
]

main : () -> ()
main() =
  let tall_names = people
    |> filter(|p| p.height > 70)
    |> map_list(|p| p.name)
  print(tall_names)
# compile and run
$ par people.par
["Jill", "Laura"]

7. Link finder

Model an HTML document and find links within in.

  • Lines 3–12 — An enum is a sum type, meaning it models data that comes in different forms. Each form is called a variant, and it represents one case that the data might fall in. For instance, if we're modeling an arithmetic expression, we could have an addition operation, an exponentiation, or a negation; these are three different variants. Every variant has a name and, optionally, one or more arguments that help describe it.

  • Lines 4–5 — The variants Href and Src are two different HTML attributes—two cases— that we're modeling in our Attribute sum type. Neither take arguments.

  • Lines 9–11Html represents an HTML Document, which is composed of tags and text. A Tag() is described by three arguments: its name, like "div" or "body", its attributes, a hash map from Attribute keys to String values, and its contents, Html (notice the recursive nature of this type). Text() is described by just a String. Finally, since we can have multiple tags and text side-by-side, Siblings() is described by an array of Html values.

  • Lines 14–25 — Using the Attribute and Html enums we just declared, we model the following HTML document:

    <a href="http://par-lang.com">Par</a>
    <div>
      <a href="/learn">Learn Par</a>
      <img src="par.png" />
    </div>
    

    Tag(), Text(), and Siblings() are functions that accept their corresponding arguments, whereas Href/Src are constants.

  • Line 27find_links() recursively searches through Html for "a" tags with Href attributes, which represent hyperlinks. It returns a list of those hyperlinks, so for the document above, we'd get ["http://par-lang.com", "/learn"].

  • Line 29find_links() uses pattern matching to handle different cases of Html. The pattern Text(_) matches the Text variant. The _ is a way of ignoring what the String argument is; we don't care because we know text isn't a link, so we return an empty list.

  • Lines 31–32 — The pattern Tag("a", attrs, content) matches <a> tags. We assign the names attrs and content to the attributes and contents of the tag. We then refer to these in the right-hand side of the =>, where we get the value of the Href attribute, and continue finding links in the content.

  • Lines 35–36List.flat_map() will call find_links() for each sibling, and then concatenate the results into one list.

File link-finder.par:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
module LinkFinder

enum Attribute {
  Href
  Src
}

enum Html {
  Tag(String, Map<Attribute, String>, Html)
  Text(String)
  Siblings([Html])
}

doc : Html
doc = Siblings([
  Tag(
    "a",
    { Href => "http://par-lang.com" },
    Text("Par")
  )
  Tag("div", {}, Siblings([
    Tag("a", { Href => "/learn" }, Text("Learn Par"))
    Tag("img", { Src => "par.png" }, Text(""))
  ]))
])

find_links : Html -> [String]
find_links(html) = match html {
  Text(_) => []

  Tag("a", attrs, content) =>
    [get(attrs, Href) | find_links(content)]
  Tag(_, _, content) => find_links(content)

  Siblings(siblings) =>
    List.flat_map(siblings, find_links)
}

main : () -> ()
main() = print(find_links(doc))
# compile and run
$ par link-finder.par
["http://par-lang.com", "/learn"]

8. Validator

Validates a user's post and fixes any issues.

  • Lines 3–6 — A struct is very similar to an anonymous record, but it's a named type. It contains a set of fields, each with a name and an associated value. We define a struct by specifying each field name and the associated value's type.

  • Lines 8–9 — An exception is an error condition that we can raise in our program to halt execution. We can also catch exceptions and respond to them. Here, we define two exceptions. Note that each exception is defined in the same way as an enum variant: it has a name and, optionally, arguments to describe it. Exceptions have type Exception.

  • Lines 13, 15 — We access struct fields in the same way as anonymous record fields, using the . operator: p.title and p.likes.

  • Lines 14, 16 — We raise an exception using the raise keyword. Exceptions with arguments act as functions, whereas those without act as constants, just like enum variants.

  • Lines 20–25 — We declare an array of Post objects. Each Post looks almost exactly like an anonymous record, except that we specify the type Post prior to the {.

  • Lines 30–33 — The try ... catch { ... } syntax lets us catch exceptions and respond to them. After try follows the expression we want to monitor for exceptions. The catch block is very similar to a match block, where we pattern match different exceptions.

  • Line 31 — The syntax { title = "untitled" | p } creates a new Post that has all the same fields as p, but with the title set to "untitled". So if the exception matches EmptyTitle, we set a title on the post to fix it. This syntax also works with anonymous records.

  • Line 32 — If the exception is NegativeLikes() with a like count l, we update the post's like count to -l to make it positive.

File validator.par:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
module Validator

struct Post {
  likes : Int
  title : String
}

exception EmptyTitle
exception NegativeLikes(Int)

check_post : Post -> Post
check_post(p) =
  if p.title == "" then
    raise EmptyTitle
  else if p.likes < 0 then
    raise NegativeLikes(p.likes)
  else
    p

posts : [Post]
posts = [
  Post { likes = 0, title = "My first post!" }
  Post { likes = -37, title = "Boring" }
  Post { likes = 15, title = "" }
]

main : () -> ()
main() =
  let fixed_posts = map(posts, |p|
    try check_post(p) catch {
      EmptyTitle => { title = "untitled" | p }
      NegativeLikes(l) => { likes = -l | p }
    }
  )
  print(fixed_posts)
# compile and run
$ par validator.par
[
  Post { likes = 0, title = "My first post!" }
  Post { likes = 37, title = "Boring" }
  Post { likes = 15, title = "untitled" }
]

9. Conversion

Convert different types to an integer.

  • Lines 3–5 — An interface declares one or more functions that work across many types. to_integer() accepts a type T and returns Int. T is a placeholder for any type that implements the ToInteger interface. In other words, for any type T that implements the ToInteger interface, you may call to_integer() to get an Int.

  • Lines 7–9 — We state that the type Bool implements the ToInteger interface. To satisfy the interface, we provide an implementation for to_integer() that accepts a Bool and returns an Int. We can now call to_integer() on any Bool value and get back an Int.

  • Lines 11–13 — We repeat the above, except for Float, calling trunc() to truncate a Float into an Int. We can now call to_integer() on any Bool or Float value to get an Int!

  • Lines 15–16zero?() accepts a value of type A ~ ToInteger, meaning any type A such that A satisfies the ToInteger interface, and returns a Bool. It calls to_integer() on the argument, and checks if the result is 0.

  • Lines 20–21 — We can call to_integer() and zero?() with either a Float or Bool thanks to our interface! Both functions are said to be polymorphic over the Float and Bool types.

File conversion.par:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module Conversion

interface ToInteger {
  to_integer : T -> Int
}

impl ToInteger for Bool {
  to_integer(b) = if b then 1 else 0
}

impl ToInteger for Float {
  to_integer(f) = trunc(f)
}

zero? : A ~ ToInteger -> Bool
zero?(x) = to_integer(x) == 0

main : () -> ()
main() =
  let float_results = (to_integer(3.7), zero?(3.7))
  let bool_results = (to_integer(false), zero?(false))
  print(float_results)
  print(bool_results)
# compile and run
$ par conversion.par
(3, false)
(0, true)

10. Importer

Split code into multiple files and import between them.

  • Line 9 — The export keyword lets us expose functions/constants like raw_data to other modules, allowing them to import and use it. Enums and structs are automatically exported.

File csv.par:

1
2
3
4
5
6
7
8
9
10
11
12
13
module Csv

enum Row {
  Company(String, Int)
  Person(String)
}

raw_data : String
export raw_data =
  "Google,1998\n" ++
  "Larry Page\n" ++
  "Apple,1976\n" ++
  "Steve Jobs\n"
  • Line 3 — We can import a module by specifying the path to it, either relative to the current file, or absolute. Since csv.par is in the same directory as parser.par, our relative path is simply "./csv" (the extension is implied). This allows us to access all exported fields of the Csv module by their full qualified name, like Csv.raw_data.

  • Line 4 — Standard library modules like String, List, Set, etc. are imported by default, but we can also directly import any names so we can access them unqualified. In this case, the (*) indicates we're directly importing all names from String. Hence, functions like lines() and split() are accessible without the String. prefix.

  • Lines 6, 8, 10, 11 — We access exported types, variants, and constants in the Csv module by their full qualified name.

  • Lines 8, 9 — We access lines() and split() from the String module without the String. prefix.

  • Line 12 — The compiler will force us to handle all cases when we perform a pattern match. A list may contain more than 2 elements or fewer than 1, so we must add a catch-all case here. We call fail() to generate a runtime exception.

File reader.par:

1
2
3
4
5
6
7
8
9
10
11
12
13
module Reader

import "./csv"
import String (*)

entities : [Csv.Row]
export entities =
  let csv_lines = lines(Csv.raw_data)
  map(csv_lines, |line| match split(line, ",") {
    [name, year] => Csv.Company(name, to_int(year))
    [name] => Csv.Person(name)
    _ => fail("Unexpected line: " ++ line)
  })
  • Line 3 - (entities) means we directly import entities from parser.par. This way, we can access it unqualified as entities and not Parser.entities.

  • Line 4 - (Row, variants Row) means we directly import the Row type and all variants of the Row type, which are Person() and Company(). We can access all three of these unqualified. If we'd like to add more direct imports, we can comma separate them in these parens.

File formatter.par:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module Formatter

import "./reader" (entities)
import "./csv" (Row, variants Row)

format_row : Row -> String
format_row(r) = match r {
  Company(name, year) =>
    "Company: " ++ name ++ ", founded in " ++
      to_str(year)
  Person(name) => "Person: " ++ name
}

main : () -> ()
main() = map(entities, format_row) |> print
# compile and run
$ par formatter.par
[
  "Company: Google, founded in 1998"
  "Person: Larry Page"
  "Company: Apple, founded in 1976"
  "Person: Steve Jobs"
]