Hi, guys!
I’m trying to create a class with a parameterized type for object CRUD operations and I’m facing some challenges with type handling in Motoko.
Below is an example of code with a compilation error.
Any help would be appreciated.
import Time "mo:base/Time";
actor {
type ValueObject = {
lastChange : Time.Time;
};
class EntityStore<T <: ValueObject>() {
func addObject(obj : T) {
let newObj = {obj with lastChange = Time.now()};
// What is wrong here?
var test : T = newObj;
}
}
}
expression of type
{lastChange : Time}
cannot produce expected type
T__27(M0096)
Hmm,
The problem here is that the with operator only works on values of record type.
When you apply with to obj of type T, obj’s type is first promoted to a record type.
Since T is a subtype of ValueObject, a record type, this works.
Then the field of the record is updated.
However, the resulting object, newObj, only has type ValueObject, not T, which is why the assignment of var test : T = newObj fails.
At this point the compiler only knows that newObj has type ValueObject but it requires a T so the assignment is illegal. The compiler only knows that T is a subtype of ValueObject but the assignment requires that ValueObject is a subtype of T (the other way around).
I decided to use types for internal object storage to facilitate delivery through actors, without the need for transformation to shared types. Is there any other suggested pattern for this problem?
For now, my solution is to pass an update function as a parameter from the concrete classes, unfortunately, having to replicate the sequence and update time logic.
public class MemberDAO() {
...
private let ON_INSERT = func(m : Member) : Member {
return {
m with id = nextSequenceId();
lastChange = Time.now();
};
};
public func addMember(aMember : Member) : Result<Member, [Text]> {
return store.addObject(aMember, ON_INSERT);
};
...
}
Since obj has a forallT (as long as it has a field lastChange : Time.Time) the compiler knows only one field in it. That (and only that) gets recreated in the record update. The T-ness of it gets lost. There could be a way to runtime-observe the actual fields of the obj and create a (shallow) copy by carrying over all dynamically explored fields. This could then be typed as T. (The syntax could be { obj with<T> lastChange = Time.now() }.) But there is no way to extend: T must have the statically known fields that you overwrite (as you do above). If you feel strongly about this, please submit an issue.
PS.: You can use the non-block syntax for both functions:
private func ON_INSERT(m : Member) : Member =
{ m with id = nextSequenceId();
lastChange = Time.now()
};
public func addMember(aMember : Member) : Result<Member, [Text]> =
store.addObject(aMember, ON_INSERT);
This is much more convenient for one-liner function bodies. Also return should only be used for early returns, values falling off the tail of the function body don’t need it.