Rust + Daml: a biz-friendly smart contract platform deserves a biz-friendly client language

György Balázsi
DAML Masterclass
Published in
14 min readApr 11, 2023

--

TL;DR Daml is an expressive language and so is the Rust code generated from the Daml model. The reason for the similarity is that both languages adopt basic design choices from Haskell.

Photo by James Forbes on Unsplash

A Daml model implements the legal architecture of a business model. So using Daml poses a double challenge:

  1. For the business partners collectively to agree on the common rules of the game and implement them in the form of a Daml model. The Daml model, deployed on the Daml/Canton platform will constitute the shared virtual ledger, used by the business partners as a transaction platform and system of record with need-to-know basis privacy. On the positive side, the fact that all members of a business community use exactly the same piece of software, reduces the risk for the advantages of the collaboration to be overshadowed by costly coordination problems leading to something which economists call “diseconomies of scale”.
  2. For the business partners individually to integrate the Daml/Canton node they operate with their own systems.

Both challenges need the collaboration of business stakeholders, legal experts and developers in order to make sure that the application serves the business purpose for which it is created, it’s legally correct and fulfills the non-functional requirements like processing speed and throughput.

In this blog post, I will focus on point #2, from the angle of business-friendliness. By this, I mean how easy it is for developers and business stakeholders to reason about wether the application really does what it is supposed to do.

Client-side support for Daml

Theoretically, you can write client applications for the Daml/Canton platform in any language which supports gRPC codegen, because the platform exposes a gRPC API, defined by these protobuf docs.

In practice though, it’s tedious to write your client-side datatypes which you want to use as a representation of the Daml datatypes, based directly on the code generated from the protobuf files. This is the reason why some higher-level alternatives exist.

Digital Asset (DA), creators of the Daml/Canton platform offer the following options (you can find further details about all these in the Daml docs):

  • A JSON API which is language-independent. It exposes a small number of endpoints for querying, creating and manipulating smart contracts on the ledger and for some housekeeping. The API is deceptively simple — but this means that the complexity is transferred into the JSON payload. In order to be able to submit a valid request, the client application has to create a JSON which is a Daml-compatible representation of the data to be submitted. For some more involved data types, this is far from being trivial. The JSON API exposes a core set of features of the gRPC API. It doesn’t provide advanced features like retrieving the full transaction stream or a former snapshot of the active contract set. But on the positive side, it provides a cache-like feature (called query store) which makes it faster to retrieve the active contract set than just pulling it from the ledger every time. (A little bit of context as an explanation: a Daml ledger is an event source system, meaning it actually doesn’t store contracts, but contract creation and exercise events (where an exercise may or may not consume the underlying contract), from which — without caching — the active contract set needs to be computed every time we need it. The concept is similar to what is called “unspent transaction output” or UTXO in the Bitcoin world.) The JSON API can be used as a REST or as a websocket API.
  • A Python binding. Using Python’s “async/await” syntax, it’s easy to express ledger interactions. The challenge is again the Daml-compatible representation of the input/output data. The library supports a syntax based on Python (frozen) dictionaries and lists, more or less identically to the Daml-compatible JSON syntax, but nothing more. Using more involved Daml data types can quickly spiral out of control in terms nesting levels and complexity. For this reason, it’s good practice to write (nested) classes for the data types, but this is not supported in any way by the Python library provided by DA. The number of classes needed can be high for a more complex Daml model, and has to be updated manually every time the Daml model changes. (As a personal aside: I like to use the Python binding because it’s compatible with Colab and Jupyter notebooks which is a convenient way of demonstrating some ledger behavior. The output of queries can be displayed conveniently as Pandas dataframes. UI widgets can be used for fast prototyping UI events. I also used the Python bindings to upload data to a ledger from Excel files, building on the Excel reading feature of Pandas.)
  • A TypeScript binding to be used against the JSON API. There is a basic library which can be used independently from any UI framework, and a React library built on top of it. Its main advantage over the Python library is codegen. Through the codegen feature of the Daml platform, we can generate an up-to-date set of TypeScript types from the Daml model. The limitation of TypeScript codegen is thought that TypeScript types are mere type annotations, and don’t provide constructor functions for typed objects. But nonetheless, if you want to write single page applications against Daml using a TypeScript/JavaScript based UI framework, this library is a useful companion.
  • A Java binding to be used agains the gRPC API. It offers the full feature set of the gRPC API, and also supports codegen. The generated code contains Java classes corresponding to the data types contained in the underlying Daml model.
  • Scala binding. It still exists on Maven, but the Daml docs say it’s deprecated and there is no documentation for it.

And last but not least, on top of the client solutions provided by DA, we have an unofficial Rust binding. In the rest of this blog post I will examine this one from the angle of business friendliness. And before that, I will explain how Daml is very business-friendly.

The secret sauce of Daml’s business friendliness

The most expressive way of representing real-world data is the so-called algebraic data type (ADT). An ADT allows for a limited set of options, each option potentially being a compound piece of data, containing a set of values of specified types. It’s similar to the enum datatype present in many languages with the difference that the variants can potentially hold one or more typed fields rather than being a single text label.

An example can be a data type expressing book attributes. Let’s say a piece of book attribute data can express

  • either the number of pages,
  • or the list of authors,
  • or the book’s title,
  • or the book’s publishing details, consisting of the year of publishing and the publisher.

We want to use this data type so that we can rest assured that it’s expressing some book attributes, and we also want to be able to unwrap the individual details from the piece of data.

Functional languages usually (or maybe all of them?) support ADTs, paired with pattern matching for extracting the individual details from it. The data type is called “algebraic” because the set of options is conceptually similar to addition, and the bundle of fields within one variant is conceptually similar to multiplication. (If you want to know what exactly “conceptually similar” means you can consult a category theory text book or watch this video.)

Daml, being a functional language, more explicitly a descendant of Haskell, supports ADTs, called “variant” in the docs. (So “variant” has two meanings: it’s a label for the whole data type and also for one element of it.) The aforementioned book attribute data type can be expressed in Daml (almost, but not entirely in the same way as in Haskell) like this:

data BookAttribure = Pages Int
| Authors [Text]
| Title Text
| Published with year: Int, publisher: Text
deriving (Eq, Show, Ord)

The “BookAttribute” name is the type constructor, it specifies the name of the data type. This is what we use in e.g. function signatures. The pipe operators divide the options for being a book attribute under the umbrella. The “Pages”, “Authors”, “Title” and “Published” names are the data constructors, which are used to specify pieces of data of type BookAttribute. The data types accompanying the data constructors specify their field value types, with or without specifying field names. If we specify more than one field for a constructor it’s mandatory to specify field names in Daml (which is not manadatry in Haskell). (Under the hood, the value constructors are functions, mapping their inputs to a BookAttribute value. This is the reason why they are not namespaced under the wrapper datatype, and must be unique within a module.) The “deriving” clause is necessary so that we can check for equality, display and order pieces of BookAttribute data using default operators, without having to write explicitly these operators for this data type.

In order to mimic a real-world business model more closely, let’s create a Daml contract template with a field which contains a set of book attributes (we don’t use isolated pieces of book attributes, right?). The template below is a slightly modified version of the default skeleton template which is loaded when you start a new Daml project. The Asset can be thought of as a very simple version of an NFT, representing a real-world book, and can be given to somebody else by its owner.

template Asset
with
issuer : Party
owner : Party
id : Text
attributes : Set BookAttribute
where
ensure id /= ""
signatory issuer
observer owner
choice Give : AssetId
with
newOwner : Party
controller owner
do create this with
owner = newOwner

So this kind of expressiveness and type safety is what I mean when I speak about Daml being business-friendly. It turns out that the Rust types generated by the Rust Daml binding are as expressive as the original Daml data types. You will appreciate this similarity when we compare it with the alternative client language options.

Expressive generated Rust types

The generated Rust type for the Asset template payload, which we need to create an Asset contract instance looks like this:

#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Debug)]
pub struct Asset {
pub issuer: DamlParty,
pub owner: DamlParty,
pub id: DamlText,
pub attributes:
super::super::super::daml_stdlib_da_set_types::da::set::types::Set<BookAttribute>,
}

(The code contains the full module path to the Set datatype, which is ugly but necessary, because the code generator doesn’t declare this path in a “use” statement.)

The BookAttribute data type used in the definition of the “attributes” field is the following:

#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Debug)]
pub enum BookAttribute {
Pages(DamlInt64),
Authors(DamlList<DamlText>),
Title(DamlText),
Published(Published),
}

Digging deeper into the Published struct:

#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Debug)]
pub struct Published {
pub year: DamlInt64,
pub publisher: DamlText,
}

Apart from the curly braces and some other syntax elements, pretty similar, right? One notable difference is that in Rust an enum variant, unlike a Daml ADT variant, cannot have fields itself, but instead can store a struct data type which wraps the fields. Another difference is that, as we will see shortly, in Rust enum variants are namespaced under the enum type, which in general gives more freedom: two different enums can have identical variants without causing a name clash.

The similarity is not a coincidence. Rust is not a functional language per se, but its creators adopted some design choices from functional languages. Among these is the expressive enum datatype.

An important aside: A more fundamental similarity between Daml (Haskell) and Rust is, also based on ADT and enum respectivly, the lack of a null value. Neither of these languages contain a universal “nothing” value, which in other languages constitutes a crack on the shield of type checking, as a result of the infamous “billion dollar mistake”. Instead, the lack of a value is always a typed “nothing”, expressed by an ADT/enum variant, called Nothing (of type Maybe a) in Haskell, None (of type Optional a) in Daml, and Option<a>::None in Rust.

Now let’s see how we can specify values in Daml vs Rust.

The following piece of Daml data, a possible value for an Asset contract payload (in Daml, the name of a contract template is at the same time the name of the record data type holding the contract payload): …

import DA.Set qualified as S

let asset = Asset with
issuer = alice
owner = alice
id = "abc123"
attributes = S.fromList [ Pages 104
, Authors ["Lewis Carroll", "John Tenniel"]
, Title "Alice's Adventures in Wonderland"
, Published 1865 "Macmillan"]

… can be expressed using the generated Rust types as follows:

fn main() {

let attributes_map : DamlGenMap<BookAttribute, ()> = DamlGenMap::from([
(BookAttribute::Pages(104), ()),
(BookAttribute::Authors(DamlList::from(["Lewis Carroll".to_string(), "John Tenniel".to_string()])), ()),
(BookAttribute::Title("Alice's Adventures in Wonderland".to_string()), ()),
(BookAttribute::Published(Published{ year: 1865, publisher: "Macmmillan".to_string() }), ())
]);
let attributes: Set<BookAttribute> = Set::new(attributes_map);
let asset:Asset = Asset::new("alice", "alice", "abc123", attributes );

dbg!(asset);
}

The construction of the set of book attributes is a little bit involved. Under the hood, a set is a map which maps its elements to the unit value, and there is no constructor function which hides this implementation detail. (This is true for other client languages, see later.)

Otherwise the syntax for creating a fairly complex piece of data is clean and readable.

Future vision: UI widgets generated dynamically from the generated Rust types

This section is short, because I didn’t try anything from what I write here, I’m just speculating.

It would be great to be able to generate and update UI widgets dynamically, based on the Daml model/generated client code, in a similar fashion as Navigator works currently. This would alleviate the burden of updating the UI code each time the Daml model changes.

Another concern is sometimes that teams use use two sets of generated code: TypeScript for the UI, and Java for the middleware. If we could generate the UI widgets from the generated code used for the middleware, we could simplify the technology stack.

My hunch is that all this will be pretty much doable, maybe not tomorrow but soon, as Rust is on its way be become a viable UI language. Let’s see some hints:

As the generated Rust code for ADT’s contains enums and structs, we need a way to enumerate enum variants to populate dropdown lists representing enum variants dynamically. The strum crate does just this: “Strum is a set of macros and traits for working with enums and strings easier in Rust.”

Rust is not a widely used GUI language, but there are some libraries already. The website Are we GUI yet? e.g. lists some of these libraries.

Inside-out programming for harmonized applications

Let’s take a step back for a moment and take a look at the purpose we are serving with Daml applications.

By “inside-out” I mean that start thinking at the core of the application progressing towards the end users.

The core of a shared transaction system and virtual system of record is the common Daml model.

The fact that we can create a seamless fit between the common Daml code and the client applications, means that we create harmony along this pipeline:

BUSINESS MODEL >> DAML MODEL >> GENERATED CODE >> CLIENT-SIDE DATA REPRESENTATION >> UI

Which means at the end of the day that e.g. users of client systems of different members of the business community use harmonized applications, without the need to actually harmonizing their systems other than connecting to the same Daml model.

What about the alternative client languages?

Now let’s go back to the topic of the expressiveness and type safety of the client-side code.

You will appreciate the expressiveness and type safety of the generated Rust types if you compare them to the way other client library languages express the same kind of data, namely a set of variants.

For the JSON API, the following payload is needed. You can observe that both the variant and the set datatype representation is a bit involved. It’s needless to say that on this JSON object there is not type checking whatsoever, so you will only know if the JSON is not compatible with the Daml model when the ledger API rejects the submission.

{
"issuer": "Alice::1220026465cd981d159dae404a92f96477a3aab5eb5cc37e924363c43b7203074d47",
"owner": "Alice::1220026465cd981d159dae404a92f96477a3aab5eb5cc37e924363c43b7203074d47",
"id": "abc123",
"attributes": {
"map": [
[
{
"tag": "Pages",
"value": 104
},
{}
],
[
{
"tag": "Authors",
"value": [
"Lewis Carroll",
"John Tenniel"
]
},
{}
],
[
{
"tag": "Title",
"value": "Alice's Adventures in Wonderland"
},
{}
],
[
{
"tag": "Published",
"value": {
"year": 1865,
"publisher": "Macmillan"
}
},
{}
]
]
}
}

For the Python binding, you have to produce the corresponding structure using Python dicts and lists, so I will skip that for now.

The generated code for the TypeScript binding doesn’t contain classes (so we don’t get value constructors with it). It contains type aliases which can be used for type checking. The type alias mirroring the BookAttribute Daml ADT looks like as follows:

export declare type BookAttribute =
| { tag: 'Pages'; value: damlTypes.Int }
| { tag: 'Authors'; value: string[] }
| { tag: 'Title'; value: string }
| { tag: 'Published'; value: BookAttribute.Published }

Using this type annotation, we can declare the Asset payload as follows:

let attributesMap : damlTypes.Map<BookAttribute, {}> = damlTypes.emptyMap();

let pages: BookAttribute = {tag: "Pages", value: "104"};
attributesMap = attributesMap.set(pages, {});

let authors: BookAttribute = {tag: "Authors", value: ["Lewis Carroll", "John Tenniel"]};
attributesMap = attributesMap.set(authors, {});

let title: BookAttribute = {tag: "Title", value: "Alice's Adventures in Wonderland"};
attributesMap = attributesMap.set(title, {});

let published: BookAttribute = {tag: "Published", value: {year: "1865", publisher: "Macmillan"}};
attributesMap = attributesMap.set(published, {});

let attributes : Set<BookAttribute> = { map : attributesMap };

const asset: Main.Asset = {
issuer: "alice",
owner: "alice",
id : "abc123",
attributes : attributes

}

The BookAttribute type annotation will prevent us from declaring variants where the tag string is not the name of an allowed BookAttribute variant, and the value is not of the corresponding type.

Java doesn’t know the notion of ADT. The code generated for the Java binding applies a workaround: the equivalent of the Daml type constructor (BookAttribute) is represented by an abstract class, and the Daml value constructors (Pages, Authors, Title, Published) are represented by classes which extend BookAttribute.

public abstract class BookAttribute extends Variant<BookAttribute> {...}

public class Authors extends BookAttribute {...}

public class Pages extends BookAttribute {...}

public class Published extends BookAttribute {...}

public class Title extends BookAttribute {...}

You can find a detailed description of this implementation in the Daml docs. (Actually, I have chosen BookAttribute as an illustration so that I can refer to this section of the docs.)

This workaround of course is not type safe: nothing prevents us from creating by hand another subclass extending BookAttribute, like Isbn.

public class Isbn extends BookAttribute {...}

The Java compiler will accept an Isbn value as BookAttribute. The following code will compile, and you will only know that it’s not compatible with the Daml model (because Isbn is not a valid value constructor for the BookAttribute Daml type) when the Daml ledger API rejects the submission which contains it.

public class Main {

public static void main(String[] args) {

Pages pages = new Pages(104L);
Authors authors = new Authors(List.of("Lewis Carroll", "John Tenniel"));
Title title = new Title("Alice's Adventures in Wonderland");
Published published = new Published(1865L, "Macmillan");
Isbn isbn = new Isbn("9780440000754");
Map<BookAttribute, Unit> attribuesMap = Map.of(
pages, Unit.getInstance(),
authors, Unit.getInstance(),
title, Unit.getInstance(),
published, Unit.getInstance(),
// Delete the line below so that
// Daml accepts the input.
isbn, Unit.getInstance()
);
Set<BookAttribute> attributes = new Set<>(attribuesMap);
Asset asset = new Asset("alice", "alice", "abc123", attributes);

System.out.println(asset);

}
}

With that said, this lack of type safety, though not elegant, is not likely to cause any problems in practice, because we don’t have any good reason to extend BookAttribute by hand. If the set of variants changes in the Daml model, we apply codegen again which will generate the correct new set of subclasses. (I ignore here the criminal case when a malicious agent is able to infiltrate your organization and wants to cause a DOS situation exploiting this gap.)

Conclusion

When you make a decision about which client language/library to use, there are several factors to consider.

Probably there are limitations encoded in the context in the first place: architecture decisions, technology stack used by the connected systems and/or the project, preferred languages of the team, etc. Given the fact that Python, Java and JavaScript/TypeScript are among the most widely used languages currently, there is a good chance that your project will choose one of these as a client language.

Another consideration is that the current version of the Daml Rust binding doesn’t support (yet) the latest Daml-LF version which is needed to use Daml interfaces. (Daml-LF is the underlying “raw” language which actually encodes Daml data, produced by the Daml compiler.) So if you need Daml interfaces you currently cannot use the Rust binding (unless you are willing to put sum effort into extending it).

But if speed, memory safety, thread safety, expressiveness, type safety and cool factor are on top of the list of your non-functional requirements and you don’t need Daml interfaces (or are willing to put in some extra effort), the Rust binding can be a viable choice.

--

--