In theory module A, B, and C are built by different development shops, are audited and provide functionality in different domains. They are prebuilt functionality that a dev can add into their canister with little or, ideally, 0 knowledge of how or why the work. We certainly don’t want the canister developer to have to worry about migration. They should ‘just work’.
For example, I want a canister that is an ICRC-1/2/3/(ICRC-1 module, ICRC-2 Module that requires ICRC-1(but ICRC1-knows nothing of 2), ICRC-3 Module for logging that is completely agnostic to tokens and is just a ledger compatible token that uses ICRC-75 to maintain a list of accounts that are each issued tokens once a month via TimerTool module that is fed events and makes scheduling that survives restart and upgrade as easy as writing a couple of lines of code.
In an ideal world, all can just be wired together(and yes actor modules would be huge here because right now when we do this we have a ton of boilerplate that equates to public shared(msg) my_func(args :MyModule.args){ await* MyModule.myFunc(msg.caller, args)};
import TT "mo:timer-tool";
import ICRC1 "mo:icrc1.mo";
import ICRC2 "mo:icrc2.mo";
import ICRC3 "mo:icrc3.mo";
import ICRC75 "mo:icrc75.mo";
(with migration = func(old: TT.old and ICRC1.old and ICRC2.old and ICRC3.old and ICRC75.old) :
{TT.new and ICRC1.new and ICRC2.new and ICRC3.new and ICRC75.new}{
{
ttState = TT.migrate(old.ttState);
icrc1State = ICRC1.migrate(old.icrc1State);
icrc2State = ICRC2.migrate(old.icrc2State);
icrc3State = ICRC2.migrate(old.icrc2State);
icrc75State = ICRC2.migrate(old.icrc2State);
};
persistent actor class MyToken(args: MyTpes.args) = this {
//I'm guessing with orthogonal persistence these don't need to be stable anymore?
var ttState = TT.initialState : TT.new;
var icrc1State = ICRC1.initialState : ICRC1.new;
.... other states
//now we pass these states to classes(perhaps we get stateful classes in the future)
let tt = TT.TT(ttState, {environmentVariables});
let icrc1 = ICRC1.ICRC1(icrc1State, {environmentVariables and handler functions};
....
// ideally the : public shared(msg) (ICRC1.TransferArgs) => async Nat would be optional
public shared icrc1_transfer = icrc1.transfer : public shared(msg) (ICRC1.TransferArgs) => async Nat;
.... and so on
.... but also the following so you can override the default function if desired
public shared override icrc2_approval......
};
I guess super ideal would be something like inheritance for actor classes so the shared functions get added auto magically and you only have to write some code if you want to override.
If you could put the with Migration in that sub actor then the end user of the component wouldn’t need to know about migration at all…just mark it as persistent and add all the descendant classes and it will look for and call the migration mentioned on any public vars in that class(which I guess should get prompted and ‘injected’ into the wrapping actor as well.
This is how I want to be able to write a canister from multiple third party components…this currently would be probably hundreds of lines of code shorter than what I currently have to do:
import TT "mo:timer-tool";
import ICRC1 "mo:icrc1.mo";
import ICRC2 "mo:icrc2.mo";
import ICRC3 "mo:icrc3.mo";
import ICRC75 "mo:icrc75.mo";
//args would need to be 'smart' here and know which item in args to pass to each class or maybe they are dumb and HAVE to have no arguments and is assumed the user will call something like .init()
//and I don't know about the notation, I know `:` is already types...so I used
persistent actor class MyToken(args: MyTypes.args) :: {
TT.TT as tt;
ICRC1.ICRC1 as icrc1,
ICRC2.ICRC2 as icrc2;
ICRC3.ICRC3 as icrc3;
ICRC75.ICRC75 as icrc75; = this {
///define envVars, handlers, config, et
tt.init(envVars);
icrc3.init(envVars); //we have to do 3 first because it is passed to 1
icrc1.init({
addRecord = icrc3.addRecord;
symbol = "MYT";
logo = "data;....";
name = "My Name"
....});
icrc2.init(envVars);
....etc
tt.setTimer(now + one_month, paytokens, null):
private payTokens(eventArgs: TT.EventArgs){
for(thisItem in ICRC75.ICRC75.instance.membersOf("com.mylist")){
ICRC1.ICRC1.instance.tranfer(mintingAccount, thisItem, 1_000_000
}
};
public override shared icrc2_approve(approveArgs: ICRC2.ApproveArgs) : async Nat{
//custom code for this functions
};
};
//other file for icrc1 definition...do migrations here.
//type defs
//migration def
(with migration = migrations)
public subactorclass ICRC75(){
public func init(env: ENV) {//define};
var accounts = Map.Map<Account, Nat>;
public shared(msg) function icrc1_transfer(args: TransferArgs) : async Nat {
//define
};
....other funcs
}