[ANN] kdl-hs: Parser for KDL configuration files

KDL is a nifty configuration language I recently found, and I think it’d be cool to start seeing more uses of it. There was a Haskell library to parse it, but that library didn’t have an easy API to decode the AST into a custom type. So I vendored that library and exposed a decoding API, with plans to rewrite the parser eventually to make it v2-compliant + preserve formatting information.

The thing I’m most excited about with kdl-hs is that it enables decoding with both Arrows notation and Monad do-notation. If you use Arrows notation, you can statically get the full schema of the Decoder without running anything. You get this feature with ApplicativeDo as well, but with Arrows, you can decode values based on previously decoded values, which you can’t do with Applicative alone.

10 Likes

I was just looking for a library like this a week ago! KDL is awesome, and I throughly look forward to full v2 support

1 Like

Amazing, thank you very much @brandonchinn178. I was just thinking that I’d love to use that configuration format for my next application.

1 Like

Finally. Now, can we switch from cabal and hpack to this? :folded_hands:

1 Like

Is this the most pressing thing our ecosystem needs? :stuck_out_tongue:

While I think it’d be cool, I personally would rather see TOML. With Rust and Python using TOML, I see TOML as being an easy way to signal to people outside the Haskell world that Haskell is/could be a mainstream language. KDL would still be better than the Haskell-specific Cabal format though.

I’ve heard a lot of complaints about both cabal (alien syntax) and hpack (ugly yaml programming). So, maybe it is one of them?

1 Like

I doubt that the self-inflicted pain of TOML would buy us that much signalling to get Haskell into the “mainstream” league. I’d rather have something expressive, clean (like KDL!), and shared with the others (thus having editor support etc.)

Sorry for the ranting, I just want to see nice language proliferating and unergonomic parser-pleasers to go dodo.

3 Likes

The main complain about Cabal’s syntax stems from the fact that you have to spend more time editing it than with other software project systems.

If kdl-hs had a comment-preserving parser that would be a point in its favour, otherwise we’re back to step one with a syntax that is not even 10 years old and for which there is very little knowledge in our community.

1 Like

Can you give an example of this? I’ve seen this claim in other places too, but haven’t seen a concrete motivating example of it yet.

Say I have a config with a list of rules, but some rule types have extra arguments

rules {
  - a
  - b {
    foo 123 # only allowed in b, not a
  }
}

You can do this with arrows notation, desugaring to ArrowChoice:

KDL.dashNodesWith "rules" $ proc () -> do
  name <- KDL.arg -< ()
  case name of
    "a" -> returnA -< RuleA
    "b" -> do
      foo <- KDL.children $ KDL.argAt "foo" -< ()
      returnA -< RuleB foo
    _ -> KDL.fail -< "Invalid rule: " <> name

If you did this with Monad do-notation, you lose the schema. With ArrowChoice, it can track the schema as an alternative, roughly equivalent to:

NodeNamed "rule" $
  SchemaAnd
    [ SchemaOne $ NodeArg TextSchema
    , SchemaOr
        [ SchemaAnd []
        , SchemaOne . NodeNamed "foo" $
            SchemaOne $ NodeArg NumberSchema
        ]
    ]

Applicative can’t do this at all, since it would need to do different effectful actions based on the result of a previous action.

1 Like

AST appears to carry the comments and should be formattable :thinking:
If that’s indeed the case, kudos to @brandonchinn178 for the forethought!
Otherwise, that’s a valid and important feature request.

Yes, the AST is future-facing, which I adapted from kdl-rs. But the parser doesn’t actually populate these correctly right now (I wanted to get a MVP out the door first). It seems like there’s excitement here, though, so maybe I’ll take a stab at it!

1 Like

I realise I’m kind of moving the goalposts, but you can do this if you add custom functions, like this:

KDL.dashNodesWith "rules" $
  switch KDL.arg $ do
    case "a" $ pure RuleA
    case "b" $ RuleB <$> KDL.children (KDL.argAt "foo")
    casefail ("Invalid rule: " <>)

In fact, I think you could implement this using selective functors (maybe something like this already exists?)

1 Like

Yeah, could probably make that particular pattern work. But still wouldnt have the full power of pattern matching, or pattern match on two KDL.args at the same time (case (arg1, arg2) of ...).

But you could use the Arrow combinators explicitly, it would just be tedious without the desugaring:

KDL.dashNodesWith "rules" $
  (toEither <$> KDL.arg) >>> fromEither
where
  toEither = \case
    "a" -> Left ()
    "b" -> Right $ Left $ ()
    name -> Right $ Right $ "Invalid rule: " <> name
  fromEither =
    -- "a"
    pure RuleA |||
    -- "b"
    (RuleB <$> KDL.children (KDL.argAt "foo")) |||
    -- else
    KDL.Arrow.fail

EDIT: Released kdl-hs-0.2.1, which adds KDL.Applicative and documents this

3 Likes