On Style
once neglected, Haskellers are becoming more stylish
For me the style of a program has always been important — the better the arrangement, the easier to read. I try to adopt a style that communicate what code is doing from the layout.
Take the main function from this blog’s generator, which of course has been inspired by many other example blogs down the years (including my own). Jasper’s blog is laid out on the same principles so he gets the credit in my mind.
main :: IO ()
main = hakyllWith cfg $ do
match (fromList ["favicon.ico","apple-touch-icon.png"]) $ do
route idRoute
compile copyFileCompiler
match (fromRegex "^assets/scss/[^_].*.scss") $ do
route $ setExtension "css"
compile $ fmap compressCss <$> sassCompilerWith sass_options
match "assets/css/*" $ do
route idRoute
compile compressCssCompiler
match "assets/images/*" $ do
route idRoute
compile copyFileCompiler
match (fromList ["pages/about.md"]) $ do
route $ setExtension "html"
compile $ pandocCompiler
>>= loadAndApplyTemplate "templates/post.html" postCtx
>>= loadAndApplyTemplate "templates/default.html" postCtx
>>= relativizeUrls
match "posts/*" $ do
route $ setExtension "html"
compile $ pandocCompiler
>>= loadAndApplyTemplate "templates/post.html" postCtx
>>= loadAndApplyTemplate "templates/default.html" postCtx
>>= relativizeUrls
create ["pages/extension-reviews.html"] $ do
route idRoute
compile $ do
let ctx = mconcat
[ constField "title" "Language Extension Reviews"
, field "extension-review-list" extensionReviewList
, defaultContext
]
makeItem ""
>>= loadAndApplyTemplate "templates/extension-reviews.html" ctx
>>= loadAndApplyTemplate "templates/default.html" ctx
>>= relativizeUrls
create ["pages/package-reviews.html"] $ do
route idRoute
compile $ do
let ctx = mconcat
[ constField "title" "Package Reviews"
, field "package-review-list" packageReviewList
, defaultContext
]
makeItem ""
>>= loadAndApplyTemplate "templates/package-reviews.html" ctx
>>= loadAndApplyTemplate "templates/default.html" ctx
>>= relativizeUrls
create ["pages/structure-reviews.html"] $ do
route idRoute
compile $ do
let ctx = mconcat
[ constField "title" "Structure Reviews"
, field "structure-review-list" structureReviewList
, defaultContext
]
makeItem ""
>>= loadAndApplyTemplate "templates/structure-reviews.html" ctx
>>= loadAndApplyTemplate "templates/default.html" ctx
>>= relativizeUrls
create ["pages/archive.html"] $ do
route idRoute
compile $ do
posts <- chronological =<< loadAll "posts/*"
let ctx = mconcat
[ listField "posts" postCtx $ pure posts
, constField "title" "Archives"
, defaultContext
]
makeItem ""
>>= loadAndApplyTemplate "templates/archive.html" ctx
>>= loadAndApplyTemplate "templates/default.html" ctx
>>= relativizeUrls
create ["index.html"] $ do
route idRoute
compile $ do
posts <- recentFirst =<< loadAll "posts/*"
let ctx = mconcat
[ listField "posts" postCtx (return posts)
, constField "title" "Home"
, constField "heading" "Posts"
, defaultContext
]
makeItem ""
>>= loadAndApplyTemplate "templates/archive.html" ctx
>>= loadAndApplyTemplate "templates/default.html" ctx
>>= relativizeUrls
match "templates/**" $ compile templateBodyCompiler
create ["atom.xml"] $ do
route idRoute
compile $ do
let feedCtx = postCtx <>
constField "description" "This is the post description"
posts <- fmap (take 10) . recentFirst =<< loadAll "posts/*"
renderAtom feedConfig feedCtx posts
create ["rss.xml"] $ do
route idRoute
compile $ do
let feedCtx = postCtx <>
constField "description" "This is the post description"
posts <- fmap (take 10) . recentFirst =<< loadAll "posts/*"
renderRss feedConfig feedCtx posts
In the bad old days I would expect to be greeted by a wall of text, with each stage compressed into as few characters as possible with no consideration given to layout as a means of explaining the intent of the code. I have seen non-Haskellers singing the praises of Hakyll with its pure Haskell DSL — and I doubt if that would have happened if the Hakyll blogs were poorly laid out.
Now of course, in commercial team contexts some people are strong advocates of running all code through maximally opinionated code formatters like ormolu and it is interesting to see what ormolu makes of our blog main
function.
main :: IO ()
main = hakyllWith cfg $ do
match (fromList ["favicon.ico", "apple-touch-icon.png"]) $ do
route idRoute
compile copyFileCompiler
match (fromRegex "^assets/scss/[^_].*.scss") $ do
route $ setExtension "css"
compile $ fmap compressCss <$> sassCompilerWith sass_options
match "assets/css/*" $ do
route idRoute
compile compressCssCompiler
match "assets/images/*" $ do
route idRoute
compile copyFileCompiler
match (fromList ["pages/about.md"]) $ do
route $ setExtension "html"
compile $
pandocCompiler
>>= loadAndApplyTemplate "templates/post.html" postCtx
>>= loadAndApplyTemplate "templates/default.html" postCtx
>>= relativizeUrls
match "posts/*" $ do
route $ setExtension "html"
compile $
pandocCompiler
>>= loadAndApplyTemplate "templates/post.html" postCtx
>>= loadAndApplyTemplate "templates/default.html" postCtx
>>= relativizeUrls
create ["pages/extension-reviews.html"] $ do
route idRoute
compile $ do
let ctx =
mconcat
[ constField "title" "Language Extension Reviews",
field "extension-review-list" extensionReviewList,
defaultContext
]
makeItem ""
>>= loadAndApplyTemplate "templates/extension-reviews.html" ctx
>>= loadAndApplyTemplate "templates/default.html" ctx
>>= relativizeUrls
create ["pages/package-reviews.html"] $ do
route idRoute
compile $ do
let ctx =
mconcat
[ constField "title" "Package Reviews",
field "package-review-list" packageReviewList,
defaultContext
]
makeItem ""
>>= loadAndApplyTemplate "templates/package-reviews.html" ctx
>>= loadAndApplyTemplate "templates/default.html" ctx
>>= relativizeUrls
create ["pages/structure-reviews.html"] $ do
route idRoute
compile $ do
let ctx =
mconcat
[ constField "title" "Structure Reviews",
field "structure-review-list" structureReviewList,
defaultContext
]
makeItem ""
>>= loadAndApplyTemplate "templates/structure-reviews.html" ctx
>>= loadAndApplyTemplate "templates/default.html" ctx
>>= relativizeUrls
create ["pages/archive.html"] $ do
route idRoute
compile $ do
posts <- chronological =<< loadAll "posts/*"
let ctx =
mconcat
[ listField "posts" postCtx $ pure posts,
constField "title" "Archives",
defaultContext
]
makeItem ""
>>= loadAndApplyTemplate "templates/archive.html" ctx
>>= loadAndApplyTemplate "templates/default.html" ctx
>>= relativizeUrls
create ["index.html"] $ do
route idRoute
compile $ do
posts <- recentFirst =<< loadAll "posts/*"
let ctx =
mconcat
[ listField "posts" postCtx (return posts),
constField "title" "Home",
constField "heading" "Posts",
defaultContext
]
makeItem ""
>>= loadAndApplyTemplate "templates/archive.html" ctx
>>= loadAndApplyTemplate "templates/default.html" ctx
>>= relativizeUrls
match "templates/**" $ compile templateBodyCompiler
create ["atom.xml"] $ do
route idRoute
compile $ do
let feedCtx =
postCtx
<> constField "description" "This is the post description"
posts <- fmap (take 10) . recentFirst =<< loadAll "posts/*"
renderAtom feedConfig feedCtx posts
create ["rss.xml"] $ do
route idRoute
compile $ do
let feedCtx =
postCtx
<> constField "description" "This is the post description"
posts <- fmap (take 10) . recentFirst =<< loadAll "posts/*"
renderRss feedConfig feedCtx posts
I am impressed by how well it has done and I do see the value of such opinionated code formatters in large industrial projects but I think we should not stop here and keep iterating because reading code is such a critical activity in the whole software development lifecycle. We have seen major benefits of good modern layout over poor layout and I suspect there is more to come.
Points to note.
I am not sure that removing the empty lines has helped and would be tempted to leave that one with the coders.
There are some things that an algoritmic formatter is most unlikely to capture (like the alignment of the post contexts inside pipeline stages).
This particular example — the main function of a Hakyll blog — is one that is just so singular and critical to the (blog) enterprise that I would like the option of requesting that it should not be algorithmically formatted (at least until the formatters get really good) by dropping a pragma that tells the formatter to leave well alone.
There are some choices that have been reversed that I really do not like (I am not going to name them) and I think it would be wise of any lead engineer to allow local pragmatic hints where well-known variants are concerned. (Either that or adopt the correct conventions!)
Got an issue with any of this? Please share or drop me a line (see below).