Why a new language was created rather than a Haskell library?
While I can’t give you the original reasoning, I can give you my own, somewhat formalist, answer to the question, “why is Daml a language instead of a library or EDSL?”
In short: Haskell is too big, and Daml does not fit the intended use cases of lifted embeddings, which would have to nearly-but-not-quite reproduce Haskell.
Daml’s capabilities are defined by the interplay between the compiler and the runtime, which are mediated by Daml-LF. You can perfectly understand those capabilities by understanding Daml-LF.
Every capability of Daml-LF is an advantage for Daml. Every restriction is also an advantage, in a different way. That you can type-check or statically analyze the code that actually runs on a ledger, or deterministically validate that a record of choice exercise is an honest record, these are due to restrictions, carefully chosen.
If to exercise a choice is to evaluate Haskell, by definition, the evaluation environment must be at least as powerful as Haskell’s. Insofar as it is not so powerful, it is not really Haskell anymore. (Surprise surprise, therein lies another way to describe what Daml is.)
unsafePerformIO, for example. GHC doesn’t really offer a stable meaning of this function, but it is stable enough that significant libraries use it. But its existence has big implications for the evaluation environment: it means that instead of merely producing IO instructions, a program may—somewhat nondeterministically and unreliably—invert control of IO, taking it from the containing runtime for itself. If the equivalent existed in Daml,
unsafePerformUpdate, could we honestly say “you can validate your choice exercises just by reinterpreting the Daml later on”? Absolutely not.
The environment in which Haskell is executed has naturally led to an inflationary view of what Haskell should be permitted to do in that environment; it expands to fill the space it exists in. We don’t want the evaluation environment of choice exercise to reproduce that space.
The above is the case against treating Daml types and functions exactly like Haskell types and functions. But what if executing a Daml-in-Haskell program had nothing to do with what happens when you exercise a choice, but instead executing the program only output descriptions of what happens when you exercise a choice? For example, you could imagine working with Daml in this environment being
- compile Haskell program
- run Haskell program once; this produces Daml-LF, or maybe an error
- we use this Daml-LF just like we do now.
You can use type constructors in the host language—Haskell—to define such an EDSL. So instead of
x: Int, you have
x: Daml Int, and also
(+) : DamlNum a => Daml a -> Daml a -> Daml a. And so on for every function you imagine; running the Haskell that calls this function doesn’t add anything, it produces a Daml-LF instruction to add the expressions represented by the arguments.
This is a reasonable design approach when you cannot import Haskell’s evaluation environment into a space—i.e. there is no possibility that I can supply a Haskell
fmap for the
Daml type constructor. A common use case is a SQL EDSL; you do not want to require SQL servers to be able to evaluate Haskell, just to interact with them nicely from Haskell programs.
But when you are working with such types, you are always thinking in two layers at once: the layer in which you are embedded (Haskell), and the layer that is embedded (Daml). The two are unified by the type system, but each has different rules. Things that look like evaluations are not—well, they are, but not quite the evaluations you think.
Moreover, every feature you want to be in the lifted embedding must be reproduced there. I’ve mentioned only simple arithmetic expressions. What about variables, functions, &c?
In practice, SQL reveals the limits of lifted embedding. It is a good choice precisely because we usually want to think in the host language, e.g. Haskell, and only occasionally shift over to the embedded language, getting out of it as quickly as possible. That describes most programs that interact with SQL: a small portion of the program devoted to queries and DML, whose purpose is to get that data into or out of the host program.
That doesn’t really fit Daml; we would be talking about writing the whole program in this dual-layer style. And by the time we have gotten all the features we want? We’re pretty much asking Daml users to write Haskell-in-Haskell, in Haskell.