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
andy
, 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 ourmain()
, we calladd(3, 4)
and uselet
to assign the result to the variablesum
. We then call the builtin functionprint()
, which will printsum
tostdout
.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 isMap<String, Float>
, meaning there areString
keys andFloat
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, aString
and aFloat
, and then returns aBool
.can_buy?()
uses the builtin functionget()
to find the value associated with the keyitem
in theprices
map. It then returns whether the resulting price is within the providedbudget
.Line 14 —
main : () -> ()
means thatmain()
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 intoto_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 elementh
followed by all elements in the existing listt
.h
is called the head of the new list, andt
is the tail. This is anO(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 itO(n)
.Line 8 —
%
is the remainder operator.num % 10
finds the remainder whennum
is divided by10
, giving us the last digit.Line 9 —
trunc()
converts aFloat
to anInt
by truncating anything after the decimal point. Sotrunc(num / 10)
returnsnum
with the last digit removed. e.g.trunc(137 / 10) == trunc(13.7) == 13
.Line 12 —
to_digits(137, [])
callsto_digits(13, [7])
, which callsto_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 3 —
to_num()
accepts a list of integers,[Int]
as the sole parameter and returns anInt
.Lines 4–8 — The
match
keyword lets us pattern match the list of digits. It goes through each casepat => expr
on lines 5-7 in order. If the patternpat
matchesdigits
, thenexpr
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 ifdigits
has exactly one element, which we'll calld
.d
is accessible to the right-hand side expression, and we simply returnd
as the corresponding number.Line 7 — The pattern
[d1, d2 | t]
checks ifdigits
has at least two elementsd1
andd2
, followed by any remaining elements in the tail listt
.t
may be empty. In this case, we combined1
andd2
into one number, prepend it tot
, and recursively call ourselves.Line 11 —
to_num([1, 3, 7])
callsto_num([13, 7])
, which callsto_num([137])
, which returns137
.
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–4 —
vowels_set
is a hash set containing characters, or aSet<Char>
. A literal set is created with the#[elem1, elem2, ...]
syntax. A character is denoted by single quotes.Line 6 —
analyze()
accepts aString
word argument and returns a tuple containing two integers,(Int, Int)
.Line 8 —
String
is a standard library module that contains a functionto_chars()
, which converts aString
to a list of characters,[Char]
.Line 10, 14 — The builtin
filter()
accepts two arguments: a function that, given an element, returns aBool
indicating whether to keep that element, and a collection of elements, which could be aList
,Map
, orSet
. It returns a new collection containing only the elements for which the function returnstrue
.Line 12 — The syntax
|arg1, arg2, ...| expr
creates an anonymous function that accepts argumentsarg1
,arg2
, etc. and executes the givenexpr
expression. In this case, our function accepts a characterch
and returnstrue
ifch
isn't a vowel. You can refer to the documentation forcontains?()
.Line 16 — The syntax
contains?(vowels_set, _)
creates a new function that accepts one argument, which we'll callch
, and returnscontains?(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 prettyString
.
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–7 —
A
andB
are type variables, which represent any two types. For any typesA
andB
,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 typeB
.
map_list()
returns a new list containg elements of typeB
. 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 typesA
andB
, and is said to be polymorphic overA
andB
. For example, inmap_list([1.5, 2.7, 3], |x| x < 2)
,A
isFloat
andB
isBool
, with the result[true, false, false]
.- A list containing elements of type
Lines 9–14 —
people
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 aString
, andheight
, whose value is aFloat
.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 tof(a, b)
.p.height
andp.name
access theheight
andname
fields, respectively, of the records created on lines 11-13.Note: The builtin function
map()
does exactly whatmap_list
does, but it also works for sets and maps. We're implementingmap_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
andSrc
are two different HTML attributes—two cases— that we're modeling in ourAttribute
sum type. Neither take arguments.Lines 9–11 —
Html
represents an HTML Document, which is composed of tags and text. ATag()
is described by three arguments: its name, like"div"
or"body"
, its attributes, a hash map fromAttribute
keys toString
values, and its contents,Html
(notice the recursive nature of this type).Text()
is described by just aString
. Finally, since we can have multiple tags and text side-by-side,Siblings()
is described by an array ofHtml
values.Lines 14–25 — Using the
Attribute
andHtml
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()
, andSiblings()
are functions that accept their corresponding arguments, whereasHref
/Src
are constants.Line 27 —
find_links()
recursively searches throughHtml
for"a"
tags withHref
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 29 —
find_links()
uses pattern matching to handle different cases ofHtml
. The patternText(_)
matches theText
variant. The_
is a way of ignoring what theString
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 namesattrs
andcontent
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 theHref
attribute, and continue finding links in thecontent.
Lines 35–36 —
List.flat_map()
will callfind_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
andp.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. EachPost
looks almost exactly like an anonymous record, except that we specify the typePost
prior to the{
.Lines 30–33 — The
try ... catch { ... }
syntax lets us catch exceptions and respond to them. Aftertry
follows the expression we want to monitor for exceptions. Thecatch
block is very similar to amatch
block, where we pattern match different exceptions.Line 31 — The syntax
{ title = "untitled" | p }
creates a newPost
that has all the same fields asp
, but with thetitle
set to"untitled"
. So if the exception matchesEmptyTitle
, 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 countl
, 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 typeT
and returnsInt
.T
is a placeholder for any type that implements theToInteger
interface. In other words, for any typeT
that implements theToInteger
interface, you may callto_integer()
to get anInt
.Lines 7–9 — We state that the type
Bool
implements theToInteger
interface. To satisfy the interface, we provide an implementation forto_integer()
that accepts aBool
and returns anInt
. We can now callto_integer()
on anyBool
value and get back anInt
.Lines 11–13 — We repeat the above, except for
Float
, callingtrunc()
to truncate aFloat
into anInt
. We can now callto_integer()
on anyBool
orFloat
value to get anInt
!Lines 15–16 —
zero?()
accepts a value of typeA ~ ToInteger
, meaning any typeA
such thatA
satisfies theToInteger
interface, and returns aBool
. It callsto_integer()
on the argument, and checks if the result is0
.Lines 20–21 — We can call
to_integer()
andzero?()
with either aFloat
orBool
thanks to our interface! Both functions are said to be polymorphic over theFloat
andBool
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 likeraw_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 asparser.par
, our relative path is simply"./csv"
(the extension is implied). This allows us to access all exported fields of theCsv
module by their full qualified name, likeCsv.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 fromString
. Hence, functions likelines()
andsplit()
are accessible without theString.
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()
andsplit()
from theString
module without theString.
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 importentities
fromparser.par
. This way, we can access it unqualified asentities
and notParser.entities
.Line 4 -
(Row, variants Row)
means we directly import theRow
type and all variants of theRow
type, which arePerson()
andCompany()
. 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" ]