monadic-bang: GHC plugin to desugar ! into do-notation

This is a package candidate release! Here you can preview how this package release will appear once published to the main package index (which can be accomplished via the 'maintain' link below). Please note that once a package has been published to the main package index it cannot be undone! Please consult the package uploading documentation for more information.

[maintain] [Publish]

A plugin for GHC which takes expressions prefixed with a ! and effectively takes them out of their monadic context, by creating bind statements in the do-block surrounding the expression. Inspired by Idris's !-notation. For more information, see README.md.


[Skip to Readme]

Properties

Versions 0.1.0.0, 0.1.1.0, 0.2.1.0, 0.2.2.0, 0.2.2.1, 0.2.2.1, 0.2.2.2
Change log CHANGELOG.md
Dependencies base (>=4.17.0.0 && <4.21), containers (>=0.6.4.1 && <0.8), fused-effects (>=1.1.1.2 && <1.2), ghc (>=9.4 && <9.11), transformers (>=0.5.6.2 && <0.7) [details]
License MIT
Author Jakob Brünker
Maintainer jakob.bruenker@gmail.com
Category Development
Home page https://github.com/JakobBruenker/monadic-bang
Bug tracker https://github.com/JakobBruenker/monadic-bang/issues
Source repo head: git clone https://github.com/JakobBruenker/monadic-bang.git
Uploaded by JakobBruenker at 2024-05-20T17:37:03Z

Modules

[Index] [Quick Jump]

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees


Readme for monadic-bang-0.2.2.1

[back to package description]

Monadic Bang

Run Tests

This is a GHC Parser plugin for GHC 9.4 and above, intended to make monadic code within do-blocks more concise and nicer to work with. Works with HLS.

This is heavily inspired by Idris's !-notation, but with some important differences.

Contents

  1. Motivating Examples
  2. Usage
  3. Cute Things
  4. Caveats
  5. Details
  6. Comparison with Idris's !-notation

Motivating Examples

Let's look at a few examples where Haskell syntax can be a bit annoying when it comes to monads - and what this plugin allows you to write instead:

When you use Reader or State, you will often have to use <- to bind fairly simple expressions:

launchMissile :: StateT Int IO ()
launchMissile = do
  count <- get
  liftIO . putStrLn $ "Missile no. " <> show count <> " has been launched"
  modify' (+ 1)
help :: Reader Config String
help = do
  manualLink <- asks (.links.manual)
  email <- asks (.contact.email)
  pure $
    "You can find help by going to " <> manualLink <>
    " or writing us at " <> email

With Monadic Bang, you can instead write

launchMissile :: StateT Int IO ()
launchMissile = do
  liftIO . putStrLn $ "Missile no. " <> show !get <> " has been launched"
  modify' (+ 1)
help :: Reader Config String
help = do
  pure $
    "You can find help by going to " <> (!ask).links.manual <>
    " or writing us at " <> (!ask).contact.email

With IORefs, STRefs, mutable arrays, and so on, you'll often have to write code that looks like this, having to use somewhat redundant variable names:

addIORefs :: IORef Int -> IORef Int -> IO Int
addIORefs aRef bRef = do
  a <- readIORef aRef
  b <- readIORef bRef
  pure $ a + b

With Monadic Bang, you can write

addIORefs :: IORef Int -> IORef Int -> IO Int
addIORefs a b = do pure $ !(readIORef a) + !(readIORef b)

Implicit parameter definitions have somewhat more limited syntax than regular definitions: You can't write something like ?foo <- action.
That lead me to have to write this in a Vulkan program:

initQueues = do
  let getQueue = getDeviceQueue ?device
  graphicsQueue <- getQueue ?graphicsQueueFamily 0
  presentQueue  <- getQueue ?presentQueueFamily  0
  computeQueue  <- getQueue ?computeQueueFamily  1
  let ?graphicsQueue = graphicsQueue
      ?presentQueue  = presentQueue
      ?computeQueue  = computeQueue
  pure Dict

