15. Modules and projects
As a program grows you want to group related definitions and give them a namespace, the way Python
uses modules and packages. Pyfun offers this at two scales. The small one is an in-file module, a
named block of let bindings. Inside the block members call each other by their bare names, and from
outside you reach them qualified as Module.member.
module Geom =
let square x = x * x
let area w h = w * h
let diagSq w h = square w + square h
print (Geom.area 4 5)
print (Geom.diagSq 3 4)
20
25
diagSq calls square directly because they share the module. From the top level you write
Geom.area, which reads like math.sqrt does in Python. This grouping is purely organizational, so
an in-file module runs anywhere Pyfun runs, including the playground.
The larger scale is one module per file. A sibling source file becomes a module under its capitalized
name, and you bring it into scope with import. The project in examples/modules splits work across
geometry.pyfun, store.pyfun, and a main.pyfun that imports both:
import Geometry
import Store
let floor = Geometry.area 4 5
let nine = Geometry.square 3
let widen = Geometry.area 10
let strip = widen 2
let hit = Store.lookup 1 |> Option.withDefault 0
let miss = Store.lookup 9 |> Option.withDefault 0
print floor
print nine
print strip
print hit
print miss
Running the whole project with pyfun run examples/modules/main.pyfun prints:
20
9
20
100
0
import Geometry refers to geometry.pyfun by its capitalized name, and the import graph must be
acyclic. A Some built in Store is the same Option type main inspects, because the runtime
classes are shared. Compiling with pyfun compile examples/modules/main.pyfun -o out writes a
readable Python file tree, one .py per module plus a shared _pyfun_rt.py:
_pyfun_rt.py
geometry.py
main.py
store.py
A cross-module call lowers to plain Python attribute access like geometry.area(4, 5). Multi-file
projects need the installed compiler on disk, since the playground runs a single source at a time.
For practice there, an in-file module gives you the same qualified-use experience.
Types cross too
Functions are the obvious thing to share, but the types themselves cross a module boundary as well,
which is what lets you split a program along its data rather than around it. Given a shapes.pyfun
that declares an ADT and a record:
type Shape =
| Circle float
| Rect float float
type Placed = { shape: Shape, x: float, y: float }
another module can build those values, match them, and name the types in its own declarations:
import Shapes
type Scene = { name: string, item: Shapes.Placed }
let scene =
Scene { name = "demo", item = Shapes.Placed { shape = Shapes.Circle 2.0, x = 0.0, y = 0.0 } }
let describe p =
match p.shape:
case Shapes.Circle r: f"circle of radius {r}"
case Shapes.Rect w h: f"{w} by {h} rectangle"
print scene.name
print (describe scene.item)
demo
circle of radius 2.0
Constructors qualify like values do, in expressions and in case patterns alike, so
Shapes.Circle 2.0 builds one and case Shapes.Circle r: takes it apart. In type position the
bare name works as well, so the record above could equally say item: Placed. Because a type can be
named from a file other than the one declaring it, a record that holds another module’s data no
longer forces the two into a single file.
Exercise
Here is a flat program with two temperature conversions. Group both functions into an in-file module
called Temp, then call them qualified as Temp.cToF and Temp.fToC. The output stays the same.
let cToF c = c * 9 // 5 + 32
let fToC f = (f - 32) * 5 // 9
print (cToF 100)
print (fToC 212)
Expected output:
212
100
Show solution
module Temp =
let cToF c = c * 9 // 5 + 32
let fToC f = (f - 32) * 5 // 9
print (Temp.cToF 100)
print (Temp.fToC 212)
The two functions now live under Temp, and the call sites qualify them. As a take-home step, move
the module body into its own temp.pyfun, import Temp from a main.pyfun, and run the project with
pyfun run main.pyfun.