I've been playing around with writing a simple static blog generator in Haskell (mostly for experience, since there's probably more than a few already made choices that fit my needs). I don't have very much experience with functional programming, but I've been doing a lot of C++ lately, so using typeclasses as "interfaces" seems natural to me.
For the beginnings configuration, I've come up with this typeclass:
module BlogGenerator.Config where
class Config t where
outputPath :: t -> FilePath
templatePathFor :: String -> t -> FilePath
And for a YAML implementation (using this):
module BlogGenerator.YamlConfig
( YamlConfig
, toYamlValue
, toYamlConfig
) where
import qualified Data.HashMap.Strict as HashMap
import Data.Maybe
import Data.ByteString
import Data.Text as Text
import BlogGenerator.Config
import qualified Data.Yaml as Yaml
newtype YamlConfig = YamlConfig
{ toYamlValue :: Yaml.Value
}
toYamlConfig :: ByteString -> YamlConfig
toYamlConfig = YamlConfig . fromJust . decode
where
decode = (Yaml.decode :: ByteString -> Maybe Yaml.Value)
instance Config YamlConfig where
templatePathFor name = stringYamlLookup ["templates", name] . toYamlValue
outputPath = stringYamlLookup ["output", "path"] . toYamlValue
-- helpers
yamlLookup :: [String] -> Yaml.Value -> Yaml.Value
yamlLookup [] _ = Yaml.Null
yamlLookup (key:[]) value = fromJust $ look key $ toObject value
where
look k v = HashMap.lookup (Text.pack k) v
toObject = fromJust . toOptionalObject
toOptionalObject (Yaml.Object object) = Just object
toOptionalObject _ = Nothing
yamlLookup (key:rest) value = yamlLookup rest (yamlLookup [key] value)
stringYamlLookup :: [String] -> Yaml.Value -> String
stringYamlLookup ks v = Text.unpack (toText (yamlLookup ks v))
where
toText = fromJust . toOptionalText
toOptionalText (Yaml.String text) = Just text
toOptionalText _ = Nothing
I know the YAML package contains support for automagic binding to records, but tight coupling between the external format and the internal format is fairly unappealing to me, and before I start trusting advanced code like that I feel better about writing a "low-tech" version of my own.
Is this general approach "idiomatic" for Haskell, and am I following good practices/idioms in general?
1 Answer 1
Sorry to say this is not good practice. Exactly why is a bit more technical of an explanation than I like to give off the cuff, Google for "Haskell typeclass interface" and you'll find a bunch of threads comparing and contrasting and recommending you use an ADT. Here's one from Reddit with a game programming example that I think is particularly practical.
In your case, I imagine you intend to provide different configuration methods (XML, command line flags, &c) which would also be instances of Config
. You've already realized that you want to work with some abstract Config
thing elsewhere in your program and not a YamlConfig
, because whatever is using a configuration value doesn't care where that value came from. The next logical step is that by making Config
a class, you actually are still passing a YamlConfig
around everywhere, you're just relying on callers to never look more closely at it than as a generic Config
instance. You've got implementation leaking all over the place.
Instead, making Config
a datatype.
data Config = Config { outputPath :: FilePath
, template :: String -> FilePath
}
Now populate it from a YAML source.
makeConfig :: Yaml.Value -> Config
makeConfig yaml = Config { outputPath = stringYamlLookup ["output", "path"] yaml
, template = \name -> stringYamlLookup ["templates", name] yaml
}
It's easy to make arbitrary Config
s now. Maybe you want a default Config
.
defaultConfig :: Config
defaultConfig = Config { outputPath = "output/"
, template = ("template/" ++)
}
You can even make lists of Config
s from different sources ([defaultConfig, makeConfig yaml]
).