-
Notifications
You must be signed in to change notification settings - Fork 97
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
DFX-Interface: Imports canister code (as actor classes) #1549
Conversation
In meeting with @crusso and @matthewhammer about dynamic actor creation, we converged on Claudio’s proposal that a clean, easy and desirable way to expose dynamic actor creation in Motoko is to get some help from `dfx`. Big plus of this approach: You can, in one `dfx`, project, have a Motoko and a Rust canister module, and the Motoko code can “import” the rust “code” and then dynamically install instances of that code. So this allows you write high-performance infinitely scaleable Rust backend canisters together with a nice declarative Motoko canister that orchestrates things. But thanks to Candid it also works nicely within Motoko. Another big approach is that is seems the fasted way towards clean, hackfree dynamic actor creation. The rough idea is that in Motoko you can write import Worker "class:worker"; and have that to be equivalent to `actor class Worker(params) body` where `params` is imported (via a Candid-like file, `.params`) and the `body` is also imported (by reading the actual `.wasm` file). The impact on `dfx` is that it would do more of the stuff that it currently does with the `.did` files: Figure out which canisters depend on which, get build artifacts from `moc` (now: `.did`, then: also the `.wasm` (which will be used the Motoko to install) and `.params` (so that Motoko knows how to type the the imported class)) and making them available to the using canister. (The `.params` may also be useful for `dfx` to type-check the argument to `canister canister install` for parametrized canisters.) All filenames and other names up for bikeshedding. This was written together by @crusso and @matthewhammer.
a4f630a
to
4fcca28
Compare
This PR does not affect the produced WebAssembly code. |
|
||
As the previous point, but passing `--param-idl` to `moc`. | ||
|
||
Theis does not issue any warnings. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Theis does not issue any warnings. | |
This does not issue any warnings. |
How do we generate the |
I'd like to see a concrete example with 4 files: the Motoko code, its generated IDL and Params, and a Motoko canister that uses those. |
Here is a full example Input for
Output for
Output for
Output for
An actor using this:
|
Since we are extending candid syntax, maybe we can make Candid to support service class directly? So there is no need for a separate |
We discussed this, but the rationale for separating them is that there is a phase distinction that gives very little gain from combining these AST fragments into a single file, with a single AST:
|
We can refine the |
Make sense. So if the |
The idea would be to generate the Once we add the ability to deserialize the installation argument passed to a motoko canister, if you forge the I guess this wouldn't easily support recursive canister installation unless we add some system method to access the wasm blob of the current actor class. I guess that leaves the question of how to dynamically create an actor from a Motoko wasm file that produces an actor, not an actor class. and has no It's a bit weird that the client of an imported actor (class) that is mean to be installed dynamically needs to embed the code for that class (rather than somehow link to some existing code) but that seems to be what our platform supports as of now. |
To fix the strange linking/code duplication issue, we could move to a system where we can install the wasm module of a canister class and then separately create canister instances of that class with a system call that takes an installed class id and an argument blob. But I guess that's much further down the line. |
But why not make the Candid Something like (to take @matthewhammer's example above):
I'm making up stuff on the actual syntax, but the point of having a single file has advantages; for example, we don't need 2 files in assets to know a canisters' API (with this we need both did and params). Also we can enhance the ecosystem around Candid, not around Motoko. Other languages are already working with Candid for interfaces. Adding a new file just mean more work for I don't see why having two files is better than having one. |
They're both needed for code generation or compilation, and they're both declaring types related to the same API. They also both have to live with the canister since we need to be able to query a canister's API... Please understand that Rust canisters (and soon Assembly Script) need all the files from output of a build, and not only that, but it needs to produce those outputs too, to be able to interop with Motoko. We would need to move those additional file around, produce it somehow, and use it. And error out when it's not found. For Rust we're already asking people to create a This does not seem to hit that threshold. |
That is all only half true:
Another reason: we want to support both driving Candid from the Canister code, but also programming canisters against an externally given Candid interface. In that case It seems that shuffling two files in parallel is less work for dfx than actually having to parse a candid class file (as you propose) to extract the candid service file. (Or we just leave it there as cruft, but that's kinda leaky.) |
This is interesting. But I'm missing a vital piece of information here, namely how multi-canister apps get deployed. I can guess that you have one of two ways in mind:
I assume you're thinking the former, but can you elaborate on the specifics? Taking a step back, I can see two coherent approaches to introducing multi-canister apps:
Option 1 does not seem particularly valuable. We could just as well support regular modules with actor classes in them without introducing a new ad-hoc kind of import (let's call that alternative Option 0). Option 2 on the other hand extends the expressiveness of the platform, which of course adds value. However, it seems like substantially more work than Option 0. So is the only motivation cross-language bundling? Is that relevant for Tungsten? In particular, if we do Option 2 then I think we should do it a little less ad-hoc. This idea effectively brings a new concept to the (Candid-level abstraction of) the platform. So if we go there, then Candid should have a proper way of speaking about it. In particular, I think "actor classes" should be a first-class notion then, with its own first-class Candid type, and a suitable name ("service factory"? Yikes! :) ). Let's say we introduce such factory types, e.g.:
They are semantically similar to a function type A .did file could then describe a service factory. When dfx is faced with such a .did for a canister to upload, then it effectively performs the "application". That is, it (1) knows that the user has to supply arguments, and (2) it produces a .did describing the result type, i.e., rewrites the .did to a plain service type to be put on chain. Probably a bit more work than what's described in the PR. But let's avoid half-assed solutions. If we think this is too much work for Tungsten then I prefer we stick to Option 0 for now. ;) |
The proposal here is actually option 1: There are no new system features proposed or required, and this is of course crucial for something we want to have running in <30 days! Something to keep in mind before we stroll into the territory of what we might envision in an ideal future. I disagree that this is Motoko-specific. Rust, with
Similar, but not quite the same: you could not include a Rust or raw Wat canister classes then (as you could only import |
This just means we bundle the proposed |
The proposal is meant to be language agnostic, so languages can instantiate canisters written in other languages in a strongly typed way. The idea was that you could have a motoko manager canister instantiating Rust worker canisters if you wanted. Or an all motoko solution. I actually don't see any reason to introduce the service constructor type now unless we also want to pass factories/constructors in message/store them in data structures. Nice to have maybe, but not necessary now. I sympathise with the view that the idl for a wasm module should be self-contained. Maybe we can leave it to the compilers to project the relevant parts of the idl instead of leaving it to dfx. A canister import takes the type of service declaraton and codomain of a factory declaration, a factory import takes the domain and range. We don't need a first class service type, just an optional argument on the final service exported by the IDL. |
Oh, I completely missed that Andreas is proposing to add first-class classes to Candid types… |
My argument is along these lines:
Having the parameters in the IDL is more consistent with that and also nicer for other reasons mentioned. It kind of fills a gap we currently have (I suppose Candid currently has no way of checking whether the user supplies the right parameters?). The only downside is that dfx has to parse and output Candid now, but that would already be the case with the params file. And it avoids questions like whether or how I can use type defs from the .did in the .param file. |
As you think about extending that format, would you also add, for example, the type information about the stable data to that file? Or would that still be a separate interface file? |
Hah, good question. You could make the argument that if Candid rewrites the .did anyway then why not strip private stable vars? I think the fundamental difference is that stable variables are purely a Motoko concept, not a platform notion. It is not realistic to me that other languages will define binary-compatible representations, or even a similar concept (even if @crusso would like that). Moreover, I believe that the types used to declare stable decls need to be Motoko types, not Candid types -- the latter are likely to be insufficient and too imprecise for this purpose, depending on what we gonna allow in stable vars eventually. |
So that settles it? |
I'm actually intrigued by the factories-in-Candid idea. I'm fine going that direction (even if only half the way for now), if we find somebody willing to do the dfx-side work. Otherwise I'd say let's keep that for after Tungsten. |
We are talking about the interface change, and it is generic for all languages: When calling Before installation:
After installation:
My proposal is to make the initialization as a comment:
|
My question remains; Why even change the DID file? |
@hansl, I'm not sure what you're asking. We're introducing parameters to canister instantiation. Dfx needs to know about them and their types, so that they can be supplied and encoded when the canister is installed. This is independent of what language a canister was compiled from. OTOH, after installation, the parameters have been eliminated. The type that a client of the installed canister sees does not involve them. It's like the difference between a function and its result. It doesn't make sense to associate a type with the running canister that still contains parameters. Besides the domain mismatch, they are in fact a private implementation detail of how that canister was created, and it would be a form of abstraction leak to expose them. Furthermore, probably at some point after Sodium, we should support dynamic actor creation coherently on the platform and in Candid. That suggests the ability to pass around uninstantiated canisters. With that it becomes even more apparent that there is a fundamental difference between types |
I think of this as a Sodium limitation. To truly support dynamic canister creation, it is natural to ask for the ability to pass around uninstantiated actors. So I assume this eventually has to become a proper part of the type algebra itself. If at all possible, I would prefer to avoid hacks like assigning semantics to comments.
Well, we could use a different keyword, but it's not necessary. It doesn't seem too bad to reuse the same keyword for both instantiated and uninstantiated services. |
Although having a dedicated, concise, clear keyword, and hence term, for what we have been calling “canister module”, “service factory”, “canister factory”, “uninstantiated service”, “actor class constructor”, etc. would be very helpful… (no solution offered here, though). |
Yeah, fair enough. But I don't have a good suggestion either. None of "factory", "constructor", "class", "template", "module" are a good fit. It's becoming apparent that it can only be "functor". :) |
I am in favor. But I also wonder: Are we just embracing future confusion, and a lifetime of explaining naming ambiguities? (since, I assume you mean "functor" in the SML sense, not the category theory sense). |
Totally agree we need a name for this (important) concept. No strong feelings about it, in particular. I do like "canister functor" the most, especially if we think of that is being the best matching concept from the ones that are available (Andreas is excluding all other choices, helpfully). Though as I mentioned, I do wonder about confusing people that have some passing familiarity with category theory, but I think we don't need to worry about that population among our target developers (?), or we can just trust them to understand that there are pre-existing name clashes, and we aren't adding new ones (?). |
I would intuitively expect a “canister functor” to be something that takes a canister, and returns another canister. |
In case the smiley wasn't clear enough: I was joking. Dudes, "functor" would be even worse than most of the other alternatives. If we have to choose among the options so far, "service constructor" would probably be the least bad option for the Candid type. But "canister constructor" sounds weird as well. |
Hmm, what happens when these actors get instantiated? Do we want to update all did files mentioning the actor? It's also unclear how to do the rewriting in the higher order case.
|
FWIW, I think of these actor classes as being like a "parametric service" or a "parameterized service" or something in that vein, in terms of the word
Show me the lattice. : ) Also, there's still "Cannery." : ) Or perhaps: "generalized service" or "abstracted service" or "abstract service" |
@chenyan-dfinity, rewriting the higher-order case seems doable. Or we could just disallow writing it that way. Your other question is a good one. Ties in with the whole question about where the Candid is stored and how it is retrieved. And how that would extend to "constructors", if at all. Hm, maybe there is a point to putting the parameters into a separate file after all. At least until we know the answer to the previous question. |
For the higher order case, there are exponentially many possibilities. The main service can be rewritten to any of the following types, depending on what service gets instantiated.
Putting the parameters into a separate file is the same as having a constructor syntax on the main service definition, maybe this is all we need?
|
@chenyan-dfinity this seems to be precisely the syntax I was suggesting above #1549 (comment) I don't see any reason for introducing constructor types as a first class notion now, since we can always do it later as an extension of Candid, and later redefine the second-class syntax as sugar for the higher-order one. I regard the constructor syntax as the interface for the module (i.e code) to be installed, and its co-domain the interface of the installed canister. It doesn't seem unreasonable for dfx to take some module |
Ah right. I was thinking of the arrow syntax earlier. With this syntax, I think we don't need rewriting, as @nomeata mentioned:
@rossberg WDYT? |
That was exactly my question; why rewrite the DID and why is it a problem?
I'm trying to make my case we should (somewhere non related to this question) at least have the interface available. Sorry I wasn't clear. When I said "why even change the DID file", I meant "why even have DFX rewrite the did output". The format changes are fine (as long as it's backward compatible I guess), but there is no need for DFX to rewrite it. |
The rewriting is implemented in the candid library, but dfx needs to call the rewriting function after |
Why? So that people won't see the init function arguments? |
I'm afraid I don't see what's exponential. The rewriting would just need to insert one type definition:
Or, if it wants to avoid the repetition, modify
(assuming we'd allow this syntax).
I don't see why that is necessary either. If we want to avoid larger rewriting for now, okay, but we can still use the more consistent syntax
while not introducing first-class service constructor types. At least that would be forward-compatible and stylistically coherent.
Because it assigns the wrong kind of type. It's a category error, an abstraction leak, and a potential forward-compatibility problem (what if we eventually want to support both kinds on the platform?). Compare to OO. If you have
would you suggest that |
I'm ok with that provided you mean a grammar like:
without extending However, this a subtly different arrow type (at least at the platform level) than the one for actor methods so I'd argue (not very strongly) |
Yes, that's what I mean. The syntax for methods is analogous and probably equally unfamiliar to Joe OO Programmer. |
I think we are conflating two different did files:
I think both files are useful, and we should keep both, not mutating a single did file to satisfy both purposes. |
You want to import an actor class from a running canister? That may be feasable, but kinda odd (OO analoge: Taking an object and getting it’s class’ constructor). If anything, I thought we’d get platform-hosted modules (but not running canister, with state) for that. But you are right, if that becomes a use case, then uploading the unmodified The use case I am thinkig of the second one you mention, importing an actor from a running canister. This is the use case that I have assumed as agreed upon since over a year, that has one proposed spec in #1510. and it’s a bit embarrassing that we never managed to implement it so far. I guess I didn’t consider the first use case, sorry if that caused confusion, and thanks for pointing out the two. |
hmm, I wasn't thinking of a running canister. I just mean from Motoko side, |
|
In meeting with @crusso and @matthewhammer about dynamic actor creation,
we converged on Claudio’s proposal that a clean, easy and desirable
way to expose dynamic actor creation in Motoko is to get some help from
dfx
.Big plus of this approach: You can, in one
dfx
, project, have a Motokoand a Rust canister module, and the Motoko code can “import” the rust
“code” and then dynamically install instances of that code. So this
allows you write high-performance infinitely scaleable Rust backend
canisters together with a nice declarative Motoko canister that
orchestrates things. But thanks to Candid it also works nicely within
Motoko.
Another big approach is that is seems the fasted way towards clean,
hackfree dynamic actor creation.
The rough idea is that in Motoko you can write
and have that to be equivalent to
actor class Worker(params) body
where
params
is imported (via a Candid-like file,.params
) and thebody
is also imported (by reading the actual.wasm
file).The impact on
dfx
is that it would do more of the stuff that itcurrently does with the
.did
files: Figure out which canisters dependon which, get build artifacts from
moc
(now:.did
, then: also the.wasm
(which will be used the Motoko to install) and.params
(sothat Motoko knows how to type the the imported class)) and making them
available to the using canister.
(The
.params
may also be useful fordfx
to type-check the argumentto
canister canister install
for parametrized canisters.)All filenames and other names up for bikeshedding.
This was written together by @crusso and @matthewhammer.