Joachim Breitner's Homepage
A Candid explainer: Quirks
This is the fifth and final post in a series about the interface description language Candid.
If you made it this far, you now have a good understanding of what Candid is, what it is for and how it is used. For this final post, I’ll put the spotlight on specific aspects of Candid that are maybe surprising, or odd, or quirky. This section will be quite opinionated, and could maybe be called “what I’d do differently if I’d re-do the whole thing”.
Note that these quirks are not serious problems, and they don’t invalidate the overall design. I am writing this up not to discourage the use of Candid, but merely help interested parties to understand it better.
References in the wire format
When the work on Candid began at DFINITY, the Internet Computer was still far away from being a thing, and many fundamental aspects about it were still in the air. I particular, there was still talk about embracing capabilities as a core feature of the application model, which would be implemented as opaque references on the system level, likely building on WebAssembly’s host reference type proposal (which only landed recently), and could be used to model access permissions, custom tokens and many other things.
So Candid is designed with that in mind, and you’ll find that its wire format is not just a type table and a value table, but actually
a triple (T, M, R), where T (“type”) and M (“memory”) are sequences of bytes and R (“references”) is a sequence of references.
Also the wire format for values of function service tyeps have an extra byte to distinguish between “public references” (represented by a principal and possible a method name in the data part), and these opaque references.
Alas, references never made it into the Internet Computer, so all Candid implementations simply ignore that part of the specification. But it’s still in the spec, and if it confused you before, now you know why.
Hashed field names
Candid record and variant types look like they have textual field names:
type T = record { syndactyle : nat; trustbuster: bool }
But that is actually only true superficially. The wire format for Candid only stores hashes of field names. So the above is actually equivalent to
type T = record { 4260381820 : nat; 3504418361 : bool }
or, for that matter, to
type T = record { destroys : bool; rectum : nat }
(Yes, I used an english word list to find these hash collisions. There aren’t that many actually.)
The benefit of such hashing is that the messages are a bit smaller in most (not all) cases, but it is a big annoyance for dynamic uses of Candid. It’s the reason why tools like dfx
, if they don’t know the Candid interface of a service, will print the result with just the numerical hash, letting you guess which field is which.
It also complicates host languages that derive Candid types from the host language, like Motoko, as some records (e.g. record { trustbuster: bool; destroys : int }
) with field name hash collisions can not be represented in Candid, and either the host language’s type system needs to be Candid aware now (as is the case of Motoko), or serialization/deserialization will fail at runtime, or odd bugs can happen.
(More discussion of this issue).
Tuples
Many languages have a built-in notion of a tuple type (e.g. (Int, Bool)
), but Candid does not have such a type. The only first class product type is records.
This means that tuples have to encoded as records somehow. Conveniently(?) record fields are just numbers after all, so the type (Int, Bool)
would be mapped to the type
record { 0 : int; 1 : bool }
So tuples can be expressed. But from my experience implementing the type mappings for Motoko and Haskell this is causing headaches. To get a good experience when importing from Candid, the tools have to try to reconstruct which records may have been tuples originally, and turn them into tuples.
The main argument for the status quo is that Candid types should be canonical, and there should not be more than one product type, and records are fine, and there needs to be no anonymous product type. But that has never quite resonated with me, given the practical reality of tuple types in many host languages.
Argument sequences
Did I say that Candid does not have tuple types? Turns out it does, sort of. There is no first class anonymous product, but since functions take sequences of arguments and results, there is a tuple type right there:
func foo : (bool, int) -> (int, bool)
Again, I found that ergonomic interaction with host languages becomes relatively unwieldy by requiring functions to take and return sequences of values. This is especially true for languages where functions take one argument value or return one result type (the latter being very common). Here, return sequences of length one are turned into that type directly, longer argument sequences turn into the host language’s tuple type, and nullary argument sequences turn into the idiomatic unit type. But this means that the types (int, bool) -> ()
and (record { 0: int, 1: bool}) -> ()
may be mapped to the same host language type, which causes problems when you hope to encode all necessary Candid type information in the host language.
Another oddity with argument and result sequences is that you can give names to the entries, e.g. write
func hello : (last_name : text; first_name : text) -> ()
but these names are completely ignored! So while this looks like you can, for example, add new optional arguments in the middle, such as
func hello : (last_name : text; middle_name: opt text, first_name : text) -> ()
without breaking clients, this does not have the effect you think it has and will likely break.
My suggestion is to never put names on function arguments and result values in Candid interfaces, and for anything that might be extended with new fields or where you want to name the arguments, use a single record type as the only argument:
func hello : (record { last_name : text; first_name : text}) -> ()
This allows you to add and remove arguments more easily and reliably.
Type “shorthands”
The Candid specification defines a system of types, and then adds a number of “syntactic short-hands”. For example, if you write blob
in a Candid type description, it ought to means the same as vec nat8
.
My qualm with that is that it doesn’t always mean the same. A Candid type description is interpreted by a number of, say, “consumers”. Two such consumers are part of the Candid specification:
- The specification that defines the wire format for that type
- The upgrading (subtyping) rules
But there are more! For every host language, there is some mapping from Candid types to host language types, and also generic tools like Candid UI are consumers of the type algebra. If these were to take the Candid specification as gospel, they would be forced to treat blob
and vec nat8
the same, but that would be quite unergonomic and might cause performance regressions (most language try to map blob
to some compact binary data type, while vec t
tends to turn into some form of array structure).
So they need to be pragmatic and treat blob
and vec nat8
differently. But then, for all practical purposes, blob
is not just a short-hand of vec nat8
. They are different types that just happens to have the same wire representations and subtyping relations.
This affects not just blob
, but also “tuples” (record { blob; int; bool }
) and field “names”, as discussed above.
The value text format
For a while after defining Candid, the only implementation was in Motoko, and all the plumbing was automatic, so there was never a need for users to to explicitly handle Candid values, as all values were Motoko values. Still, for debugging and testing and such things, we eventually needed a way to print out Candid values, so the text format was defined (“To enable convenient debugging, the following grammar specifies a text format for values…”).
But soon the dfx
tool learned to talk to canisters, and now users needed to enter Candid value on the command line, possibly even when talking to canisters for which the interface was not known to dfx. And, sure enough, the textual interface defined in the Candid spec was used.
Unfortunately, since it was not designed for that use case, it is rather unwieldy:
It is quite verbose. You have to write
record { … }
, not just{ … }
. Vectors are writtenvec { …; …}
instead of some conventional syntax like[…, …]
. Variants are written asvariant { error = "…"}
with braces that don’t any value here, and something like#error "…"
might have worked as well.With a bit more care, a more concise and ergonomic syntax might have been possible.
It wasn’t designed to be sufficient to create a Candid value from it. If you write
5
it’s unclear whether that’s anat
or anint16
or what (and all of these have different wire representations). Type annotations were later added, but are relatively unwieldy, and don’t cover all cases (e.g. a service reference with a recursive type cannot be represented in the textual format at the moment).Not really the fault of the textual format, but some useful information about the types is not reflected in the type description that’s part of the wire format. In particular not the field names, and whether a value was intended to be binary data (
blob
) or a list of small numbers (vec nat8
), so pretty-printing such values requires guesswork. The Haskell library even tries to brute-force the hash to guess the field name, if it is short or in a english word list!
In hindsight I think it was too optimistic to assume that correct static type information is always available, and instead of actively trying to discourage dynamic use, Candid might be better if we had taken these (unavoidable?) use cases into account.
Custom wire format
At the beginning of this post, I have a “Candid is …” list. The list is relatively long, and the actual wire format is just one bullet point. Yes, defining a wire format that works isn’t rocket science, and it was easiest to just make one up. But since most of the interesting meat of Candid is in other aspects (subtyping rules, host language integration), I wonder if it would have been better to use an existing, generic wire format, such as CBOR, and build Candid as a layer on top.
This would give us plenty of tools and libraries to begin with. And maybe it would have reduced barrier of entry for developers, which now led to the very strange situation that DFINITY advocates for the universal use of Candid on the Internet Computer, so that all services can smoothly interact, but two of the most important services on the Internet Computer (the registry and the ledger) use Protobuf as their primary interface format, with Candid interfaces missing or an afterthought.
The type constructor opcodes
A small quirk of the wire format: It defines op-codes for every type constructor, both primitive (bool
…) and composite (vec
…), and the op-codes from primitive and composite types share a common namespace. This is a left-over from earlier iterations of the design where primitive and composite types may have been encoded together. But eventually, the “type table” was introduced, and composite types are now always referenced via their index in the type table, and the type table contains only composite types. In other words: In any place, one either deals with primitive types/table indices, or composite types. I think it might have been clearer and more honest if primitive and composite types got numbered independently.
Sideways Interface Evolution
This is not a quirk of Candid itself, but rather an idiom of how you can use Candid that emerged from our solution for record extensions and upgrades.
Consider our example from before, a service with interface
service { add_user : (record { login : text; name : text }) -> () }
where you want to add an age
field, which should be a number.
The “official” way of doing that is to add that field with an optional type:
service { add_user : (record { login : text; name : text; age : opt nat }) -> () }
As explained above, this will not break old clients, as the decoder will treat a missing argument as null
. So far so good.
But often when adding such a field you don’t want to bother new clients with the fact that this age
was, at some point in the past, not there yet. And you can do that! The trick is to distinguish between the interface you publish and the interface you implement. You can (for example in your documentation) state that the interface is
service { add_user : (record { login : text; name : text; age : nat }) -> () }
which is not a subtype of the old type, but it is the interface you want new clients to work with. And then your implementation uses the type with opt nat
. Calls from old clients will come through as null
, and calls from new clients will come through as opt 42
.
We can see this idiom used in the Management Canister of the Internet Computer. The current documented interface only mentions a controllers : vec principal
field in the settings, but the implementation still can handle both the old controller : principal
and the new controllers
field.
It’s probably advisable to let your CI system check that new versions of your service continue to implement all published interfaces, including past interfaces. But as long as the actual implementation’s interface is a subtype of all interfaces ever published, this works fine.
This pattern is related to when your service implements, say, http_request
(so its implemented interface is a subtype of that common interface), but does not include that method in the published documentation (because clients of your service don’t need to call it).
Self-describing Services
As you have noticed, Candid was very much designed assuming that all parties always have the service type of services they want to interact with. But the Candid specification does not define how one can obtain the interface of a given service, and there isn’t really a an official way to do that on the Internet Computer.
That is unfortunate, because many interesting features depend on that: Such as writing import C "ic:7e6iv-biaaa-aaaaf-aaada-cai"
in your Motoko program, and having it’s type right there. Or tools like ic.rocks, that allow you to interact with any canister right there.
One reason why we don’t really have that feature yet is because of disagreements about how dynamic that feature should be. Should you be able to just ask the canister for its interface (and allow the canister to vary the response, for example if it can change its functionality over time, even without changing the actual wasm code)? Or is the interface a static property of the code, and one should be able to query the system for that data, without the canister’s active involvement. Or, completely different, should interfaces be distributed out of band, maybe together with API documentation, or in some canister registry somewhere else?
I always leaned towards the first of these options, but not convincingly enough. The second options requires system assistance, so more components to change, more teams to be involved that maybe intrinsically don’t care a lot about this feature. And the third might have emerged as the community matures and builds such infrastructure, but that did not happen yet.
In the end I sneaked in an implementation of the first into Motoko, arguing that even if we don’t know yet how this feature will be implemented eventually, we all want the feature to exist somehow, and we really really want to unblock all the interesting applications it enables (e.g. Candid UI). That’s why every Motoko canister, and some rust canisters too, implements a method
__get_candid_interface_tmp_hack : () -> (text)
that one can use to get the Candid interface file.
The name was chosen to signal that this may not be the final interface, but like all good provisional solutions, it may last longer than intended. If that’s the case, I’m not sorry.
This concludes my blog post series about Candid, for now. If you want to know more, feel free to post your question on the DFINTY developer forum, and I’ll probably answer.
Have something to say? You can post a comment by sending an e-Mail to me at <mail@joachim-breitner.de>, and I will include it here.