Is there a way to use generalized do blocks?

114 Views Asked by At

I want to create a Configuration Dictionary, so Haskell can be used similar to TOML. For that purpose I have created the structure "Dictionary"

Dictionary a
  = Section [(String, Dictionary a)]
  | Point a

key @ dictionary = Section [(key, dictionary)
key |= value = Section [(key, Point value)]

Now via semigroups I can merge two dictionaries to create a new one:

example_configuration =
  "resolution" @ "width" |= 1920
  <> "resolution" @ "height" |= 1080

But I think always having to combine via the semigroup operator is noisy.

Initially, I wanted to implement Monad for dictionaries, so I could use the do block. But monads naturally need to allow transforming old values. That is something I dont want. Is there a way to create a custom block notation (like do) for another operator instead of bind? Maybe like this:

example_configuration = combine
  "resolution"@"width" |= 1920
  "resolution"@"height" |= 1080
2

There are 2 best solutions below

0
leftaroundabout On

You can turn any monoid into a monad, as it were, by wrapping it in Writer, or just by adding a type argument that's used exactly once in the data structure:

data Dictionary a r
  = Section [(String, Dictionary a ())] r
  | Point a r

instance Monad (Dictionary a) where
  return = Section []
  Section ls q >>= f = Section ls () <> f q  --ish, some type matching is required
  

And then you can just use ordinary do syntax

example_configuration :: Dictionary Int ()
example_configuration = do
  "resolution"@"width" |= 1920
  "resolution"@"height" |= 1080

Is this a good idea? Debatable. It could certainly be argued that this is just abusing do syntax for something quite un-monadic. On the other hand, it can actually be quite convenient to abuse this syntax, in particular because it often allows omitting paretheses. In fact, I sometimes use do on values that aren't even monadic at all for this one reason - as long as there's only one statement in a do block, no monad operators are needed and the do simply acts as a paretheses that's auto-closed when indentation drops back to the previous level. Still abuse, but quite useful. An example where this is done is HaTeX, which e.g. I used for writing my entire PhD thesis in Haskell.

For configuration files however I would agree with chepner that it's not a good idea to use Haskell, or another Turing-complete language, but instead better something dedicated like Dhall.

2
Daniel Wagner On

You can use do notation with the tuple instance of Monad.

key @ dictionary = (Section [(key, fst dictionary)], ())
key |= value = (Section [(key, Point value)], ())

exampleConfiguration = fst $ do
  "resolution" @ "width" |= 1920
  "resolution" @ "height" |= 1080