Well-typed paths
Despite the fact that there are several “path” libraries in Haskell, I decided to write a new one I would like to use.
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, or be relative to some absolute path called home
. And it can point either to a directory or a file. Instead of encoding these cases directly as type sums, we will do some trick.
The absolute path is just a path that relative to the root, the same for the home and current working directory. Let’s create a type that indicates subject of relativity:
data Origin = Root | Now | Home | Early | Vague
And a sum type shows which object we point to:
data Points = Directory | File
We use stack as a core data structure for path, so our type is:
{-# language DataKinds, KindSignatures #-}
newtype Outline (origin :: Origin) (points :: Points) =
Outline { outline :: Cofree Maybe String }
We can split our paths on groups:
type Incompleted = Relative | Current | Homeward | Previous
type Certain = Absolute | Current | Homeward | Previous
Now we need some rules that can help us build valid paths depending on theirs types. So, we can do this:
Incompleted Path To Directory + Relative Path To Directory = Relative Path To Directory
"usr/local/" + "etc/" = "usr/local/etc/"
Incompleted Path To Directory + Relative Path To File = Relative Path To File
"bin/" + "git" = "bin/git"
Absolute Path To Directory + Incompleted Path To Directory = Absolute Path To Directory
"/usr/local/" + "etc/" = "/usr/local/etc/" =
Absolute Path To Directory + Incompleted Path To File = Absolute Path To File
"/usr/bin/" + "git" = "/usr/bin/git"
But we can’t do this:
_ Path To File + _ Path To File = ???
_ Path To File + _ Path To Directory = ???
Absolute Path To _ + Absolute Path To _ = ???
Incompleted Path To _ + Absolute Path To _ = ???
Based on these rules we can define two generalized combinators. Current
, Homeward
, Previous
and Relative
paths are the same internally, they are different only for type system.
(<^>) :: Incompleted Path To Directory -> Relative Path To points -> Relative Path To points
(</>) :: Absolute Path To Directory -> Incompleted Path To points -> Absolute Path To points
And, if you want improve your code readability, you can use specialized combinators:
-- Add relative path to incompleted path:
(<.^>) :: Current Path To Directory -> Relative Path To points -> Currently Path To points
(<~^>) :: Homeward Path To Directory -> Relative Path To points -> Homeward Path To points
(<-^>) :: Previous Path To Directory -> Relative Path To points -> Previous Path To points
(<^^>) :: Relative Path To Directory -> Relative Path To points -> Relative Path To points
-- Absolutize incompleted path:
(</.>) :: Absolute Path To Directory -> Current Path To points -> Absolute Path To points
(</~>) :: Absolute Path To Directory -> Homeward Path To points -> Absolute Path To points
(</->) :: Absolute Path To Directory -> Previous Path To points -> Absolute Path To points
(</^>) :: Absolute Path To Directory -> Relative Path To points -> Absolute Path To points
There are some functions in System.Monopati.Posix.Calls
that work with our Path definition:
current
returns Nothing
. We return absolute path because we actually want to know where we are exactly in the filesystem:
current :: IO (Maybe (Absolute Path To 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 :: Absolute Path To Directory -> IO (Absolute Path To Directory)
Creating and removing directories with absolute paths only:
create :: Absolute Path To Directory -> IO ()
remove :: Absolute Path To Directory -> IO ()
Let’s imagine that we need to save some content in temporary files grouped on folders based on some prefix.
mkdir :: String -> IO (Absolute Path To Directory)
mkdir prefix = create $ part "Temporary" <^> part prefix
filepath :: String -> String -> IO (Absolute Path To 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.
As you can understand, this library motivates you use only absolute paths, but not force it.
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.