Well-typed paths: revisited

3 minute read

Published:

Despite the fact that there are several well-typed “path” libraries in Haskell, I decided to write a new one I would like to use: the library called monopati, whose definitions are used in this post.

Problem description

Often (when you write a useful program) you need to do something to the filesystem. Using temporary files, reading directory contents, writing logs - in all of these cases you need to clarify the path. But path can be specified either in absolute or relative form. And it can point either to a directory or a file. Okay, let’s encode these cases in types.

{-# language DataKinds, KindSignatures #-}

data Reference = Absolute | Relative

data Points = Directory | File

type Stack = Cofree Maybe

data Path (reference :: Reference) (points :: Points) = Path { path :: Stack String }

We use stack as a core data structure for path - it will become clear later why it was chosen. Now we need some rules that can help us build valid paths depending on theirs types. So, we can do this:

Path Relative Directory + Path Relative Directory = Path Relative Directory
"usr/local/" <> "etc/" = "usr/local/etc/"
Path Relative Directory + Path Relative File = Path Relative File
"bin/" <> "git" = "bin/git"
Path Absolute Directory + Path Relative Directory = Path Absolute Directory
"/usr/local/" <> "etc/" = "/usr/local/etc/" =
Path Absolute Directory + Path Relative File = Path Absolute File
"/usr/bin/" <> "git" = "/usr/bin/git"

But we can’t do this:

Path _ File + Path _ File = ???
Path _ File + Path _ Directory = ???
Path Absolute _ + Path Absolute _ = ???
Path Relative _ + Path Absolute _ = ???

Based on these rules we can define two main combinators.

(<^>) :: Path Relative Directory -> Path Relative points -> Path Relative points
(</>) :: Path Absolute Directory -> Path Relative points -> Path Absolute points

Get our hands dirty

There are some functions in System.Monopati.Posix that work with our Path definition:

  • As you may remember, we use Stack - it’s an not empty inductive data structure. When we are in a root, we have no directory or file to point in - we are in the starting point, so current returns Nothing. We return absolute path because we actually want to know where we are exactly in the filesystem:
    current :: IO (Maybe (Path Absolute Directory))
    
  • Sometimes we want to change our current working directory for some reason. As documentation of System.Directory says: it’s highly recommended to use absolute rather than relative paths cause of current working directory is a global state shared among all threads:
    change :: Path Absolute Directory -> IO (Path Absolute Directory)
    
  • Often I need to create some folder and get an absolute path of it:
    create :: Path Relative Directory -> IO (Path Absolute Directory)
    

Simple example of usage

Let’s imagine that we need to save some content in temporary files grouped on folders based on some prefix.

mkdir :: String -> IO (Path Absolute Directory)
mkdir prefix = create $ part "Temporary" <^> part prefix

filepath :: String -> String -> IO (Path Absolute File)
filepath filename prefix = (\dir -> dir </> part filename) <$> mkdir prefix

In this example, part function is like a pure for Path but it takes strings only. We create a directory and then construct a full path to the file.

Motivation of using this library

Well, it’s easier for me to define what exactly I don’t like in another “path”-libraries:

  • filepath - The most popular, but using raw strings
  • path - TemplateHaskell (I really hate it), using raw strings in internals
  • posix-paths - Focusing on performance instead of usage simplicity

Leave a Comment