Struggling in getting basic things like imports to work

Hi,

Just discovered Dhall the other week and got really exited, especially about the “One config to rule them all” part :smile:

Have tried reading up on the tutorial and getting started sections on the Dhall site but have no previous experience in functional programming so I suspect that is the major cause of my struggles.

I was planning to do a hackday project to get a boiler plate for defining the services and the global environment for the company I work for, but I get stuck on the local file imports and not finding the right answers to my questions in the documentation.

First question is if it is at all possible to import Dhall files that are normalized and which does not contain any functions or logic?

I started out with a file structure where all Dhall files are in the same directory and with one file called environments.dhall, one called services.dhall and then two files called service-1.dhall and service-2.dhall.

Optimally I would like the files defining our environment and each service to just contain normalized Dhall so they are easy for everyone to keep updated but still being able to import for example environments.dhall to service-1.dhall to substitute things like domain names, proxies and other stuff that are global to all services and which would be awesome to just have defined in one place.

In services.dhall my though was that it would only contain imports of all service-1, 2 etc files and get a few variables from each file to be able to list the names and endpoints of all existing services.

Not sure if I am targeting this problem the completely wrong way :slight_smile: but any help would be appreciated.

Cheers,
//Jon

Hi Jonten, welcome!

If I understand correctly the use-case, you would like to have:

  • environments.dhall containing { domain = "example.com" }
  • service-1.dhall containing { url = "http://example.com/service1" }

And then you would like to be able to update the domain for all the services.

To answer your first question, I think dhall imports are always normalized, e.g. importing a file with let x = 42 in x will just be 42.

It seems like what you can do is:

  • create a service-1-source.dhall file with this content let env = ./environments.dhall in { url = "http://${env.domain}/service1" }.
  • Then you can render the normalized service-1.dhall using dhall --file service-1-source.dhall --output service-1.dhall ,
  • and can check the difference using dhall diff ./service-1-source.dhall ./service-1.dhall.

Hi Tristan,

Thanks a lot for your answer :slightly_smiling_face:
I will try that out later tonight.

Hi again Tristan,

Sorry for the really late reply! I tried your suggestion and it worked great :smiley:
Unfortunately what I really would like to do is a bit more complicated. Say for example that I have four domains, two sections for our on-prem environments and two in AWS EKS. Some services only exists on-prem and others in EKS. Do you know if it Is possible to specify two of the domains for the service-1.dhall file and somehow get those from the nested environments.dhall file?

environments.dhall example:

[ { environment =
    { context = "example-dev"
    , domain = "dev.example.com"
    , region = "on-prem"
    , subnet = "10.0.1.0/24"
    }
  }
, { environment =
    { context = "example-prod"
    , domain = "prod.example.com"
    , region = "on-prem"
    , subnet = "10.0.2.0/24"
    }
  }
, { environment =
    { context = "example-eks-dev"
    , domain = "dev.aws.example.com"
    , region = "eu-west-1"
    , subnet = "10.0.3.0/24"
    }
  }
, { environment =
    { context = "example-eks-prod"
    , domain = "prod.aws.example.com"
    , region = "eu-west-1"
    , subnet = "10.0.4.0/24"
    }
  }
]

service-1.dhall example (that works when having only one level but not with nested levels in environments.dhall):

let env = ./environments.dhall
in
[ { name = "service-1"
  , port = 2323
  , endpoint = "service-1.${env.domain}"
  , subnet = "${env.subnet}"
  , region = "${env.region}"
  }
]

When using nested levels in environments.dhall I only get the following error:
Error: Not a record or a union
I have tried using the “let env” variable in a function or a union but somehow I cannot seem to get it right, so that I am able to fetch the nested values :confused:

The error indicates that the dot in env.domain only works for record or union. Your env value is a list, thus you need to use one of the Prelude.List functions to access its elements, for example List/head

Thanks, I will try that

Also, if you want to easily access the different environments, I’d recommend storing them in a record instead of a list, like this:

