Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Path

Path: lexical file paths, with no filesystem in sight.

A Path is a list of components and a flag saying whether it starts at the root. Every operation here is pure: none of them asks the host whether a path exists, what a symlink points at, or what the current directory is. Those are filesystem questions and belong to a capability, not to a value.

Parsing normalizes only what is lexically certain. Repeated separators, a trailing separator, and . components carry no meaning, so they are dropped and "a//./b/" is the same path as "a/b". A .. component is kept as it is, because erasing it together with the component before it is only right when that component is not a symlink, and nothing here can know. For the same reason parent refuses a path whose last component is .. rather than guessing.

Paths combine with </>: dir </> name is name inside dir. The operator groups to the left, so root() </> etc </> hosts builds outward from the root, and an absolute right side replaces what is on its left, since it is already anchored. With the empty relative path as identity this makes Path a monoid, and the module gives it the Semigroup and Monoid instances.

The host boundary is UTF-8. A Path is built from a String, so a name the host spells in bytes that are not valid UTF-8 cannot be written down here at all; path fails on the one string a host can never accept, one containing a NUL byte, rather than letting it reach a process call and be cut short there.

match path("/usr//local/./bin/") of
  Ok(p) => path_str(p </> relative(["prism"]))
  Err(_) => "invalid"
/usr/local/bin/prism

Types

PathError

type PathError = EmptyPath | ContainsNul deriving (Eq, Show)

Why a string is not a path.

Path

type Path = Path(Bool, List(String))

A lexical path: whether it is rooted, and its components in order.

Instances

pathJoinPath

instance pathJoinPath : PathJoin(Path)

p </> q is join(p, q). The operator groups to the left and binds looser than :: and arithmetic, tighter than comparison.

path_str(root() </> relative(["usr"]) </> relative(["local", "bin"]))
/usr/local/bin

A rooted right operand discards everything on its left:

path_str(relative(["build"]) </> root() </> relative(["tmp"]))
/tmp

semigroupPath

instance semigroupPath : Semigroup(Path)

Joining is associative: components concatenate, and an absolute operand resets the result wherever it falls. No .. is ever collapsed, so the law holds exactly rather than only up to the filesystem.

let a = relative(["a"])
let b = root() </> relative(["b"])
let c = relative(["c"])
path_eq((a </> b) </> c, a </> (b </> c))
true

monoidPath

instance monoidPath : Monoid(Path)

The empty relative path is the identity on both sides. It prints as ..

let p = relative(["docs", "index.md"])
path_eq(mappend(mempty(), p), p) && path_eq(mappend(p, mempty()), p)
true

Functions and Values

path

path : (String) -> Result(Path.Path, Path.PathError)

Parse a path, normalizing it lexically. Fails on the empty string, which names nothing, and on a string containing a NUL byte, which no host accepts.

match path("a//b/./c/") of
  Ok(p) => path_str(p)
  Err(_) => "invalid"
a/b/c

relative

relative : (List(String)) -> Path.Path

A relative path from components, each of which must be a single name. Components that are empty, ., or contain a separator or NUL are dropped.

path_str(relative(["src", "main.pr"]))
src/main.pr

root

root : () -> Path.Path

The root directory.

path_str

path_str : (Path.Path) -> String

Render a path for the host. The relative path with no components is ..

is_absolute

is_absolute : (Path.Path) -> Bool

Is the path rooted?

components

components : (Path.Path) -> List(String)

The components, in order. The root is not a component.

join

join : (Path.Path, Path.Path) -> Path.Path

q relative to p, the function behind p </> q. An absolute q is already anchored, so it is the answer as it stands.

path_str(join(relative(["src"]), relative(["main.pr"])))
src/main.pr

parent

parent : (Path.Path) -> Option(Path.Path)

The path one component up, or None when that is not lexically known: at the root, at the empty relative path, and after a ...

match path("/a/b") of
  Ok(p) => map_option(path_str, parent(p))
  Err(_) => None
Some(/a)

file_name

file_name : (Path.Path) -> Option(String)

The last component, unless it is .. or there is none.

extension

extension : (Path.Path) -> Option(String)

The file name after its last .. A name with no dot, a dot only in first position (a hidden file), or a dot only in last position has none.

match path("dist/prism.tar.gz") of
  Ok(p) => extension(p)
  Err(_) => None
Some(gz)

stem

stem : (Path.Path) -> Option(String)

The file name before its extension.

path_eq

path_eq : (Path.Path, Path.Path) -> Bool

Equality of paths is equality of their lexical forms.