
Dhall: a programmable configuration language that is not Turing-complete - happy-go-lucky
https://github.com/dhall-lang/dhall-lang/blob/master/README.md
======
sly010
Every configuration documentation is full of wagely defined or undefined
combination of options and "this-flag-cannot-be-used-together-with-that-flag"
clauses. Often these things are not even documented but are implicit.

e.g. in systemd service definitions depending on your service "Type" the
meaning and the validity of the other properties change. (e.g. Type=oneshot
cannot have ExecStart)

Statically typed languages allow you to create very elegant configuration DSLs
so the user cannot create invalid programs (where invalid can mean whatever
the DSL creator wants). But most software is written to read shitty (untyped)
configuration languages.

I always wished config files were not a soup of magic keywords but typed
datastructures (talking to you, every yaml configuration file ever). It looks
like Dhall is just a statically typed and functional language that allows you
to create a nice typed DSLs to generate always valid config files. It's at
least worth a closer look.

~~~
touisteur
Talking about statically-typed languages and configuration paramètres,
something coming from the Ada/SparkAda world is the use of type predicates and
invariants, in addition to expressive static typing.

Here's an interesting paper on the subject : [http://acg-solutions.fr/acg/wp-
content/uploads/2017/04/PDI-5...](http://acg-solutions.fr/acg/wp-
content/uploads/2017/04/PDI-5.pdf)

