Two units divide one job here, and the division is the lesson.
The paths unit is pure text algebra. Join, DirName, BaseName and
Extension rewrite POSIX path spellings without ever asking the operating
system whether anything exists — so every one of them is total, and the
unit has no failure surface at all. The files unit is where a path
finally meets the filesystem, and there every answer rides the
failure channel.
A function that cannot fail should not pretend it might; a function that touches the world should say so in its signature. So the two live in different units and read differently at every call site.
The working example is
examples/PathsAndDirectories:
make -C examples/PathsAndDirectories runThe algebra, and whose semantics it follows
Every verb takes its definition from the industry precedent with the cleanest textual definition, so each answer is checkable against an outside oracle:
| Call | Answers | Following |
|---|---|---|
Join("/usr/lib", "mica") | /usr/lib/mica | python os.path.join, C++ operator/ |
Join("/usr/lib/", "mica") | /usr/lib/mica | exactly the separator the seam is missing is added |
Join("/usr/lib", "/etc") | /etc | an absolute right side replaces the left |
DirName("/usr/lib/mica.a") | /usr/lib | dirname(1) |
DirName("/usr/lib/") | /usr | trailing separators stripped before the split |
BaseName("mica.a") | mica.a | basename(1) — no separator means all base |
DirName("mica.a") | . | …and the current directory |
Extension("archive.tar.gz") | .gz | python splitext — the last dot |
Extension(".bashrc") | (empty) | a run of dots that opens a component never counts |
Three consequences worth stating outright:
Joinnever cleans. A doubled separator inside either side, or a trailing separator you passed, survives untouched. Nothing is collapsed or resolved, because resolution is a filesystem question and this unit does not ask filesystem questions.- A dotfile is a name, not an extension.
.bashrchas no extension. - These answers are the same on a machine where none of those paths exist. That is what “pure text” buys you: the algebra is testable without a filesystem, and it cannot fail.
The walk, where the world answers back
names := Entries(WorkDir) on fail leave;
subdirs := Directories(WorkDir) on fail leave;
Sort(address names);Entries answers everything the directory holds — the . and ..
self-references already skipped at the source — and Directories answers
only what a descent could enter next. Both fail with the reason they
cannot walk: Enoent for a missing path, Enotdir for a file where a
directory was named, Eacces for a permission wall.
The order is the filesystem’s, and no filesystem promises it is
sorted. So a caller who wants order sorts the answer — which is what the
example does, with the arrays unit’s generic
Sort: the comparison is the element type’s own <,
cloned per element type, so ordering a list of strings needs no comparator
and no function pointer.
Then the two units meet. Every name that comes back is a bare component,
so Join puts it back onto its directory and Extension classifies it —
text algebra applied to filesystem answers:
3 entries, 1 of them directories
alpha.txt extension .txt full path paths-demo/alpha.txt
beta.log extension .log full path paths-demo/beta.log
nested (no extension) full path paths-demo/nestedThe split, in one contrast
The example ends by asking for a directory that is not there:
DirName of a path that does not exist still answers: /no/such/place
Entries failed with Enoent — the missing-path reason, by nameSame spelling, two units, two entirely proper answers. DirName answers
happily because it never asked anyone. Entries fails with a name because
it did. Neither returns a sentinel, and neither traps — this is the
environment’s business, which is what the failure channel is for.
Directory creation and removal are still raw POSIX (MkDir, RmDir
via imp … : posix), on the very same failure channel — so one idiom
covers the curated road and the raw one, as
Mica calls Linux shows in full.
Next
Memory classes: hosted, and a fixed arena — the same programs under the OS heap and under one fixed block, for the targets where there is no OS heap at all.