Data Abstraction in Dhall

System-F1, the core theory that underlies the idealized data abstraction that I described in Data Abstraction: Theory and Practice, also underlies Dhall, a programmable configuration language. Thus, it's pretty straightforward to translate it into executable Dhall code. This post serves three purposes: (1) Dhall coders can use techniques shown here to tame complexity through data abstraction. (2) Scholars seeking to understand idealized data abstraction can use this executable description as the further exposition of the concepts introduced in the first post. (3) I, Brandon, can use this prose to express my excitement over some very cool code.

Background

"Dhall is a programmable configuration language that you can think of as: JSON + functions + types + imports"2. For the functional programming enthusiasts, you can think of it as literally "System-Fω3 with records" and a little syntactic sugar on top. In other words, it's a well-designed programming language to replace the mess that is templated YAML or JSON. Dhall compiles into YAML or JSON (and Nix and Bash and a few other things) in the same way are existing template expanders compile templated YAML into YAML.

Since idealized data abstraction using the "Existential Types As Rank2 Universal Types" interpretation as described in my last post is implementable in pure System-F, I realized it would be pretty easy to translate it to Dhall.

Translation

I created a GitHub repo, abstraction, that houses a collection of the code shown below. It builds and runs with Dhall v1.30.0.

Types

Before we begin to describe any particular interfaces, clients, and implementations, let's consider the type of these pieces.

Let's make a file Existential.dhall.

Given some specific "interface" τ\tau:

\(tau : Type -> Type) -> -- ...

Clients consume arbitrary implementations of our interface t.τt. \tau and return some other arbitrary result uu specific to our choice of client. Dhall has no type inference so we are explicit with our type-lambda for uu.

let Client
: Type -> Type
= \(u : Type) -> forall(t : Type) -> tau t -> u

We wish to describe t.τ\exists t. \tau, the type of erased packed implementations. In the last data abstraction post, we discussed that in the Rank2 interpretation existentials are the type of a machine that takes a client and runs it on the concrete tt to produce our arbitrary uu value.

let ExistsTau
: Type
= forall(u : Type) -> Client u -> u

Finally, we can return these types in a record

in { Impl = ExistsTau, Client = Client }

This expression's type captures the input t.τt.\tau and this record.

: (Type -> Type) -> { Impl : Type, Client : Type -> Type }

These types are reusable in any place we wish to use data abstraction in Dhall programs.

Stack

We'll describe and implement the same example as we discussed in the blog post.

In a file Stack.dhall:

-- An infinite stack interface and implementation

First let's import the Prelude, Dhall's standard library.

let Prelude =
https://prelude.dhall-lang.org/v15.0.0/package.dhall sha256:6b90326dc39ab738d7ed87b970ba675c496bed0194071b332840a87261649dcd

And the file we made above.

let Existential = ./Existential.dhall

Now we can describe the infinite stack interface.

let Interface
: Type -> Type
= \(t : Type)
-> { create : Natural -> t
, push : Natural -> t -> t
, pop : t -> { x : Natural, rest : t }
}

Feeding this interface into Existential gives us back the type for packed implementations and clients

let e = Existential Interface
let Impl = e.Impl
let Client = e.Client

Let's choose a simple implementation. A default if the stack would otherwise be empty and a list xs to represent the stack when it's not empty.

let Stack = { default : Natural, xs : List Natural }

Now, we write the implementation of our interface using this concrete implementation. It is the engine that feeds a client the concrete stack implementation.

let StackImpl
: Impl
= \(u : Type)
-> \(x : Client u)
-> x
Stack
{ create = \(i : Natural) -> { default = i, xs = [] : List Natural }
, push = \(i : Natural) -> \(t : Stack) -> t // { xs = [ i ] # t.xs }
, pop =
\(t : Stack)
-> { x =
Prelude.Optional.fold
Natural
(Prelude.List.head Natural t.xs)
Natural
(\(x : Natural) -> x)
t.default
, rest =
{ default = t.default
, xs = Prelude.List.drop 1 Natural t.xs
}
}
}

Finally, we return the interface, our concrete implementation, and the client type.

in { Interface = Interface, Impl = StackImpl, Client = Client }
: { Interface : Type -> Type, Impl : Impl, Client : Type -> Type }

The interface can be reused to build other implementations and the client type can be used for any consumers of this implementation or any others.

Client

We first import our interface, implementation, and client type.

let Stack = ./Stack.dhall

Clients can't depend on any particular implementation of Stack. They act on the interface. The following client creates a stack and pushes a few things then pops once and returns the value.

let stackClient
: Stack.Client Natural
= \(t : Type)
-> \(stack : Stack.Interface t)
-> let s = stack.create 10
let s = stack.push 1 s
let s = stack.push 2 s
let y = stack.pop s
in y.x

We can run our client by feeding it to some particular implementation

in Stack.Impl Natural stackClient

When we execute Client.dhall we get 2 as expected.

$ dhall <<< './Client.dhall'
2

That's Not All

Implementations as values, upcasting, extension, and interface composition are all directly expressible by translating almost literally character-by-character from their latex System-F representation to Dhall. I cover each of these capabilities in the last post.

Conclusion

Since System-F powers both idealized data abstraction and Dhall, we can describe these primitives directly in Dhall. Regardless of whether you've used this post to learn a new capability of Dhall, to further study information hiding and encapsulation, or to indulge my insatiable need to share in that which I am interested, feel free to send me a tweet with any feedback, corrections, or comments to @bkase_.


  1. System-F is a typed lambda calculus with scoped universal quantification. In other words, we can mix \foralls with our λ\lambdas.
  2. Quote from the Dhall website
  3. System-Fω adds dependent typing to System-F.