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,xandy, 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 useletto assign the result to the variablesum. We then call the builtin functionprint(), which will printsumtostdout.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 areStringkeys andFloatvalues. 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, aStringand aFloat, and then returns aBool.can_buy?()uses the builtin functionget()to find the value associated with the keyitemin thepricesmap. 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:Intand[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,accumwill 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 elementhfollowed by all elements in the existing listt.his called the head of the new list, andtis 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 % 10finds the remainder whennumis divided by10, giving us the last digit.Line 9 —
trunc()converts aFloatto anIntby truncating anything after the decimal point. Sotrunc(num / 10)returnsnumwith 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
matchkeyword lets us pattern match the list of digits. It goes through each casepat => expron lines 5-7 in order. If the patternpatmatchesdigits, thenexprwill be executed and returned; otherwise,matchwill try the next case. You can find all possible patterns in the reference manual.Line 5 — If
digitsis precisely an empty list,[], then we return 0 as the corresponding number.Line 6 — The pattern
[d]checks ifdigitshas exactly one element, which we'll calld.dis accessible to the right-hand side expression, and we simply returndas the corresponding number.Line 7 — The pattern
[d1, d2 | t]checks ifdigitshas at least two elementsd1andd2, followed by any remaining elements in the tail listt.tmay be empty. In this case, we combined1andd2into 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_setis 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 aStringword argument and returns a tuple containing two integers,(Int, Int).Line 8 —
Stringis a standard library module that contains a functionto_chars(), which converts aStringto a list of characters,[Char].Line 10, 14 — The builtin
filter()accepts two arguments: a function that, given an element, returns aBoolindicating 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, ...| exprcreates an anonymous function that accepts argumentsarg1,arg2, etc. and executes the givenexprexpression. In this case, our function accepts a characterchand returnstrueifchisn'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
letbinding 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 —
AandBare type variables, which represent any two types. For any typesAandB,map_list()requires two arguments:- A list containing elements of type
A. - A function that accepts some value of type
Aand 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 typesAandB, and is said to be polymorphic overAandB. For example, inmap_list([1.5, 2.7, 3], |x| x < 2),AisFloatandBisBool, with the result[true, false, false].- A list containing elements of type
Lines 9–14 —
peopleis 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.heightandp.nameaccess theheightandnamefields, respectively, of the records created on lines 11-13.Note: The builtin function
map()does exactly whatmap_listdoes, but it also works for sets and maps. We're implementingmap_listhere 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
HrefandSrcare two different HTML attributes—two cases— that we're modeling in ourAttributesum type. Neither take arguments.Lines 9–11 —
Htmlrepresents 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 fromAttributekeys toStringvalues, 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 ofHtmlvalues.Lines 14–25 — Using the
AttributeandHtmlenums 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/Srcare constants.Line 27 —
find_links()recursively searches throughHtmlfor"a"tags withHrefattributes, 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 theTextvariant. The_is a way of ignoring what theStringargument 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 namesattrsandcontentto 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 theHrefattribute, 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.titleandp.likes.Lines 14, 16 — We raise an exception using the
raisekeyword. Exceptions with arguments act as functions, whereas those without act as constants, just like enum variants.Lines 20–25 — We declare an array of
Postobjects. EachPostlooks almost exactly like an anonymous record, except that we specify the typePostprior to the{.Lines 30–33 — The
try ... catch { ... }syntax lets us catch exceptions and respond to them. Aftertryfollows the expression we want to monitor for exceptions. Thecatchblock is very similar to amatchblock, where we pattern match different exceptions.Line 31 — The syntax
{ title = "untitled" | p }creates a newPostthat has all the same fields asp, but with thetitleset 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-lto 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 typeTand returnsInt.Tis a placeholder for any type that implements theToIntegerinterface. In other words, for any typeTthat implements theToIntegerinterface, you may callto_integer()to get anInt.Lines 7–9 — We state that the type
Boolimplements theToIntegerinterface. To satisfy the interface, we provide an implementation forto_integer()that accepts aBooland returns anInt. We can now callto_integer()on anyBoolvalue and get back anInt.Lines 11–13 — We repeat the above, except for
Float, callingtrunc()to truncate aFloatinto anInt. We can now callto_integer()on anyBoolorFloatvalue to get anInt!Lines 15–16 —
zero?()accepts a value of typeA ~ ToInteger, meaning any typeAsuch thatAsatisfies theToIntegerinterface, 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 aFloatorBoolthanks to our interface! Both functions are said to be polymorphic over theFloatandBooltypes.
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
exportkeyword lets us expose functions/constants likeraw_datato 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.paris 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 theCsvmodule 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
Csvmodule by their full qualified name.Lines 8, 9 — We access
lines()andsplit()from theStringmodule 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 importentitiesfromparser.par. This way, we can access it unqualified asentitiesand notParser.entities.Line 4 -
(Row, variants Row)means we directly import theRowtype and all variants of theRowtype, 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" ]