-- ./environments.dhall
{ example-dev =
  { domain = "dev.example.com"
  , region = "on-prem"
  , subnet = "10.0.1.0/24"
  }
, example-prod =
  { domain = "prod.example.com"
  , region = "on-prem"
  , subnet = "10.0.2.0/24"
  }
, example-eks-dev =
  { domain = "dev.aws.example.com"
  , region = "eu-west-1"
  , subnet = "10.0.3.0/24"
  }
, example-eks-prod =
  { domain = "prod.aws.example.com"
  , region = "eu-west-1"
  , subnet = "10.0.4.0/24"
  }
}

Then you can use it like this:

let env = (./environments.dhall).example-eks-prod
in
[ { name = "service-1"
  , port = 2323
  , endpoint = "service-1.${env.domain}"
  , subnet = "${env.subnet}"
  , region = "${env.region}"
  }
]

Also, I want to note a few small things:

First, "${x}" is the same thing as x, so you can simplify your example to:

let env = (./environments.dhall).example-eks-prod
in
[ { name = "service-1"
  , port = 2323
  , endpoint = "service-1.${env.domain}"
  , subnet = env.subnet
  , region = env.region
  }
]

… and you can simplify that even further to:

let env = (./environments.dhall).example-eks-prod
in
[ { name = "service-1"
  , port = 2323
  , endpoint = "service-1.${env.domain}"
  } // env.{subnet, region}
]

Also, regarding your original question: Dhall atomatically normalizes imports for you, but that doesn’t actually change the result in any way. In other words, even if Dhall didn’t pre-normalize imports the result would be the same. This is because Dhall is confluent, meaning that the order in which things are normalized/interpreted does not change the result.

Hi Gabriel,

Thanks a lot for helping out and also for taking time explaining how things work.
This solution worked like a charm :smiley: but I have a few additional questions if you have the time?

Is it possible to somehow pass the .example-eks-prod and .example-eks-dev dynamically to let env = (./environments.dhall) so I can use the same service-1.dhall file for several environments?

I am also curious to know if you think this structure/setup with environments/service files is a good fit if I would want to use it as a central service configuration generator at work, or would you split things up in more specific dhall files as for example regions.dhall, domains.dhall etc? Are there any pros/cons with having to much configuration in one file vs splitting things up in many smaller files?

Cheers,
//Jon

(If I understand what you’re asking correctly), just makes service-1.dhall a function that takes the environment as an argument, e.g.

-- service-1.dhall

let Environment = { domain : Text, region : Text, subnet : Text }

in  λ(env : Environment) →
      [ { name = "service-1"
        , port = 2323
        , endpoint = "service-1.${env.domain}"
        , subnet = "${env.subnet}"
        , region = "${env.region}"
        }
      ]

You can then call the file as a function with whatever environment you like, e.g.

dhall <<< './service-1.dhall (./environments.dhall).example-eks-prod'

outputs

[ { endpoint = "service-1.prod.aws.example.com"
  , name = "service-1"
  , port = 2323
  , region = "eu-west-1"
  , subnet = "10.0.4.0/24"
  }
]

If you have multiple services, you can break out the type (Environment = { domain : Text, region : Text, subnet : Text }) into it’s own file so you don’t have to have it in every place, e.g.

-- EnvironmentType.dhall

{ domain : Text, region : Text, subnet : Text }
-- service-1.dhall

let EnvironmentType = ./EnvironmentType.dhall

in  λ(env : EnvironmentType) →
      [ { name = "service-1"
        , port = 2323
        , endpoint = "service-1.${env.domain}"
        , subnet = "${env.subnet}"
        , region = "${env.region}"
        }
      ]
-- service-2.dhall

let EnvironmentType = ./EnvironmentType.dhall

in  λ(env : EnvironmentType) →
      [ { name = "service-2"
        , port = 117
        , endpoint = "service-2.${env.domain}"
        , subnet = "${env.subnet}"
        , region = "${env.region}"
        }
      ]

Thank you very much! This was exactly what I was looking for