dhall-lang / dhall-haskell

Maintainable configuration files
https://dhall-lang.org/
BSD 3-Clause "New" or "Revised" License
915 stars 213 forks source link

Pretty Print Builtin #1611

Open brendanhay opened 4 years ago

brendanhay commented 4 years ago

Note: I added this to dhall-haskell although possibly? it belongs under dhall-lang or dhall-kuberentes ¯\_(ツ)_/¯

The following usecase and example outlines how I'd like to pretty-print fully normalized expressions as Text literals within Dhall. This is desirable because I'd like to configure Haskell services using Dhall - but these configurations must be passed as Text to dhall-kubernetes ConfigMap type. To workaround this currently, multiple invocations of dhall must be run, which is error prone.

Usecase

Below is an expression which results in a list of dhall-kubernetes types corresponding to Kubernetes resources, such as a Haskell Deployments and the respective Dhall configuration for the Haskell application which is stored in a ConfigMap:

-- Config.dhall
{ name : Text, role : Text, domain : Text }
-- my-application-config.dhall
let Config = ./Config in { name = "foo", role = "bar", domain = "foo.bar.com" } : Config
-- kuberenetes-config.dhall
let k8s = ./k8s/package.dhall -- from dhall-kubernetes

{-
This could be whatever is considered valid configuration for the deployed application.

Assume that it is a large or nested expression that is tedious to write manual 
pretty-printers for everytime we come up with a new configuration type, where
the printer is of type : Config -> Text

Alternatively it could be a partially applied function, so the printer would need
to be of type : (Foo -> Config) -> Text
-}
let Config = ./Config.dhall

let manifests : Config -> List k8s.typeUnion
    = \(config : Config) ->
    {-
    The configMap should contain the fully normalized dhall configuration,
    as this file alone is copied to the host independent of any imports, etc.
    -}
    let configMap =
            k8s.ConfigMap::{
            , data = toMap { `my-application-config` = "${Expr/print config}"
            }

    {-
    The pod containers in the deployment mount configMap above as a file
    and the applications reads the file contents when the container starts.
    -}
    let deployment =
            k8s.Deployment::{
            ..
            }

    in [ k8s.typeUnion.ConfigMap configMap
       , k8s.typeUnion.Deployment deployment
       ]

in manifests

Reality

The current workaround to make the above example work would be to replace the use of Config with Text in kubernetes-config.dhall, removing any semblance of typing:

dhall <<< ./my-application-config.dhall > my-application-config.normalized
dhall-to-yaml <<< ./kubernetes-config.dhall (./my-application-config.normalized as Text) | \
    kubectl apply -f -

And hope that you didn't stuff it up and accidentally deploy the non-normalized configuration which still has relative imports, etc. otherwise your container will explode when it tries to read the configuration file.

Pretty Printing Builtin

Regarding a hypothetical Expr/print builtin. At a very cursory glance it seems possible to write a normalizer for this:

let normalizer (App (Var "Expr/print") expr) = TextLit [] (pretty expr)

Whether or not the above is a good idea or not is moot because we cannot write a type-checker, as we've no idea what the expected type might be.

brendanhay commented 4 years ago

In case it wasn't clear this is more of a 'how do I do this?' question, rather than a 'I think this is good/necessary' suggestion.

As one counterargument - Expr/print gives rise to ad-hoc overloading of other */show builtins by way of Expr -> Text. print or pretty could be considered sufficiently different in intent, maybe.

Gabriella439 commented 4 years ago

@brendanhay: Probably the most direct route would be to build a custom Dhall interpreter to supply your own built-ins. For more details on how to do that, see:

https://docs.dhall-lang.org/howtos/How-to-add-a-new-built-in-function.html

You also might be interested in https://github.com/dhall-lang/dhall-lang/issues/800 which is a related proposal to add a standard way to declare custom built-ins (so that standard tools like the dhall or dhall-lsp-server can still work with code that depends on custom built-ins).

Another possible route would be to upstream related functionality directly into the language standard. The main issue I can think of with that is that if there were a new built-in for rendering Dhall expressions, it would most likely need to render them as binary (i.e. the standard CBOR encoding) rather than Text. The reason why is that there is no standard way to render/display Dhall expressions as Text (even unformatted expressions). If there were a built-in to encode to binary that would entail an additional Binary type and ways to convert that Binary type to Text (such as base64 encoding).

brendanhay commented 4 years ago

@Gabriel439 Thanks. I've previously added builtins for other purposes but it just ends up being untenable due to the reimplementation of not just the entire dhall binary, but dhall-to-yaml/json as well - which have a bunch of their internals un-exported, etc.

Regarding the standard + rendering as binary all makes sense, although I'm hesitant to push for this unless someone else steps forward and has a similar need for using Dhall to render Dhall sub-configurations.

brendanhay commented 4 years ago

As an example of groundwork needed to realistically implement such a builtin I've thrown together #1613