Modules
Passerine's module system allows large codebases to be broken out into indiviual reusable components. A module is a scopes turned into a struct, and isn't necessarily tied to the file system.
Modules are defined using the mod
keyword, which must be followed by a block { ... }
. Here's a simple module that defines some math utilities:
circle = mod {
PI = 3.14159265358979
area = r -> r * r * PI
circum = r -> r * PI * 2
}
pizza_radius = 12
slices = 8
slice_area = (circle::area pizza_radius) / slices
mod
takes all top-level declarations in a block - in this case, PI
, area
, and circum
- and turns them into a struct with those fields. In essence, the above is equivalent to this struct:
circle = {
PI: 3.14159265358979
area: r -> r * r * PI
circum: r -> r * PI * 2
}
mod
is nice because it's an easy way to have multiple returns. In essesence, the mod
keyword allows for first-class scoping, by turning scopes into structs:
index = numbers pos
-> floor (len numbers * pos)
quartiles = numbers -> mod {
sorted = (sort numbers)
med = sorted::(index (1/2) sorted)
q1 = sorted::(index (1/4) sorted)
q3 = sorted::(index (3/4) sorted)
}
Because we used the mod
keyword in the above example, instead of returning a single value from the function, we return a struct containing all values in the fuction:
-- calculate statistics
numbers = [1, 2, 3, 4, 5]
stats = quartiles numbers
-- use `q1` and `q3` to calculate the interquartile range of `numbers`
iqr = stats::q3 - stats::q1
print "the IQR of { numbers } is { iqr } "
This is really useful for writing functions that return multiple values.
Aside from allowing us to group sets of related values into a single namespace, modules can be defined in different files, then be imported. Here's a module defined in a different file:
-- list_util.pn
reduce = f start list -> match list {
[] -> start,
[head, ..tail] -> f (reduce f tail, head)
}
sum = reduce { (a, b) -> a + b } 0
reverse = reduce { (a, b) -> [b.., a]} []
This file defines a number of useful list utilities, defined in a traditional recursive style. If we want to use this module in main.pn
, we import it using the use
keyword:
-- main.pn
use list_util
numbers = [1, 1, 2, 3, 5]
print (list_util::sum numbers)
Note that the use
keyword is essentially the same thing as wrapping the contents of the imported file with the mod
keyword:
-- use list_util
list_util = mod { <list_util.pn> }
Once imported, list_util
is just a struct. Because of this, features of the module system naturally arise from Passerine's existing semantics for manipulating structs. To import a subset of a module, we can do something like this:
reverse = { use list_util; list_util::reduce }
Likewise, we can import a module within a block scope to rename it:
list_stuff = { use list_util; list_util }
There are a number of nice properties that arise from this module system, we've just scratched the surface. As modules are just structs, the full power of passerine and its macro system are at your disposal for building extensible systems that compose well.