3
\$\begingroup\$

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?

200_success
146k22 gold badges190 silver badges478 bronze badges
asked Mar 3, 2015 at 12:49
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

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 Configs now. Maybe you want a default Config.

defaultConfig :: Config
defaultConfig = Config { outputPath = "output/"
 , template = ("template/" ++)
 }

You can even make lists of Configs from different sources ([defaultConfig, makeConfig yaml]).

answered Mar 3, 2015 at 18:18
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.