And here's some demo code :
[https://github.com/AdaCore/SPARK_PDI_Demo/blob/master/README...](https://github.com/AdaCore/SPARK_PDI_Demo/blob/master/README.md)

The other interesting part is that you can then prove (or use static analysis
to get à high level of confidence) your application will never be run with
incoherent parameters. Another nice thing is that with the help of the prover
you can let the internal constraints of your implementation surface up to the
application parameters. Sa y you didn't handle some corner case, you can
either implement it, or use a predicate to ensure this case is never run. Your
pre-conditions and the type invariants/predicates of your parameters are part
of your interface.

------
man-and-laptop
Subtraction:

    
    
       let pred = λ(n : Natural) → (Natural/fold n { prev : Natural, next : Natural } (λ(p : { prev : Natural, next : Natural }) → { prev : p.next, next : p.next + +1}) { prev : 0, next : 0 }).prev 
       in λ(x : Natural) → λ(y : Natural) → Natural/fold y Natural pred x
    

The Ackermann function:

    
    
       let iter = λ(f : Natural → Natural) → λ(n : Natural) → Natural/fold n Natural f (f (1)) 
       in λ(m : Natural) → Natural/fold m (Natural → Natural) iter (+ +1)
    

Save the above to 'ack' and run the following: $ dhall <<< './ack 10 10' It's
going to take a while before it finishes.

This language is close to Turing complete.

~~~
ameliaquining
Wait, so you can write programs in this thing that aren't primitive recursive?
That's quite interesting; requiring primitive recursiveness is the usual way
to guarantee termination. What class of programs does this language allow you
to write? (It of course can't allow all programs that terminate, unless it
also allows some that don't, which it claims not to.)

~~~
evincarofautumn
The type system appears to be System Fω[1], the “higher-order polymorphic
lambda calculus”. I’m not really familiar with what class of programs it
accepts, other than “more than simply typed lambda calculus”, but typechecking
is decidable and evaluation is strongly normalising (i.e. both always
terminate).

[1]:
[https://en.wikipedia.org/wiki/System_F#System_F.CF.89](https://en.wikipedia.org/wiki/System_F#System_F.CF.89)

~~~
chombier
Can somebody comment on what is missing for System F(ω) to be Turing complete?

I recall that it can typecheck self-application, so is it not possible to
define and use the fixed-point combinator?

~~~
evincarofautumn
System F can check self-application with an explicit type annotation,
something like λx:(∀a. a → a). x [∀a. a → a] x, but Fω can’t.

~~~
chombier
I thought Fω was an extension of System F with type operators, so I figured if
System F can check self-application, System Fω can too.

Am I missing something else?

~~~
evincarofautumn
Sorry, that comment was worded badly. Both System F and System Fω can express
self-application _at a particular type_ , but they can’t give a _general_ type
for self-application, e.g., Ω = ω ω where ω = λx.xx.

~~~
chombier
I see, thanks for the precision!

------
JackC
Maybe I'm just not the target audience, but I found this intriguing yet
confusing. It might benefit from an explanation of typical use cases -- what
do people do now that Dhall would be a better tool for?

I read the tagline: "Dhall is a programmable configuration language that is
not Turing-complete," so I figured "programmable configuration language" was a
term I just wasn't familiar with, and it might bring up some typical use cases
if I searched for it -- but it seems like that term was invented for Dhall.

So maybe the kind of example I'm looking for is: if you're now using a non-
programmable configuration language, here's what that would look like, and
here's the improvement if you switch to Dhall. Or if you're now using a
programmable configuration language that is Turing complete, here's what that
would look like, and here's the improvement if you switch to Dhall.

(A more specific question: in what kind of scenarios is JSON+arbitrary
terminating functions useful?)

~~~
KirinDave
I use Dhall extensively now. In fact, all my typescript programs for my
current employer use JSON configuration that Dhall generates (and I've tried
so many times to get this to frontpage on YC, I guess I'm just not good at
timing).

Dhall's draw as a configuration language is that it allows you to express
logic in your configuration and even call out to remote services and file
handles _safely_. People have a gut negative reaction to this phrase even as
they essentially encode non-trivial logic directly into their Chef, CircleCI,
Docker-Compose and Kubernetes configs, so it's hardly a new idea.

Dhall lets you write logic into your configs that otherwise would be implicit.
As an example, you might define a new configuration environment with Dhall and
use the types to make sure you NEVER provide production credentials, or even
shared credentials.

Dhall also allows you to safely call out to web services (with authentication
and SSL, of course) to fill in holes or even provide non-trivial logic (it's
perfectly valid to move functions around in dhall over the wire). I use this
feature to map a tiny microservice that resolves Hashicorp Vault calls to AWS
credentials just-in-time as I ship, and I feel very confident that it's safe
because I can encode the sorts of permissions and their resolution directly
into the types I'm handling.

You could start using Dhall instead of JSON today for configs, and the main
advantage is that you can assert values have specific types (and also provide
sane loops over list fields).

Dhall is actually a pretty amazing invention and I keep trying to work up the
guts and time to write a typescript interpreter for it.

~~~
kobeya
You've also failed to link to it here.

------
azeirah
I understand that Turing completeness is not always a desirable quality for
your programming language of choice, but since it is a rather obscure thing
for those of us who haven't studied CS, when is a non-turing-complete language
a favorable choice compared to a more traditional turing complete one?

~~~
Gabriel439
Here's a practical example of why it's beneficial to not be Turing complete.
In Dhall, you can normalize programs, even if they are functions. For example,
the interpreter can automatically simplify this Dhall function:

    
    
            let replicate = https://ipfs.io/ipfs/QmQ8w5PLcsNz56dMvRtq54vbuPe9cNnCCUXAQp6xLc6Ccx/Prelude/List/replicate
    
        in  let exclaim = λ(t : Text) → t ++ "!"
    
        in  λ(x : Text) → replicate +3 Text (exclaim x)
    

... to this one:

    
    
        λ(x : Text) → [x ++ "!", x ++ "!", x ++ "!"]
    

... even though we haven't applied the function to any arguments yet. You
can't perform this sort of simplification (in general) if the language is
Turing-complete

This sort of automatic simplification comes in handy a lot when authoring
configuration files. For example, if somebody objects to the import of the
remote `replicate` function, you can just simplify the file and (voila!) all
the imports are gone because they've all been inlined and reduced. Similarly,
if somebody objects to excessive use of abstraction and functions you can
similarly simplify them to remove all indirection

~~~
skybrian
This is inlining a function and unrolling a loop. It's done in many languages,
no?

More generally, many automatic refactorings are not entirely safe but we do
them anyway, assuming that the code will terminate (since intentional infinite
loops are rare) and relying on tests and human reviewers to check for
mistakes.

Depending on the program, substituting equals for equals might have to be
rolled back for performance reasons; computing a mathematically equal value is
no guarantee.

~~~
Gabriel439
In a Turing complete language you can't safely inline all functions to
completion without risking an infinite loop. In such a language there is no
decidable way to know when to stop inlining things

~~~
skybrian
It doesn't matter in practice, because we don't need to inline every function.

~~~
Gabriel439
The key word in "automatic simplification" is "automatic". The feature loses
value if a human has to intervene to specify which functions to inline or to
continue inlining. Imagine how worthless `go fmt` would be if it prompted the
user to confirm every change to the source code

~~~
qznc
Compilers like LLVM and GCC use heuristics not human intervention. For
inlining a common heuristic is the size of the function. So we inline (even
recursive functions) until the function becomes larger than a certain
threshold. The threshold can be specified by a human (-finline-limit), but I
believe that is rarely done.

~~~
chriswarbo
This isn't compiling though; it's normalising. A heuristic like function size
is useful when optimising a Turing-complete language since (a) we have to rely
on _some_ heuristic and (b) smaller binaries are generally more efficient, all
else being equal, so we should avoid a size blow up regardless of what we're
optimising for (speed, size, memory, etc.).

In the case of normalising a non-Turing-complete language, we (a) don't need
any heuristics, beta-reduction is a complete and correct strategy and (b)
things like the size of a function are useless at telling us whether we've
reached a normal form. In fact, I would imagine that normal forms of real
Dhall programs are generally _much bigger_ than the programs themselves, since
one of the main reasons to use a language like Dhall is to reduce repetition.
Also, your heuristic is heavily dependent on the evaluation order: if we have
a program like this:

    
    
        (\x -> (\y -> x)) small-thing (duplicate 1000000 big-thing)
    

Then an evaluation strategy like call-by-name will never look at big-thing,
since it evaluates the functions first and they discard it:

    
    
        (\x -> (\y -> x)) small-thing (duplicate 1000000 big-thing)
    
        (\y -> small-thing) (duplicate 1000000 big-thing)
    
        small-thing
    
        small-value
    

On the other hand, an evaluation strategy like call-by-value _will_ evaluate
big-thing, resulting in some arbitrarily large value (which may cause your
heuristic to halt); then it will create 1000000 duplicates of that value
(again, causing a size-based heuristic to halt); then finally it will evaluate
the functions and discard the big, duplicate expression:

    
    
        (\x -> (\y -> x)) small-thing (duplicate 1000000 big-thing)
    
        (\x -> (\y -> x)) small-value (duplicate 1000000 big-thing)
    
        (\x -> (\y -> x)) small-value (duplicate 1000000 big-value)
    
        (\x -> (\y -> x)) small-value [big-value, big-value, ...]
    
        (\y -> small-value) [big-value, big-value, ...]
    
        small-value

------
qznc
Interesting. I’m pondering non-turing-complete languages myself. I believe
they are under-estimated and should be used more often for anything that looks
like configuration.

Configurations usually start simple, like we only need “x = y” statements. Oh
wait, sections would be nice, so use the ini-format. Oh wait, nesting stuff
would be nice, so use JSON/XML/Lisp. Oh wait, we want to reduce duplicated
stuff, so use a preprocessor. Oh wait, more abstractions would be nice, so use
some scripting language already. Stop, we just skipped one level before
scripting languages, the non-turing-complete languages!

Concerning Dhall, I never met the “Oh wait, we want to annotate types” idea.
Is that really desireable for configurations?

------
jopsen
We recently did a similar thing, phrased as json-expressions, ie. a json
structure that evals to another...

[https://taskcluster.github.io/json-e/](https://taskcluster.github.io/json-e/)

------
Karrot_Kream
I see you allude to Terraform, but will you ever write a dhall specific
compiler for Terraform configs?

~~~
Gabriel439
Yes, however standardizing the language is higher priority at the moment

------
thesmallestcat
What they're attempting here is interesting, but for configuration, I like a
well-known real language, or a well-known, simple configuration language (ini,
shell/env vars, JSON...). This to me is an awkward middle ground.

~~~
_ibu9
The whole point is if this (or something similar) was well known then you
wouldn't have to use shitty typeless stuff like JSON...

~~~
skybrian
It's not typeless if you validate it using a schema. (For example, there are
JSON encodings of protobufs, where validation will be done when the file is
read.)

------
kronos29296
Totally off topic but Liked the name and the reference to Planescape Torment.
(My favourite RPG)

~~~
icen
The author has a bunch of similarly named projects (Morte, Annah).