with Monadic Bang, I can write

initQueues = do
  let getQueue = getDeviceQueue ?device
  let ?graphicsQueue = !(getQueue ?graphicsQueueFamily 0)
      ?presentQueue  = !(getQueue ?presentQueueFamily  0)
      ?computeQueue  = !(getQueue ?computeQueueFamily  1)
  pure Dict

Take this (slightly adapted) code used for the test suite of this very plugin:

settings :: MonadIO m => m Settings
settings = ... -- some long function body

initialDynFlags :: MonadIO m => m DynFlags
initialDynFlags = do
  settings' <- settings
  dflags <- defaultDynFlags settings' llvmConfig
  pure $ dflags{generalFlags = addCompileFlags $ generalFlags dflags}

With this plugin, I can instead write

settings :: MonadIO m => m Settings
settings = ... -- some long function body

initialDynFlags :: MonadIO m => m DynFlags
initialDynFlags = do
  dflags <- defaultDynFlags !settings llvmConfig
  pure $ dflags{generalFlags = addCompileFlags $ generalFlags dflags}

Or, to take some more code from this plugin's implementation

do logger <- getLogger
   liftIO $ logMsg logger MCInfo (UnhelpfulSpan UnhelpfulNoLocationInfo) m

Why have logger and getLogger when you can instead write

do liftIO $ logMsg !getLogger MCInfo (UnhelpfulSpan UnhelpfulNoLocationInfo) m

The pattern you might have noticed here is that this plugin is convenient whenever you have a do-block with a <- that doesn't do pattern matching, whose bound variable is only used once, and has a short right-hand side. While that might sound like a lot of qualifiers, it does occur fairly often in practice.

Usage

To use this plugin, you have to add monadic-bang to the build-depends stanza in your .cabal file. Then you can either add -fplugin=MonadicBang to the ghc-options stanza, or add

