Haskell command-line utility using GHC generics
source link: https://www.tuicool.com/articles/hit/YbaQnqr
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Today, Justin Woo
wrote a post about writing a simple Haskell command-line utility with minimal dependencies
. The utility is a small wrapper around the nix-prefetch-git
command.
In the post he called out people who recommend overly complex solutions on Twitter:
Nowadays if you read about Haskell on Twitter, you will quickly find that everyone is constantly screaming about some “advanced” techniques and trying to flex on each other
However, I hope to show that we can simplify his original solution by taking advantage of just one feature: Haskell’s support for generating code from data-type definitions. My aim is to convince you that this Haskell feature improves code clarity without increasing the difficulty. If anything, I consider this version less difficult both to read and write.
Without much ado, here is my solution to the same problem (official Twitter edition):
{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} import Data.Aeson (FromJSON, ToJSON) import Data.Text (Text) import Options.Generic (Generic, ParseRecord) import qualified Data.Aeson import qualified Data.ByteString.Lazy import qualified Data.Text.Encoding import qualified Data.Text.IO import qualified Options.Generic import qualified Turtle data Options = Options { branch :: Bool , fetchgit :: Bool , hashOnly :: Bool , owner :: Text , repo :: Text , rev :: Maybe Text } deriving (Generic, ParseRecord) data NixPrefetchGitOutput = NixPrefetchGitOutput { url :: Text , rev :: Text , date :: Text , sha256 :: Text , fetchSubmodules :: Bool } deriving (Generic, FromJSON) data GitTemplate = GitTemplate { url :: Text , sha256 :: Text } deriving (Generic, ToJSON) data GitHubTemplate = GitHubTemplate { owner :: Text , repo :: Text , rev :: Text , sha256 :: Text } deriving (Generic, ToJSON) main :: IO () main = do Options {..} <- Options.Generic.getRecord "Wrapper around nix-prefetch-git" let revisionFlag = case (rev, branch) of (Just r , True ) -> "--rev origin/" <> r (Just r , False) -> "--rev " <> r (Nothing, _ ) -> "" let url = "https://github.com/" <> owner <> "/" <> repo <> ".git/" let command = "GIT_TERMINAL_PROMPT=0 nix-prefetch-git " <> url <> " --quiet " <> revisionFlag text <- Turtle.strict (Turtle.inshell command Turtle.empty) let bytes = Data.Text.Encoding.encodeUtf8 text NixPrefetchGitOutput {..} <- case Data.Aeson.eitherDecodeStrict bytes of Left string -> fail string Right result -> return result if hashOnly then Data.Text.IO.putStrLn sha256 else if fetchgit then Data.ByteString.Lazy.putStr (Data.Aeson.encode (GitTemplate {..})) else Data.ByteString.Lazy.putStr (Data.Aeson.encode (GitHubTemplate {..}))
This solution takes advantage of two libraries:
-
optparse-generic
This is a library I authored which auto-generates a command-line interface (i.e. argument parser) from a Haskell datatype definition.
-
aeson
This is a library that generates JSON encoders/decoders from Haskell datatype definitions.
Both libraries take advantage of GHC’s support for generating code statically from datatype definitions. This support is known as “GHC generics”. While a bit tricky for a library author to support, it’s very easy for a library user to consume.
All a user has to do is enable two extensions:
{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-}
… and then they can auto-generate an instance for any typeclass that implements GHC generics support by adding a line like this to the end of their data type:
deriving (Generic, SomeTypeClass)
You can see that in the above example, replacing SomeTypeClass
with FromJSON
, ToJSON
, and ParseRecord
.
And that’s it. There’s really not much more to it than that. The result is significantly shorter than the original example (which still omitted quite a bit of code) and (in my opinion) easier to follow because actual program logic isn’t diluted by superficial encoding/decoding concerns.
I will note that the original solution only requires using libraries that are provided as part of a default GHC installation. However, given that the example is a wrapper around nix-prefetch-git
then that implies that the user already has Nix installed, so they can obtain the necessary libraries by running this command:
$ nix-shell --packages \ 'haskellPackages.ghcWithPackages (p: [ p.turtle p.optparse-generic p.aeson ])'
… which is one of the reasons I like to use Nix.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK