Using the package path
for 'well-typed paths' in an application which writes and reads records containing filepath data. I miss a function to write the typed file path information in a record to a file and read it from the file.
Transforming Path a b
types to strings with toFilePath
to write and to read requires a conversion and managing the type information (Rel
vs Abs
, File
vs Dir
) separately, which is error prone.
I experimented with typeable
but was not able to find a solution, which would allow me to include a typed Path a b
in a record and read and write this record to/from a file. What solutions are possible?
I suggest taking a step back. As a general rule, you don't need to store type information as part of the serialization because the correct type information will already be available at the point of deserialization.
I mean, you MIGHT be trying to serialize a Path
in isolation, but normally it would be part of a larger data structure, say a configuration structure:
data Config = Config { gamedir :: Path Abs Dir
, mods :: [Path Rel Dir]
, saves :: [Path Rel File] }
...
cfg <- Config <$> parseAbsDir "/home/john/SuperPong"
<*> mapM parseRelDir ["paddleSkins", "multiplayer"]
<*> mapM parseRelFile ["game1.sav", "game2.sav"]
If you serialize cfg
using the existing ToJSON
instances, you get:
{"gamedir":"/home/john/SuperPong/","mods":["paddleSkins/","multiplayer/"],"saves":
["game1.sav","game2.sav"]}
This lacks type information, but none is needed, because what are you going to do with this string except deserialize it back to a Config
?
let cfg = decode txt :: Maybe Config
print cfg
This yields a well typed Config
whose gamedir
field is a well typed Path Abs Dir
and so on.
If you want your Config
structure to allow different Path
types for a particular field, say a gamedir
field that can either be absolute or relative to the user's home directory, the most natural way to accomplish this is via a sum type:
data Config = Config { gamedir :: GameDir
^^^^^^^
, mods :: [Path Rel Dir]
, saves :: [Path Rel File] }
data GameDir = AbsGameDir (Path Abs Dir) | HomeGameDir (Path Rel Dir)
If you serialize such a Config
using, say, the ToJSON
instance, it'll include a tag for the constructor that will properly restore a Config
with either an AbsGameDir
or a HomeGameDir
, as appropriate.
If you are imagining that you would want to make Config
polymorphic:
data Config b = Config { gamedir :: Path b Dir
^ ^
, mods :: [Path Rel Dir]
, saves :: [Path Rel File] }
and serialize that, well, that's a pretty gnarly design even setting aside the serialization/deserialization challenges (class HasGameDir
, anyone?).
However, if you genuinely find yourself in a situation where you want to serialize and deserialize a Path b t
polymorphically without knowing b
and/or t
, then you can't do much better than defining the sum type:
data SomePath = AbsDir (Path Abs Dir) | AbsFile (Path Abs File)
| RelDir (Path Rel Dir) | RelFile (Path Rel File)
and serializing that. Any self-respecting Haskell serialization scheme will handle serializing SomePath
properly with some kind of constructor-based tagging which, in turn, will guarantee type safety.
Finally, if your goal is somehow to ensure that the serialization itself is type safe in the sense that attempts to deserialize to the "wrong type" will fail, well, this seems like the wrong level of abstraction. If you anticipate serializing different types to the same place (e.g., serializing different, backward compatible versions of a Config
structure to a configuration file), you should be using some header metadata or a version tag to identify the correct type variant to deserialize.