{-# OPTIONS_GHC -fplugin=MonadicBang #-}

to the top of the files you want to use it in.

This should also allow HLS to pick up on the plugin, as long as you use HLS 1.9.0.0 or above.

The plugin supports a couple of options, which you can provide via invocations of -fplugin-opt=MonadicBang:<option>. The options are:

Cute Things

Idiom Brackets Alternative

In some cases where idiom brackets would be ideal, ! can be a reasonable alternative. For example, compare these four options:

1. liftA2 (&&) (readIORef useMetric) (readIORef useCelsius)
2. (&&) <$> readIORef useMetric <*> readIORef useCelsius
   -- hypothetical idiom brackets:
3. [| readIORef useMetric && readIORef useCelsius |]
   -- Monadic Bang:
4. do pure (!(readIORef useMetric) && !(readIORef useCelsius))

while <$> and <*> are probably better here for prefix functions, ! plays nicer with infix operators.

If you have -XApplicativeDo enabled, this even works with Applicative instances.

Nested !

! can easily be nested. E.g. you could have

do putStrLn !(readFile (!getArgs !! 1))

For how this is desugared, see Desugaring.

Using -XQualifiedDo

! always has to be used inside a do-block, but it can be a qualified do-block. For example, if you use -XLinearTypes, you could write things like

{-# LANGUAGE QualifiedDo, BlockArguments, OverloadedStrings #-}
import Prelude.Linear
import Control.Functor.Linear as Linear
import System.IO.Resource.Linear

main :: IO ()
main = run Linear.do
  Linear.pure !(move Linear.<$> hClose !(hPutStrLn !(openFile "tmp" WriteMode) "foo"))

which would be desugared as

main :: IO ()
main = run Linear.do
  a <- openFile "tmp" WriteMode
  b <- hPutStrLn a "foo"
  c <- move Linear.<$> hClose b
  Linear.pure c

List comprehensions

List comprehensions are essentially just special do-blocks, so ! can be used here as well (as well as in monad comprehensions). Example:

[ x + ![1, 2, 3] | x <- [60, 70, ![800, 900]] ]

This would be equivalent to

[ x + b | a <- [800, 900], x <- [60, 70, a], b <- [1, 2, 3]]

The reason b <- ... is at the end here instead of the beginning is that everything that appears to the left of the | in a list comprehension is essentially treated like the last statement of a do-block (+ pure).

Get Rid of <-

In principle, every instance of pattern <- action in a do-block could be replaced by let pattern = !action. Should they? That's a separate question, though it could be a viable style.

The implicit parameter example in the first section is a valid use case of this.

Monadic Variants

Oftentimes, some generic function exists, but then it turns out that a monadic variant of said function would be useful as well. For example, hoogle finds at least a dozen different packages offering whenM. With this plugin, you can instead write

main = do
  when (null !getArgs) $ print usage
  ...

⚠️ NB: This works here since when only needs to evaluate its condition once. If you were to try to replace e.g. one of the forms of whileM in this manner, you would run into trouble since it's supposed to evaluate the condition again on each iteration.

Caveats

There are a few disadvantages to using this that are worth mentioning:

Details

While the above information should cover most use cases, there are some details that could sometimes be relevant

Desugaring

The desugaring is essentially what one would expect from comparing the motivating examples with the versions using !.

To illustrate with a fairly extensive example:

x = g do
  foo
  bar <- !a + !(!b ++ !c)
  baz <- case !d of
    (!f -> e) -> do !g e

is desugared into

x = g do
  foo
  <!a> <- a
  <!b> <- b
  <!c> <- c
  <!(!b ++ !c)> <- <!b> ++ <!c>
  bar <- <!a> + <!(!b ++ !c)>
  <!d> <- d
  <!f> <- f
  baz <- case <!d> of
    (<!f> -> e) -> do
      <!g> <- g
      <!g> e

where <!a> etc. are simply special variable names.

So, broadly speaking, the order in which things are bound is top-to-bottom (statement-wise), inside-out, and left-to-right.

This can be important when the order of effects matters - though as mentioned above, if order does matter, ! might not be the clearest way to express things.

! will only bubble up to the nearest do-block. To illustrate:

x = do when nuclearStrikeDetected $ log !launchMissiles

y = do when nuclearStrikeDetected $ do log !launchMissiles

x will launch the missiles regardless of whether or not a strike has been detected. But it will only log the results in the case of detection. y will only launch the missiles (and log the results) if a strike has been detected.

The desugaring:

x = do
  <!launchMissiles> <- launchMissiles
  when nuclearStrikeDetected $ log <!launchMissiles>

y = do
  when nuclearStrikeDetected $ do
    <!launchMissiles> <- launchMissiles
    log <!launchMissiles>

The story for case and if expressions is similar, ! in the individual branches will all be executed unless the branches have their own do-blocks.

Variable scope

A variable can be used inside a ! if

In other words, this is legal:

f x = do
  let a = a
  foo !(let b = b in x + a + b)

but this is not:

c = do
  let a = a in foo !a

That's because this would be desugared as

c = do
  <!a> <- a
  let a = a in foo <!a>

but a is not in scope in the second line.

Where it can be used

It can be used in any expression that is somewhere inside a do-block. In particular, this includes for example where-blocks in case-expressions:

main = do
  putStrLn case !getLine of
    "print args" -> prettyArgs "\n"
      where prettyArgs sep = intercalate sep !getArgs
    "greeting" -> "hello there!"

and view patterns

do (extract !getSettings -> contents) <- readArchive
   print contents

Comparison with Idris's !-notation

The main difference is that Idris will insert a do if there is none - e.g. this is legal in Idris:

f : IO ()
f = putStrLn !getLine

but (assuming it's at top-level) wouldn't be with this plugin; you would have to write f = do putStrLn !getLine instead.

Some other differences: