DCI tutorial series for TypeScript

52 views
Skip to first unread message

Andreas Söderlund

unread,
Nov 2, 2022, 4:34:22 AM11/2/22
to object-composition
Sorry for the third message in a few minutes, just wanted to say that I've started on a DCI tutorial series for TypeScript, given its popularity and pretty good duck typing features for Roles. But it suffers from the same limitations as always, and the compromise I've made is to use naming conventions with a linter that checks that the code adheres to DCI rules. What we really need to do is to get Roles into JavaScript through TC39.

Anyway, check it out and let me know what you think: https://blog.encodeart.dev/dci-tutorial-for-typescript-part-1

/Andreas

Matthew Browne

unread,
Nov 2, 2022, 9:01:31 AM11/2/22
to object-co...@googlegroups.com, Andreas Söderlund

Hi Andreas,
This is great! I think this is actually what DCI needs most of all—more people talking about it and explaining it in the wider community.

As for TC39, I agree that that would be best, but do think it will be a huge challenge. I followed the development of one of the more recent additions to JS—class fields—pretty closely, and the process of incorporating even small fixes into the spec based on community feedback was quite difficult. (Of course that's better than the other extreme, where changes are made without sufficient vetting.) Anyway, I think the best first step would be to create a real implementation that works in JS (and TypeScript too if possible), and start a community behind it so people can see and experience the value of it. Even if it's still a small number of people initially, I think that would help when proposing it to TC39, and of course having a concrete implementation to share with the TC39 delegates would help.

My work on implementing true DCI in JS and TS is quite old at this point, but it could still serve as a useful starting point:

https://github.com/mbrowne/babel-dci
https://github.com/mbrowne/typescript-dci

The typescript-dci one was forked from the official TypeScript compiler at the time.

One challenge with TypeScript—as you might already be aware—is that it's now common to compile your code with Babel or one of the newer, faster JS compilers (e.g. SWC and esbuild), but still use the official TypeScript compiler for type-checking. So I'm not sure which would be better to tackle first—the TS compiler so the types work, or the JS compiler so that it also works for those not using TS. In terms of the codebase, Babel would probably be the easiest to work with. For a while they talked about adding official support for parsing plugins to add new syntax, but the last I saw they had decided against that, and they recommend just forking the parser for this purpose (for my initial implementation, I just imported some of their internals directly, but that is no longer possible, and isn't an acceptable solution for production anyway of course). But the good news is that you wouldn't need to fork anything else in Babel—the actual code transformation can be done in a Babel transform plugin. And BTW, Babel is what the TC39 committee generally uses for proof-of-concept implementations of new proposals.

I don't have much time these days to put more work into these projects, but I'd be happy to be involved where I can, offer feedback and tips, etc.

Cheers,
Matt

P.S. I'll take a more detailed look at your blog post later. The coding approach just using functions and prefix naming looks good.

--
You received this message because you are subscribed to the Google Groups "object-composition" group.
To unsubscribe from this group and stop receiving emails from it, send an email to object-composit...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/object-composition/4e4b70d8-4d64-40a0-a0ad-6296153cf695n%40googlegroups.com.

James O Coplien

unread,
Nov 7, 2022, 5:36:25 AM11/7/22
to noreply-spamdigest via object-composition
Did you also consider and explore a mix-ins approach as an alternative to the more function-based approach that you took?

Andreas Söderlund

unread,
Nov 8, 2022, 3:23:03 AM11/8/22
to object-composition
Did you also consider and explore a mix-ins approach as an alternative to the more function-based approach that you took?

I did, but mixins in typescript/javascript are either class-based, which means that objects cannot be instantiated earlier, or they are added at runtime, which makes unbinding difficult, and because of that it creates a growing list of methods on the object for every Context it participates in, and you also have to solve the issue of name collisions. I wanted to get close to the transient nature of Roles; that they only exist when the Context is executing.

With my approach the objects won't have their RoleMethods attached even when they're inside the Context, so it's not perfect, but I think this is the best compromise. How does it work in trygve under the hood - do the RoleMethods become a part of the Role-player while inside the Context, and in that case, how do you handle name collisions?

/Andreas

James Coplien

unread,
Nov 8, 2022, 5:14:21 AM11/8/22
to object-co...@googlegroups.com


Sent from my iPad

On 8 Nov 2022, at 09.23, Andreas Söderlund <gaz...@gmail.com> wrote:


Did you also consider and explore a mix-ins approach as an alternative to the more function-based approach that you took?

I did, but mixins in typescript/javascript are either class-based, which means that objects cannot be instantiated earlier, or they are added at runtime, which makes unbinding difficult, and because of that it creates a growing list of methods on the object for every Context it participates in, and you also have to solve the issue of name collisions. I wanted to get close to the transient nature of Roles; that they only exist when the Context is executing.

Reasonable.

We should perhaps archive insights like this. These are the potentially deep learnings.


With my approach the objects won't have their RoleMethods attached even when they're inside the Context, so it's not perfect, but I think this is the best compromise. How does it work in trygve under the hood - do the RoleMethods become a part of the Role-player while inside the Context, and in that case, how do you handle name collisions?

In trygve, each object knows what roles it is playing. Any message send knows the object, the role(s) that object is playing, and the invoking context. Each object of course also “knows” its class, and the static type system helps the compiler forseelse the class (and certainly the duck type) of the object, at compile time. And of course the compiler knows the static type of the identifier through which a method is invoked. This latter is key, and is the main determinant of whether to hunt a role method whose formal parameters match the actual parameters supplied for the message. Finding none, conventional lookup ensues, looking at instance methods in the class, as well as in base classes. Static type checking (duck typing) and promotion also apply in a reasonable way, and interfaces figure into those considerations.

A reference-counting architecture tracks role/role-player associations. When a context dissolves, appropriate role/role-player bindings are broken. That and the static type system keep everything rigorous. Masquerading (casting) is, as far as I can tell, impossible. There is no long-term baggage like you describe with Typescript mixins (or Ruby modules).

An object can play several roles - even across contexts. This of course is discouraged unless all but one of them is a stageprop. I have enough info at run time to know exactly what bindings are extant and can issue run-time warnings when a potential “violation of DCI design guidelines” happens. You can’t enforce this at compile time.

All of these add up surprisingly well to a no-surprises, intuitive type model. The compiler code is really (really) complex, but it creates a nice smooth experience for the programmer. There is a lot more stuff afoot than I would put into a production compiler (e.g., the anal multiple-role bindings at run time) but it’s really useful stuff from a research perspective.

Things for Rune to ponder as he goes forward with C#.

James O Coplien

unread,
Nov 8, 2022, 5:30:03 AM11/8/22
to object-co...@googlegroups.com
Maybe a bit more on name collisions:

I can detect almost all name collisions at compile time. The one exception is when an object plays multiple roles. I can catch that, too, but it is a run-time warning. Name collisions are never fatal (I think).

For those I can catch at compile time, I choose “the most obvious choice” and issue a warning.

For the run-time problem, the code is complicated, but it does what “it is supposed to do” based on the type system.

The trygve scoping model is fascinating given the presence of an additional level of both static and dynamic scope in the context, and given that the scope lookup of the object discriminant and of the method follow different scoping rules. Again, the code is complicated (and this has been the source of most compiler bugs), but the results are intuitive, and are often accompanied by warnings if there is any potential unclarity. That means that warnings in trygve actually mean something.


--
You received this message because you are subscribed to the Google Groups "object-composition" group.
To unsubscribe from this group and stop receiving emails from it, send an email to object-composit...@googlegroups.com.

Matthew Browne

unread,
Nov 8, 2022, 5:44:14 AM11/8/22
to object-co...@googlegroups.com

Another thing to keep in mind with JS/TS is that asynchronous code is very common - more common than it is in many other languages - so efforts to automatically unbind role methods somehow after context execution completes are of limited usefulness. (When I say "automatically" I'm still referring to using the language itself without source transformation - with source transformation this issue can be solved. See this for the manual technique that could be done under the hood in a simpler way by a transpiler.)

On another note, it occurred to me that if one were willing to accept the possibility of name collisions as a drawback, one could alternatively just forget about unbinding role methods and rely on the type system to prevent role methods from being called where they shouldn't be—like Cope's C++ implementation. But unlike the C++ implementation, you could theoretically at least wait to add the role methods in the first place until they're needed. In other words, method injection without method un-injection. So TypeScript potentially opens up a new implementation option that you wouldn't have in plain JS. I wonder if the name collision issue would become any more problematic in idiomatic JS code than it is with the C++ implementation. (I should add that I haven't personally tried this approach and I'm not sure how difficult it would be to implement in TypeScript.)

Personally I would rather avoid the name collision issue entirely and focus on a source transformation solution, but it's interesting to think about alternatives.

James O Coplien

unread,
Nov 8, 2022, 5:50:00 AM11/8/22
to noreply-spamdigest via object-composition


On 8 Nov 2022, at 11.44, Matthew Browne <mbro...@gmail.com> wrote:

Personally I would rather avoid the name collision issue entirely and focus on a source transformation solution, but it's interesting to think about alternatives.

I think that the very nature of DCI scoping make it impossible to altogether avoid this issue.

Matthew Browne

unread,
Nov 8, 2022, 6:20:45 AM11/8/22
to object-co...@googlegroups.com


I meant as the DCI programmer (user), not the author of the DCI implementation. Like how if I'm programming in trgve, I don't have to worry about choosing a role method name that won't conflict with something else.


Andreas Söderlund

unread,
Nov 9, 2022, 4:15:22 AM11/9/22
to object-composition
tisdag 8 november 2022 kl. 11:30:03 UTC+1 skrev Cope:
Maybe a bit more on name collisions:

I can detect almost all name collisions at compile time. The one exception is when an object plays multiple roles. I can catch that, too, but it is a run-time warning. Name collisions are never fatal (I think).

Why isn't it possible to get the object information at compile-time in that case?
 
For those I can catch at compile time, I choose “the most obvious choice” and issue a warning.

I see that in tests/roletest3.k it's a compile error to have the same RoleMethod name as in the object. Signature mismatches seems to be checked only afterwards, as a warning.

After all the experimentation over the years, my experience is that RoleMethod naming collisions are either some mismatch with the mental model of the RolePlayer (its contract), or an inclination to use the Role as a data accessor.

In the first case, if a RolePlayer has the ability to manufacture, it's explicit in the contract, so it should give a hint that manufacturing is the result of the interaction in the Context, not a purpose in itself. Manufacturing shouldn't be recreated in the Context, it should only be called upon when it's time in the bigger picture of the Context. So in roletest3.k for example, Role1 is making something, and at a certain point in its scripts, it's time to actually manufacture what it's making.

Is this obvious already?

The second case is more of a convenience, especially when you use Contexts with MVC. If the Context is a View and the Roles are Models, it's convenient to output Model data by its properties directly, but accessing data isn't much of an interaction (thinking vs. doing), so a DCI Context may not be the best fit for displaying data with a View. But if it is, this is where naming collisions can either be convenient as a direct proxy, or if other Roles could directly access "const" contract scripts, that would avoid boilerplate code, but it seems to go against the nature of Roles...?

I cannot say I've found a decent solution to this. With a private var instead of a Role you'll get convenient access, so maybe an interface is better to access data. Any thoughts?

/Andreas

James O Coplien

unread,
Nov 9, 2022, 4:57:53 AM11/9/22
to noreply-spamdigest via object-composition


On 9 Nov 2022, at 10.15, Andreas Söderlund <gaz...@gmail.com> wrote:

tisdag 8 november 2022 kl. 11:30:03 UTC+1 skrev Cope:
Maybe a bit more on name collisions:

I can detect almost all name collisions at compile time. The one exception is when an object plays multiple roles. I can catch that, too, but it is a run-time warning. Name collisions are never fatal (I think).

Why isn't it possible to get the object information at compile-time in that case?

First, there are no objects at compile time, so I’m not sure the question is well-formed.

An object can play roles in multiple contexts which in theory could be separately  compiled. The compiler has no chance to detect, at compile time, how many roles an object might play. It has only the ability to detect what roles might (only possibly) be played by any object of a given class.

Because of Gödel’s theorem, this also applies in general to an object and its relationship to several roles within the same context.

I see that in tests/roletest3.k it's a compile error to have the same RoleMethod name as in the object. Signature mismatches seems to be checked only afterwards, as a warning.

There is mainly a bit of cleaning up to be done, but the main principle behind trygve is to bark whenever there is danger.

The overly strong error probably has longstanding roots in all of the silly arguments for and against overloading across subclassing boundaries, which are similar to the boundary between role (as parent) and role player (akin to a subclass).


I cannot say I've found a decent solution to this. With a private var instead of a Role you'll get convenient access, so maybe an interface is better to access data. Any thoughts?

I’m beginning to dislike context member data and am moving more to using role bindings. Accessing naked member data of a context opens the door to all the polymorphism probllems DCI was designed to solve.

As for MVC, using the trygve construct to pull data initerface signatures from the requires section into the public role interface perfectly communicates what you want to communicate about the data’s part (role) in the view / model contract.

Andreas Söderlund

unread,
Nov 9, 2022, 1:36:39 PM11/9/22
to object-co...@googlegroups.com
I’m beginning to dislike context member data and am moving more to using role bindings. Accessing naked member data of a context opens the door to all the polymorphism probllems DCI was designed to solve.

I agree about member data, I had some idea with "const" contract members always being accessible, to avoid some boilerplate code (and avoiding naming collisions).
 
As for MVC, using the trygve construct to pull data initerface signatures from the requires section into the public role interface perfectly communicates what you want to communicate about the data’s part (role) in the view / model contract.

But how to do that without name collisions? If the contract defines String email(), I cannot use email as the name of a RoleMethod.

It gets a bit more complicated when using role vectors, for example when printing a list. It's a bit of a long piece, so here's a link to it: https://pastecode.io/s/qfp84uw4

In that case it's tricky to access individual users, so I inverted the accessor logic by passing the data to an Output role. Does it make sense?

Btw, role vectors are a pretty cool, underrated feature. How did you come up with it? :)

James O Coplien

unread,
Nov 9, 2022, 2:39:17 PM11/9/22
to noreply-spamdigest via object-composition


On 9 Nov 2022, at 19.36, Andreas Söderlund <gaz...@gmail.com> wrote:

But how to do that without name collisions? If the contract defines String email(), I cannot use email as the name of a RoleMethod.

Then: don’t.

Creating names may be the hardest part of programming, but we’re up to it. Both of these APIs do different things; let the names reflect that. I think “email” is a terrible method name, created by someone who hates typing. What does it do? Read a message and give me its content? Give me someone’s email address? We can do better than this.

Andreas Söderlund

unread,
Nov 9, 2022, 3:38:50 PM11/9/22
to object-co...@googlegroups.com
Creating names may be the hardest part of programming, but we’re up to it. Both of these APIs do different things; let the names reflect that. I think “email” is a terrible method name, created by someone who hates typing. What does it do? Read a message and give me its content? Give me someone’s email address? We can do better than this.

Sure, but this may relate to the Data vs. Context distinction that Rune talked about. If I want to simply display data in a MVC context, and a Person data structure has an email, at least it should have a getter-like property for that, right? What to call it, and how to expose that data to Contexts? Is that purely a naming issue in a specific Context? I thought MVC had this sorted out already.

An alternative is maybe to avoid it being exposed at all, instead doing as in the UserList example I linked to: https://pastecode.io/s/qfp84uw4

James O Coplien

unread,
Nov 9, 2022, 4:09:48 PM11/9/22
to noreply-spamdigest via object-composition


On 9 Nov 2022, at 21.38, Andreas Söderlund <gaz...@gmail.com> wrote:

Sure, but this may relate to the Data vs. Context distinction that Rune talked about. If I want to simply display data in a MVC context, and a Person data structure has an email, at least it should have a getter-like property for that, right? What to call it, and how to expose that data to Contexts? Is that purely a naming issue in a specific Context? I thought MVC had this sorted out already.

An alternative is maybe to avoid it being exposed at all, instead doing as in the UserList example I linked to: https://pastecode.io/s/qfp84uw4

I think this is thinking inside the box.

Think more broadly. This is orthogonal to MVC, and, to some degree, reinterprets it. In pure DCI there really are no data; you can find a number of CS texts that say the same thing about OO. This is consistent with the recently expressed opinions here that there are no classes in DCI. And classic MVC — especially as you describe it here — is based on accessing the naked models, sans display knowledge.

So under DCI, a View instead should get information from Roles that it uses to update its image. Such a Role might have a method like “displayableTemperatureReading” that either forwards what is returned by the underlying “degrees” instance method of a dumb class object (MVC typically uses model methods instead of reading data directly, anyhow) or synthesizes a result for use by the View.

I think MVC was based on the assumption that you had a bunch of objects laying around that you wanted to display; that was in any case true back then. The goal was likely to be able to display them without putting display logic inside the model objects themselves. Naked data — even when accessed by an insulating instance method — kind of goes against the grain of where DCI has led us. The “D” in DCI is a shame. But DCI affords us a way to beef up the models that makes them a bit more display-savvy by wrapping them in suitable Roles. It’s still a non-invasive (to the Model objects) approach, but has the benefit of putting some of the View-relevant understanding closer to where it belongs. So if you’re going to do a DCI-inspired MVC I’d guess that you’d wrap the objects in Roles through which the View retrieves what it needs to know.That means creating a whole Context and Role architecture for the MVC setup, and I could well imagine that the API vocabulary at that level could be optimized for the goals of the View in consideration of end-user needs instead of nerd-think (“email,” indeed). There is nothing sacred about class instance method names as regards display mechanisms — and in fact they are more likely to suffer from some form of nerdness. So instead of “email” I’d say “immutableEndUserEmailAddress” or “subscriberCandidateEmailAddress” that better communicated some semantic relative to human interaction. Those are really suitable Role method names. That is, after all, the context — to coin a phrase — in which these computations figure.

Matthew Browne

unread,
Nov 9, 2022, 10:48:41 PM11/9/22
to object-co...@googlegroups.com, James O Coplien

FWIW, I've done a few experiments using contexts and roles to implement MVC, and I never came up with anything that I felt was much better than using standard classes for that. I noticed that Trygve's Prokon example also mainly sticks to regular classes for the UI part of the app. I wonder if this is because MVC (or at least the V and the C) is more of an "atomic event architecture" to use a term from Cope's Lean Architecture book, rather than the type of system (or part of a system) with more complex data/role mapping. I realize that you certainly can have a data object play the role of a View, I just haven't found it to be very useful in practice, nor have I seen any sizeable/realistic examples of this. Side-note: the context itself would sort of become the controller.

(I don't think this goes against what I was saying earlier about having only contexts and no classes; you could substitute "classes" with "context declarations with no roles" in the above paragraph for the same essential concept.)

But perhaps I'm missing something?

--
You received this message because you are subscribed to the Google Groups "object-composition" group.
To unsubscribe from this group and stop receiving emails from it, send an email to object-composit...@googlegroups.com.

Matthew Browne

unread,
Nov 20, 2022, 9:47:47 AM11/20/22
to object-co...@googlegroups.com

Hi Andreas,
I was thinking about the code examples in the tutorial, and some alternative syntax options that might be worth considering. I'm not sure if these are an improvement or not, but they feel more role-like to me even though of course they don't get any closer to a unified data/role object.

Option 1:

function HelloWorld(
Speaker: { phrase: string },
World: { log: (msg: unknown) => void }
) {
const roles = {
Speaker: {
proclaim() {
roles.World.note(Speaker.phrase)
},
},
World: {
note(phrase: string) {
World.log(phrase)
},
},
}
roles.Speaker.proclaim()
}

Option 2:

function HelloWorld(
Speaker: { phrase: string },
World: { log: (msg: unknown) => void }
) {
const SpeakerRole = {
proclaim() {
WorldRole.note(Speaker.phrase)
},
}
const WorldRole = {
note(phrase: string) {
World.log(phrase)
},
}
SpeakerRole.proclaim()
}


I'm not sure if making the roles into objects would make it more likely for the programmer to try to use this and become confused by it, but we should keep in mind that this doesn't work with the current examples in the tutorial anyway. It could be made to work by using .call() or .apply() when calling role methods, but that would best be done by a real transpiler, so I totally understand and agree with the decision not to do that to keep things simpler for the programmer.

One thing I do prefer about the current technique in the tutorial—and maybe the tutorial could be updated to take advantage of this—is that you could use function hoisting so that you don't have to scroll past all the roles in order to see the method call that kicks off the Context, like this:

function HelloWorld(
Speaker: { phrase: string },
World: { log: (msg: unknown) => void }
) {
Speaker_proclaim()
function Speaker_proclaim() {
World_note(Speaker.phrase)
}
function World_note(phrase: string) {
World.log(phrase)
}
}


Just some food for thought.

Congrats on writing such a thorough tutorial, with four parts so far!

-Matt

Reply all
Reply to author
Forward
0 new messages