I encountered a pretty simple problem recently, or how it’s probably
better phrased, I recently made my life a bit harder by thinking:
“What if?”. I needed to pass multiple arguments via the CLI to a
tasty test-suite
to use in some tests. As an example, I’ll pass the connection string to use to
connect to a PostgreSQL database. That could go something like the following.Typically, you’d tell tasty about the existence of your new option via
includingOptions, and then access these options via
askOption.-- Create the option in the style of tasty.
data PgConnectionString = PgConnectionString Text
instance IsOption PgConnectionString where ...
-- `defaultMainWithIngredients` gives us an ingredient to
-- plug the option into, so it gets shown in the CLI.
main = do
defaultMainWithIngredients
[ includingOptions [Option (Proxy @PgConnectionString)]
] $ askOption setupTests
-- `askOption` propogates it so we can setup our test-suite
-- to make the appropriate calls.
setupTests :: PgConnectionString -> TestTree
setupTests _ = undefined
That’s all nice and all, but the querying doesn’t really extend in a nice way.
Concretely, I’d need to call askOption for each individual option I’d want to
add to my setupTests function.data PgPoolSize = PgPoolSize Text
instance IsOption PgPoolSize where ...
main = do
defaultMainWithIngredients
[ includingOptions
[ Option (Proxy @PgConnectionString)
, Option (Proxy @PgPoolSize)
]
] $
askOption $ \(connectionString :: PgConnectionString) ->
askOption $ \(poolSize :: PgPoolSize) ->
setupTests connectionString poolSize
What if I could write a helper that did this passing around of options for me
for free?main = do
defaultMainWithIngredients
[ includingOptions
[ Option (Proxy @PgConnectionString)
, Option (Proxy @PgPoolSize)
]
] $ withTests setupTests
As has been a recurring pattern in a few of my posts, variadic functions come to
the rescue! We want to build up a variadic continuation (like last
time) that
setupTests can be passed to. In this case we want instance resolution to keep
adding arguments via askOption at each resolution step.Continuation functions generally have the pattern of forall c. (a -> c) -> c,
and considering we’re intending on giving this function our test setup, some
good intuition (or practical experimentation) gives something of the form
forall r. ((a -> c) -> r) -> r. Here a will play the role of capturing more
and more options, and c will capture the TestTree. I’m not super sure, but I feel like there’s a simpler alternative
here that either reduces the number of arrows or the number of type variables
in the type class. I kept hitting my head on trying to find a simpler
representation though.
class WithOptions a c where
withOptions_ :: forall r. ((a -> c) -> r) -> r
instance WithOptions TestTree TestTree where
withOptions_ f = f identity
instance (IsOption o, WithOptions c TestTree)
=> WithOptions (o -> c) TestTree where
withOptions_ outer = withOptions_ $
\next -> outer $ \inner -> askOption $
\option -> next (inner option)
A nice benefit of these variadic functions is, once you’ve found your core
definition and how the incremental step should be setup, the implementation
flows out of the type variables you’ve used. In this case the base case of the
continuation is the identity function, and in the incremental step, we call
askOption with the appropriate type. This gives the very nice final interface
of…withOptions :: WithOptions a TestTree => a -> TestTree
withOptions f = withOptions_ $ \addOptions -> addOptions f
main = do
defaultMainWithIngredients
[ includingOptions
[ Option (Proxy @PgConnectionString)
, Option (Proxy @PgPoolSize)
]
] $ withOptions setupTests