www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Why does D not have generics?

reply Q. Schroll <qs.il.paperinik gmail.com> writes:
I don't want to be mistaken. I know what the differences of 

Maybe my question has obvious answers and I just don't see them.

The only programming language I know of that has both, templates 
and generics, is C++/CLI and ones closely related.


have generics. Templates can be used in some use cases, templates 
have their specific use cases (like DbI). What seems to never 
mentioned is that generics have their specific use cases, too.

What I may be missing, is how Java-esce generics can be 
implemented using D's templates (without much effort). What I'd 
expect from generics is this:

A generic class or interface states its requirements (base 
classes, interfaces, [never seen in the wild:] subclasses, ...) 
to its type parameters exactly. Everything that is part of the 
implementation is checked when the generic aggregate is defined. 
It is not necessary to instantiate a rainbow of types to be sure 
the implementation is valid for every kind of type parameter the 
programmer intends to support.

Generics allow for implicit conversion by covariance and 

to its covariant or contravariant part-interface (that is a 
supertype of the interface, really). (Java [2, 3])

Generics are really great for specifying the expectations of your 
type parameters' requirements and letting the compiler make sure 
the requirements suffice to allow for the stuff you want to work.
Using templates, the compiler checks requirements only for 
specific instances. It might be that the requirements are 
insufficient, but because no test tried the potentially very 
specific type argument, it will be unrecognized.

Also, one feature D doesn't have, is expressing the precise union 
or intersection of interfaces: Say you have two interfaces I and 
J. The interface (I & J) has all the methods specified by any of 
them. A class that implements I and J automatically implements (I 
& J). The interface (I | J) has all the methods specified by both 
of them. A class that implements I or implements J automatically 
implements (I | J). If you e.g. iterate an (I | J)[] object, you 
can call any method required by both interfaces. (Typescript [4])
It might be hard or even impossible to implement this using 
vtables and stuff.

In D, one can easily create templates intersectInterface(Is...) 
and unionInterface(Is...) that basically do that.

It could very well be that D doesn't have them because they have 
to be implemented and maintained, and the cost/benefit ratio 
wasn't good enough. Maybe they can be implemented in a library 
rather easily. I know that me failing to do that doesn't prove 
it's impossible or even hard.

[1] 
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/covariance-contravariance/variance-in-generic-interfaces
[2] 
https://docs.oracle.com/javase/tutorial/java/generics/lowerBounded.html
[3] 
https://docs.oracle.com/javase/tutorial/java/generics/upperBounded.html
[4] 
https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html?ref=hackernoon.com
Jan 11 2021
next sibling parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Monday, 11 January 2021 at 18:23:20 UTC, Q. Schroll wrote:
 I don't want to be mistaken. I know what the differences of 

 are. Maybe my question has obvious answers and I just don't see 
 them.

 The only programming language I know of that has both, 
 templates and generics, is C++/CLI and ones closely related.
I think you are really talking about generics in Java and is interpreting the semantic landscape through that lense? There are many variations: https://en.wikipedia.org/wiki/Generic_programming C++ "concepts" is to a large extent syntactical sugar that makes it easier to specify interfaces... so I don't think it has a significant semantic impact. Mostly a usability impact.
Jan 11 2021
parent reply Q. Schroll <qs.il.paperinik gmail.com> writes:
On Monday, 11 January 2021 at 18:30:19 UTC, Ola Fosheim Grøstad 
wrote:
 On Monday, 11 January 2021 at 18:23:20 UTC, Q. Schroll wrote:
 I don't want to be mistaken. I know what the differences of 

 are. Maybe my question has obvious answers and I just don't 
 see them.

 The only programming language I know of that has both, 
 templates and generics, is C++/CLI and ones closely related.
I think you are really talking about generics in Java and is interpreting the semantic landscape through that lense?
Yes. But I'm not saying: Do exactly as Java does. I'm saying: Java has this very interesting concept that D might want to learn, too.
 C++ "concepts" is to a large extent syntactical sugar that 
 makes it easier to specify interfaces... so I don't think it 
 has a significant semantic impact. Mostly a usability impact.
You might have mistaken me. C++ has no generics in the sense I meant. Generics to me means not that List<A> and List<B> result in one List type; really, I care about much more semantics than digress, because I don't care. type (which you can and entails a template-like implementation under the hood), the compiler makes sure that everything in your generic type is semantically valid for every type argument satisfying the conditions you state. For example, if you call T.method(), but no constraint says that T.method() exists, the compiler rejects the code. On the other hand, in C++ or D, if you call T.method() in a template and only test types that happen to have method(), everything is good between friends (Andrei's quote). I'm not arguing you *always* want static checks, obviously DbI hardly works that way. All I'm saying is, the big thing generics would bring to D is **statically ensuring** that the requirements put on type parameters suffice for the implementation to be valid.
Jan 12 2021
parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Tuesday, 12 January 2021 at 16:57:43 UTC, Q. Schroll wrote:
 Yes. But I'm not saying: Do exactly as Java does. I'm saying: 
 Java has this very interesting concept that D might want to 
 learn, too.
Right. D has to chose something that blends well with what is already there. :-) But I actually think that the semantics for this are there, but the syntax is lacking, but maybe I misunderstand what you want.
 For example, if you call T.method(), but no constraint says 
 that T.method() exists, the compiler rejects the code. On the 
 other hand, in C++ or D, if you call T.method() in a template 
 and only test types that happen to have method(), everything is 
 good between friends (Andrei's quote).
But I don't quite see how this is different from C++ concepts, which basically is better syntax for testing traits of types provided through parameters/methods.
 I'm not arguing you *always* want static checks, obviously DbI 
 hardly works that way. All I'm saying is, the big thing 
 generics would bring to D is **statically ensuring** that the 
 requirements put on type parameters suffice for the 
 implementation to be valid.
Yes, I certainly agree that this is desirable. I just don't understand what semantics are missing. I think D has this? Just not the syntax?
Jan 12 2021
parent reply Paul Backus <snarwin gmail.com> writes:
On Tuesday, 12 January 2021 at 17:12:11 UTC, Ola Fosheim Grøstad 
wrote:
 On Tuesday, 12 January 2021 at 16:57:43 UTC, Q. Schroll wrote:
 For example, if you call T.method(), but no constraint says 
 that T.method() exists, the compiler rejects the code. On the 
 other hand, in C++ or D, if you call T.method() in a template 
 and only test types that happen to have method(), everything 
 is good between friends (Andrei's quote).
But I don't quite see how this is different from C++ concepts, which basically is better syntax for testing traits of types provided through parameters/methods.
C++ concepts, as far as I'm aware, don't cause templates to be checked for conformance prior to instantiation--which is what's being asked for here. For that, you'd need something more like Rust's traits.
Jan 12 2021
parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Tuesday, 12 January 2021 at 17:16:19 UTC, Paul Backus wrote:
 C++ concepts, as far as I'm aware, don't cause templates to be 
 checked for conformance prior to instantiation--which is what's 
 being asked for here. For that, you'd need something more like 
 Rust's traits.
Not sure what you mean? C++ use SFINAE. If it fails it will look elsewhere.
Jan 12 2021
parent reply Paul Backus <snarwin gmail.com> writes:
On Tuesday, 12 January 2021 at 17:17:40 UTC, Ola Fosheim Grøstad 
wrote:
 On Tuesday, 12 January 2021 at 17:16:19 UTC, Paul Backus wrote:
 C++ concepts, as far as I'm aware, don't cause templates to be 
 checked for conformance prior to instantiation--which is 
 what's being asked for here. For that, you'd need something 
 more like Rust's traits.
Not sure what you mean? C++ use SFINAE. If it fails it will look elsewhere.
As far as I'm aware, C++ does not prevent you from writing code like this: template<typename T> concept MyConcept = requires (T t) { T.foo(); }; template<MyConcept T> void fun(T t) { t.bar(); } In Java or Rust, the above would be a compile-time error.
Jan 12 2021
next sibling parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Tuesday, 12 January 2021 at 17:32:17 UTC, Paul Backus wrote:
 template<MyConcept T>
 void fun(T t) { t.bar(); }

 In Java or Rust, the above would be a compile-time error.
Ok, I get what you say. Concepts is more for library implementors than applications, so it is assumed the library implementor knows what he is doing, and the focus is on putting restrictions on library usage. But you can do that in C++ to some extent. You can remove members from a declaration, even though they are present in the definition. Thanks to header files and reinterpret casting. There is a lot of stuff you can do in C++ (but not really recommended).
Jan 12 2021
parent reply Paul Backus <snarwin gmail.com> writes:
On Tuesday, 12 January 2021 at 17:43:39 UTC, Ola Fosheim Grøstad 
wrote:
 But you can do that in C++ to some extent. You can remove 
 members from a declaration, even though they are present in the 
 definition. Thanks to header files and reinterpret casting. 
 There is a lot of stuff you can do in C++ (but not really 
 recommended).
You can do it in D too, if you use an interface or a type-erasure library like Atila's tardy. But it requires additional runtime overhead compared to the loosely-checked template version. In Rust, on the other hand, you get strict type checking of generic code with zero overhead at runtime.
Jan 12 2021
parent reply Q. Schroll <qs.il.paperinik gmail.com> writes:
On Tuesday, 12 January 2021 at 17:52:16 UTC, Paul Backus wrote:
 You can do it in D too, if you use an interface or a 
 type-erasure library like Atila's tardy. But it requires 
 additional runtime overhead compared to the loosely-checked 
 template version. In Rust, on the other hand, you get strict 
 type checking of generic code with zero overhead at runtime.
I'll have a closer look at Tardy. I previously misunderstood it to be a different language and not a DUB package. Thank you.
Jan 12 2021
parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
So, this is one, perhaps clumsy way of doing it:


import std.stdio;

struct StackConcept(T,E) {
     alias self_type = T;
     alias element_type = E;
	T* self;
     this(T* obj){ self = obj; }
     pragma(inline,true) void push(E x){ self.push(x); }
     pragma(inline,true) E pop(){ return self.pop(); }
}

template is_StackConcept(T) {
     enum bool is_StackConcept = 
is(StackConcept!(T.self_type,T.element_type)==T);
}

struct intstack {
     int[] arr = [];
     void push(int x){ arr.length++; arr[$-1] = x; }
     int pop(){ auto tmp=arr[$-1]; arr.length--; return tmp; }

     auto as_StackConcept(){
     	return StackConcept!(intstack,int)(&this);
	}
}

auto pop_second(T)(T* obj){
     auto stack = obj.as_StackConcept();
     static assert (is_StackConcept!(typeof(stack)));
	auto tmp = stack.pop();
     auto tmp2 = stack.pop();
     stack.push(tmp);
     return tmp2;
}

void main()
{
     intstack stack;
     stack.push(1);
     stack.push(2);
     stack.push(3);
     pop_second(&stack);
     writeln(stack.pop());
     writeln(stack.pop());
}
Jan 12 2021
next sibling parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Tuesday, 12 January 2021 at 21:32:46 UTC, Ola Fosheim Grøstad 
wrote:
 auto pop_second(T)(T* obj){
     auto stack = obj.as_StackConcept();
     static assert (is_StackConcept!(typeof(stack)));
 	auto tmp = stack.pop();
     auto tmp2 = stack.pop();
     stack.push(tmp);
     return tmp2;
 }
An improvement is to define a helper function: auto as_stack(T)(T* obj){ auto stack = obj.as_StackConcept(); static assert (is_StackConcept!(typeof(stack))); return stack; } Then you can just do: auto pop_second(T)(T* obj){ auto stack = obj.as_stack(); auto tmp = stack.pop(); auto tmp2 = stack.pop(); stack.push(tmp); return tmp2; } Or: auto pop_second(T)(T* obj){ auto tmp = obj.as_stack.pop(); auto tmp2 = obj.as_stack.pop(); obj.as_stack.push(tmp); return tmp2; }
Jan 12 2021
prev sibling parent reply Paul Backus <snarwin gmail.com> writes:
On Tuesday, 12 January 2021 at 21:32:46 UTC, Ola Fosheim Grøstad 
wrote:
 So, this is one, perhaps clumsy way of doing it:
[...]
 auto pop_second(T)(T* obj){
     auto stack = obj.as_StackConcept();
     static assert (is_StackConcept!(typeof(stack)));
 	auto tmp = stack.pop();
     auto tmp2 = stack.pop();
     stack.push(tmp);
     return tmp2;
 }
You have written a generic function that accepts any type (unconstrained `T`), but fails to instantiate for most of them (e.g. `pop_second!int` would not compile). In a language like Rust or Java with type-checked generics, the compiler would reject this function definition.
Jan 12 2021
next sibling parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Tuesday, 12 January 2021 at 22:23:51 UTC, Paul Backus wrote:
 You have written a generic function that accepts any type 
 (unconstrained `T`), but fails to instantiate for most of them 
 (e.g. `pop_second!int` would not compile). In a language like 
 Rust or Java with type-checked generics, the compiler would 
 reject this function definition.
Hm? pop_second requires StackConcept!(T,E) ?
Jan 12 2021
parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Tuesday, 12 January 2021 at 22:29:09 UTC, Ola Fosheim Grøstad 
wrote:
 On Tuesday, 12 January 2021 at 22:23:51 UTC, Paul Backus wrote:
 You have written a generic function that accepts any type 
 (unconstrained `T`), but fails to instantiate for most of them 
 (e.g. `pop_second!int` would not compile). In a language like 
 Rust or Java with type-checked generics, the compiler would 
 reject this function definition.
Hm? pop_second requires StackConcept!(T,E) ?
Works fine for me: void push(int* x, bool b){ *x = (*x<<1)|b; } bool pop(int* x){ bool tmp = *x&1; *x=*x>>1; return tmp; } auto as_StackConcept(int* x){ return StackConcept!(int,bool)(x);} void main() { int stack = 5; pop_second(&stack); writeln(pop(&stack)); writeln(pop(&stack)); }
Jan 12 2021
next sibling parent reply jmh530 <john.michael.hall gmail.com> writes:
On Tuesday, 12 January 2021 at 22:41:03 UTC, Ola Fosheim Grøstad 
wrote:
 On Tuesday, 12 January 2021 at 22:29:09 UTC, Ola Fosheim 
 Grøstad wrote:
 On Tuesday, 12 January 2021 at 22:23:51 UTC, Paul Backus wrote:
 You have written a generic function that accepts any type 
 (unconstrained `T`), but fails to instantiate for most of 
 them (e.g. `pop_second!int` would not compile). In a language 
 like Rust or Java with type-checked generics, the compiler 
 would reject this function definition.
Hm? pop_second requires StackConcept!(T,E) ?
Works fine for me: void push(int* x, bool b){ *x = (*x<<1)|b; } bool pop(int* x){ bool tmp = *x&1; *x=*x>>1; return tmp; } auto as_StackConcept(int* x){ return StackConcept!(int,bool)(x);} void main() { int stack = 5; pop_second(&stack); writeln(pop(&stack)); writeln(pop(&stack)); }
What about auto pop_second(T)(T* obj) if (__traits(compiles, { T input = T.init; auto stack = input.as_StackConcept(); })) { ... }
Jan 12 2021
parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Tuesday, 12 January 2021 at 22:53:39 UTC, jmh530 wrote:
 What about

 auto pop_second(T)(T* obj)
     if (__traits(compiles, {
             T input = T.init;
             auto stack = input.as_StackConcept();
         }))
 {
     ...
 }
Yes, something like that! Except we wrap it up as hasStackConcept!T. So then you can use the constraint "if(hasStackConcept!T)"
Jan 12 2021
prev sibling parent reply Paul Backus <snarwin gmail.com> writes:
On Tuesday, 12 January 2021 at 22:41:03 UTC, Ola Fosheim Grøstad 
wrote:
 On Tuesday, 12 January 2021 at 22:29:09 UTC, Ola Fosheim 
 Grøstad wrote:
 On Tuesday, 12 January 2021 at 22:23:51 UTC, Paul Backus wrote:
 You have written a generic function that accepts any type 
 (unconstrained `T`), but fails to instantiate for most of 
 them (e.g. `pop_second!int` would not compile). In a language 
 like Rust or Java with type-checked generics, the compiler 
 would reject this function definition.
Hm? pop_second requires StackConcept!(T,E) ?
Works fine for me: [...]
The goal is to make it produce a compile-time error given only the *definition* of `pop_second`--so, no main function, no unit tests, and no other usage code. This is impossible to do in D (or C++) if `pop_second` is a template, because D (like C++) does not type-check uninstantiated templates.
Jan 12 2021
parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Tuesday, 12 January 2021 at 23:10:30 UTC, Paul Backus wrote:
 The goal is to make it produce a compile-time error given only 
 the *definition* of `pop_second`--so, no main function, no unit 
 tests, and no other usage code.
That's a bit pedantic. And it can never work because of static foreach.
Jan 12 2021
parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Tuesday, 12 January 2021 at 23:27:22 UTC, Ola Fosheim Grøstad 
wrote:
 That's a bit pedantic. And it can never work because of static 
 foreach.
Well, I guess it could work, but it would be very challenging when you combine static foreach, static if, string mixins and recursive templates. At some point you end up with an automatic theorem prover.
Jan 12 2021
prev sibling parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Tuesday, 12 January 2021 at 22:23:51 UTC, Paul Backus wrote:
 (e.g. `pop_second!int` would not compile). In a language like 
 Rust or Java with type-checked generics, the compiler would 
 reject this function definition.
Maybe you want it to just bypass it to allow overloading? Then you just test for the presence of as_StackConcept as a constraint in the signature?
Jan 12 2021
prev sibling parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Tuesday, 12 January 2021 at 17:32:17 UTC, Paul Backus wrote:
 As far as I'm aware, C++ does not prevent you from writing code 
 like this:

 template<typename T>
 concept MyConcept = requires (T t) { T.foo(); };

 template<MyConcept T>
 void fun(T t) { t.bar(); }

 In Java or Rust, the above would be a compile-time error.
Btw, when I really want stuff like this in C++ I create a class with a reference to the original class. So then the interface is a wrapper-class and you gain access to it through an overloaded function with a "agreed upon name". E.g. someobject.protected_access.doit() where "protected_access" is an overloaded function that returns a struct with a reference to someobject and the doit() interface.
Jan 12 2021
parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Tuesday, 12 January 2021 at 17:53:26 UTC, Ola Fosheim Grøstad 
wrote:
 someobject.protected_access.doit()
In C++ it would obviously be: protected_access(someobject).doit() :)
Jan 12 2021
prev sibling next sibling parent reply Dukc <ajieskola gmail.com> writes:
On Monday, 11 January 2021 at 18:23:20 UTC, Q. Schroll wrote:
 [snip]
I recommend you check out Atila's Github profile. I think that he wrote something to address the shortcomings of D template constraints. Also considering your way to think about design, I think there's a good chance you're going to like Tardy and/or Mirror. Keep in mind though that as Mirror isn't yet officially announced, Atila probably does not consider it quite production-ready yet.
Jan 12 2021
parent reply Petar Kirov [ZombineDev] <petar.p.kirov gmail.com> writes:
On Tuesday, 12 January 2021 at 12:25:31 UTC, Dukc wrote:
 On Monday, 11 January 2021 at 18:23:20 UTC, Q. Schroll wrote:
 [snip]
I recommend you check out Atila's Github profile. I think that he wrote something to address the shortcomings of D template constraints. Also considering your way to think about design, I think there's a good chance you're going to like Tardy and/or Mirror. Keep in mind though that as Mirror isn't yet officially announced, Atila probably does not consider it quite production-ready yet.
Generics is a pure type system feature and no library can emulate it perfectly. As mentioned in my other post, by using D meta programming to implement things like Tardy, even in the best case we still can end up with different instances of functions, sometimes containing the same machine code, just because of a difference between template arguments. Yes, linkers can sometimes remove identical code, but why pay the frontend + backend compilation time cost?
Jan 12 2021
parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Tuesday, 12 January 2021 at 13:25:36 UTC, Petar Kirov 
[ZombineDev] wrote:
 Generics is a pure type system feature and no library can 
 emulate it perfectly.
The term "generics" just means parametric types. I think you guys
Jan 12 2021
parent reply Adam D. Ruppe <destructionator gmail.com> writes:
On Tuesday, 12 January 2021 at 13:36:09 UTC, Ola Fosheim Grøstad 
wrote:
 I think you guys have something more specific in mind based on 

Yeah, in the D context since we obviously have generic templates, I'd take it to mean the Java style. A Java generic doesn't generate new code for other types at all. It is just a runtime class that works in terms of interfaces, and when the compiler parameterizes it, it basically just inserts static casts at the interface boundary for you. The benefit of this is you have almost zero compile time cost and runtime cost comparable to any other class. It avoids template bloat in codegen that can be very significant in D. I'd love to have it as an option in D as well. There's a lot of types that can use identical runtime code and just change types. Not just classes, but like integer types too can be identical and merged, const/immutable/shared/etc can be identical and merged, and even other things with cast(void*) can do it. Lots of potential for use inside druntime itself, changing the existing void* + TypeInfo pairs into templates can mean no more annoying typeinfo requirement... but then it causes runtime bloat. So we probably actually do want to merge, and Java-style generics are a very good way to do it. We could like, just for example _d_arrayappend(ref T[] arr, E ele) forward back to the same implementation functions with pointers, static if branching on special requirements like postblit, just passing the exact parameters it needs to make it work instead of typeinfo... but then having no additional codegen, no additional symbols, no additional indirection, by using the generics. Doing this in today's D comes close with inlined templates but it is still far more expensive than it has to be and really annoying to try to describe. A sum type definition in the template that actually merges would surely help.
Jan 12 2021
next sibling parent jmh530 <john.michael.hall gmail.com> writes:
On Tuesday, 12 January 2021 at 14:45:36 UTC, Adam D. Ruppe wrote:
 [snip]

 Yeah, in the D context since we obviously have generic 
 templates, I'd take it to mean the Java style.

 A Java generic doesn't generate new code for other types at 
 all. It is just a runtime class that works in terms of 
 interfaces, and when the compiler parameterizes it, it 
 basically just inserts static casts at the interface boundary 
 for you.

 The benefit of this is you have almost zero compile time cost 
 and runtime cost comparable to any other class. It avoids 
 template bloat in codegen that can be very significant in D.

 I'd love to have it as an option in D as well. There's a lot of 
 types that can use identical runtime code and just change 
 types. Not just classes, but like integer types too can be 
 identical and merged, const/immutable/shared/etc can be 
 identical and merged, and even other things with cast(void*) 
 can do it.
 [snip]
Not just classes? Could you explain a bit more about what you are thinking in that paragraph? If it's not a class, then are you talking about a struct? As in something like below. Does the same approach work when it's not a class? generic Number(T) if (isNumber!T) { struct Number { T x; } }
Jan 12 2021
prev sibling next sibling parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Tuesday, 12 January 2021 at 14:45:36 UTC, Adam D. Ruppe wrote:
 The benefit of this is you have almost zero compile time cost 
 and runtime cost comparable to any other class. It avoids 
 template bloat in codegen that can be very significant in D.
I haven't looked very closely at Phobos, but it seems like it is a bit compile time heavy ("over complicated") compared to what is most useful ("most common usage"). Perhaps simpler library type semantics with less compile time load would help. Regarding code gen, I thought LDC cached code gen and would avoid emitting the same code twice? If that does not work, then maybe one can bring the IR to a normal form such that more code will be recognized as equivalent?
 I'd love to have it as an option in D as well. There's a lot of 
 types that can use identical runtime code and just change 
 types. Not just classes, but like integer types too can be 
 identical and merged, const/immutable/shared/etc can be 
 identical and merged, and even other things with cast(void*) 
 can do it.
But we probably could fix this by designing libraries with reinterpret casting through (void*) if desired. The template itself does not generate any code. I imagine we could sketch up an alternative std lib based on the most commonly used semantics that Phobos provides. It should of course work alongside Phobos so that people can transition/choose. It does not sound like we need Java-style generics, to me, but I could be wrong. Maybe your own lib could be a starting point for such an effort?
 Lots of potential for use inside druntime itself, changing the 
 existing void* + TypeInfo pairs into templates can mean no more 
 annoying typeinfo requirement... but then it causes runtime 
 bloat.
Ideally the runtime should not contain anything that not every D program uses... Maybe some kind of auto-import would make it possible to slim down the runtime to basically nothing. Basically some kind of feature that lets certain syntax trigger an import in application code (but not in library code). You could config this for the project in a config file and basically choose e.g. what kind of string interpolation semantics you want for you application.
 Doing this in today's D comes close with inlined templates but 
 it is still far more expensive than it has to be and really 
 annoying to try to describe. A sum type definition in the 
 template that actually merges would surely help.
Not exactly sure what you meant by merging here? But if D is going to continue being GC based then a discriminating union type is needed, so that is an area that requires some rethinking (sum types, unions). (I think the only thing that prevents fully precise collection is unions with traced pointers? And precise collection is needed for reliable GC based destruction/finalization.)
Jan 12 2021
prev sibling next sibling parent reply Elronnd <elronnd elronnd.net> writes:
On Tuesday, 12 January 2021 at 14:45:36 UTC, Adam D. Ruppe wrote:
 A Java generic doesn't generate new code for other types at 
 all. It is just a runtime class that works in terms of 
 interfaces, and when the compiler parameterizes it, it 
 basically just inserts static casts at the interface boundary 
 for you.

 The benefit of this is you have almost zero compile time cost 
 and runtime cost comparable to any other class. It avoids 
 template bloat in codegen that can be very significant in D.

 I'd love to have it as an option in D as well. There's a lot of 
 types that can use identical runtime code and just change 
 types. Not just classes, but like integer types too can be 
 identical and merged, const/immutable/shared/etc can be 
 identical and merged, and even other things with cast(void*) 
 can do it.
There was a recent paper about doing this kind of transformation automatically as an optimization - https://www.microsoft.com/en-us/research/uploads/prod/2020/03/kacc.pdf
Jan 13 2021
next sibling parent Bruce Carneal <bcarneal gmail.com> writes:
On Wednesday, 13 January 2021 at 08:19:25 UTC, Elronnd wrote:
 There was a recent paper about doing this kind of 
 transformation automatically as an optimization - 
 https://www.microsoft.com/en-us/research/uploads/prod/2020/03/kacc.pdf
Thanks for the link. The paper looks like a major advance, one that should strongly influence future compiler architectures.
Jan 13 2021
prev sibling parent Imperatorn <johan_forsberg_86 hotmail.com> writes:
On Wednesday, 13 January 2021 at 08:19:25 UTC, Elronnd wrote:
 On Tuesday, 12 January 2021 at 14:45:36 UTC, Adam D. Ruppe 
 wrote:
 [...]
There was a recent paper about doing this kind of transformation automatically as an optimization - https://www.microsoft.com/en-us/research/uploads/prod/2020/03/kacc.pdf
Nice paper đŸ“„ (Well, the contents of the paper. Which is not actual paper.. *IQ increases*)
Jan 13 2021
prev sibling parent reply Guillaume Piolat <first.last gmail.com> writes:
On Tuesday, 12 January 2021 at 14:45:36 UTC, Adam D. Ruppe wrote:
 The benefit of this is you have almost zero compile time cost 
 and runtime cost comparable to any other class. It avoids 
 template bloat in codegen that can be very significant in D.

 I'd love to have it as an option in D as well. There's a lot of 
 types that can use identical runtime code and just change 
 types. Not just classes, but like integer types too can be 
 identical and merged, const/immutable/shared/etc can be 
 identical and merged, and even other things with cast(void*) 
 can do it.
A practice I've heard from game industry people was to instantiate template with void* to avoid duplicate template instantiation, and then cast at boundaries. Typically for containers.
Jan 13 2021
parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Wednesday, 13 January 2021 at 08:33:58 UTC, Guillaume Piolat 
wrote:
 A practice I've heard from game industry people was to 
 instantiate template with void* to avoid duplicate template 
 instantiation, and then cast at boundaries. Typically for 
 containers.
Yes, that is what I suggested also. I implemented it yesterday for deque, stack, etc, all using the same memory layout for 64 bit values (pointers, ints, doubles etc). What makes it cool is that you can construct datastructures that use the same ranges code, despite having different types. However, it cannot be done without very negative effects on precise collection, I think. So it is a bad fit for D, since it is GC based, and precise collection is the future... So, trashed it. (I translated it to C++ instead... :-P)
Jan 15 2021
parent reply Paulo Pinto <pjmlp progtools.org> writes:
On Friday, 15 January 2021 at 11:58:13 UTC, Ola Fosheim Grøstad 
wrote:
 On Wednesday, 13 January 2021 at 08:33:58 UTC, Guillaume Piolat 
 wrote:
 [...]
Yes, that is what I suggested also. I implemented it yesterday for deque, stack, etc, all using the same memory layout for 64 bit values (pointers, ints, doubles etc). What makes it cool is that you can construct datastructures that use the same ranges code, despite having different types. However, it cannot be done without very negative effects on precise collection, I think. So it is a bad fit for D, since it is GC based, and precise collection is the future... So, trashed it. (I translated it to C++ instead... :-P)
If it is instantiated T = void* then it would only be safe to use with pointers anyway, thus using T = object would be an option.
Jan 15 2021
parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Friday, 15 January 2021 at 17:25:59 UTC, Paulo Pinto wrote:
 If it is instantiated T = void* then it would only be safe to 
 use with pointers anyway, thus using T = object would be an 
 option.
That's a minor detail though. Overall, as long at D is based on GC I guess type erasure should be done by the compiler for traceable pointers. Maybe I'll try again for pointer-free libraries. Basically treating sequences of bytes as values. For instance, you don't really need to compare keys as double, int or chars. You might as well compare them as 4 byte, 8 byte etc sequences. So for a B+tree you could use the same implementation for all kinds of key types.
Jan 15 2021
parent reply Bruce Carneal <bcarneal gmail.com> writes:
On Friday, 15 January 2021 at 17:36:40 UTC, Ola Fosheim Grøstad 
wrote:
 On Friday, 15 January 2021 at 17:25:59 UTC, Paulo Pinto wrote:
 If it is instantiated T = void* then it would only be safe to 
 use with pointers anyway, thus using T = object would be an 
 option.
That's a minor detail though. Overall, as long at D is based on GC I guess type erasure should be done by the compiler for traceable pointers. Maybe I'll try again for pointer-free libraries. Basically treating sequences of bytes as values. For instance, you don't really need to compare keys as double, int or chars. You might as well compare them as 4 byte, 8 byte etc sequences. So for a B+tree you could use the same implementation for all kinds of key types.
Type erasure can be tricky, even when it is restricted to basic value types of the same size. This shows up when implementing radix sort where one solution is to map to/from whole numbers (NaN semantics being ignored). That said, good luck on your explorations. Meta programming bloat is vulnerable and deserves to be taken down a peg or three.
Jan 15 2021
next sibling parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Friday, 15 January 2021 at 18:11:04 UTC, Bruce Carneal wrote:
 Type erasure can be tricky, even when it is restricted to basic 
 value types of the same size.  This shows up when implementing 
 radix sort where one solution is to map to/from whole numbers 
 (NaN semantics being ignored).
Yes, I think maybe it can work if one cannot retrieve the key. Then one can store it in a coded fashion that sorts correctly as byte sequences?
Jan 15 2021
parent reply Bruce Carneal <bcarneal gmail.com> writes:
On Friday, 15 January 2021 at 18:14:22 UTC, Ola Fosheim Grøstad 
wrote:
 On Friday, 15 January 2021 at 18:11:04 UTC, Bruce Carneal wrote:
 Type erasure can be tricky, even when it is restricted to 
 basic value types of the same size.  This shows up when 
 implementing radix sort where one solution is to map to/from 
 whole numbers (NaN semantics being ignored).
Yes, I think maybe it can work if one cannot retrieve the key. Then one can store it in a coded fashion that sorts correctly as byte sequences?
You can transform to a canonical relop form wherever you'd like, of course, but mapping on entry and unmapping on retrieval or exit from a collection library would, as you might imagine, lower conversion overhead for the second and subsequent relops, reduce bloat, and enable kernel fusion. All of my use cases for this relop type erasure to date have involved fused operations where the overhead of an additional store/load out of an intermediate collection would have been very difficult to swallow. But if you're looking at something less predictable I'm guessing that type erased collections could be a win. If you decide to pursue it, a report on the good, bad and ugly would be appreciated. Good luck.
Jan 15 2021
parent reply Ola Fosheim Grostad <ola.fosheim.grostad gmail.com> writes:
On Saturday, 16 January 2021 at 00:03:00 UTC, Bruce Carneal wrote:
 You can transform to a canonical relop form wherever you'd 
 like, of course, but mapping on entry and unmapping on 
 retrieval or exit from a collection library would, as you might 
 imagine, lower conversion overhead for the second and 
 subsequent relops, reduce bloat, and enable kernel fusion.
The problem is little endian. Ascii strings will have to be reversed. The order of two 32 bit ints have to be reversed. These issues are not present for big endian...
Jan 15 2021
parent reply Bruce Carneal <bcarneal gmail.com> writes:
On Saturday, 16 January 2021 at 05:39:02 UTC, Ola Fosheim Grostad 
wrote:
 On Saturday, 16 January 2021 at 00:03:00 UTC, Bruce Carneal 
 wrote:
 You can transform to a canonical relop form wherever you'd 
 like, of course, but mapping on entry and unmapping on 
 retrieval or exit from a collection library would, as you 
 might imagine, lower conversion overhead for the second and 
 subsequent relops, reduce bloat, and enable kernel fusion.
The problem is little endian. Ascii strings will have to be reversed. The order of two 32 bit ints have to be reversed. These issues are not present for big endian...
It sounds like mappings to arbitrary precision big endian unsigned is where you're headed currently. Implementing relop-unifying xforms to whatever form you actually choose should not be difficult. You have the types in hand so you're looking at a specialized binary serialization problem. Coming back out again is trickier I think.
Jan 15 2021
parent reply Ola Fosheim Grostad <ola.fosheim.grostad gmail.com> writes:
On Saturday, 16 January 2021 at 06:33:13 UTC, Bruce Carneal wrote:
 It sounds like mappings to arbitrary precision big endian 
 unsigned is where you're headed currently.
More like converting to ulong snd ucent. But CPUs have instructions for endian conversion so that is ok. Converting from signed to unsigned is just one add.
 Implementing relop-unifying xforms to whatever form you 
 actually choose should not be difficult.  You have the types in 
 hand so you're looking at a specialized binary serialization 
 problem.  Coming back out again is trickier I think.
I think most conversion will only take 1-4 cycles. The advantage is that once everything is uniform you can use handcoded SIMD in the container algorithms, which would be too much work otherwise... Dunno if the trade off is worth it,but it might? Btw, one challenge for getting compiler level type erasure is that function addresses cannot be used for type identity, maybe a language spec issue?
Jan 15 2021
parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Saturday, 16 January 2021 at 06:57:53 UTC, Ola Fosheim Grostad 
wrote:
 On Saturday, 16 January 2021 at 06:33:13 UTC, Bruce Carneal 
 wrote:
 It sounds like mappings to arbitrary precision big endian 
 unsigned is where you're headed currently.
More like converting to ulong snd ucent. But CPUs have instructions for endian conversion so that is ok. Converting from signed to unsigned is just one add.
Or, more precisely, make it architecture dependent. I guess that is the crux. The library has to provide inline-able conversion operations.
Jan 15 2021
prev sibling parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Friday, 15 January 2021 at 18:11:04 UTC, Bruce Carneal wrote:
 value types of the same size.  This shows up when implementing 
 radix sort where one solution is to map to/from whole numbers 
 (NaN semantics being ignored).
If we accept the following encoding for floating point then it should work out ok? INPUT OUTPUT s exp sig s exp sig 1 1100 1111110 0 0011 0000001 1 0011 0000001 0 1100 1111110 1 0000 0000000 0 1111 1111111 0 0000 0000000 1 0000 1111111 0 0011 0000011 1 0011 0000001 0 1100 1111110 1 1100 1111110 Algorithm: 1. flip the signbit 2. if the flipped sign is 0 then negate the exponent and signficand/mantissa. And it can be reversed just as easily? Not too bad!?
Jan 16 2021
parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Saturday, 16 January 2021 at 08:41:18 UTC, Ola Fosheim Grøstad 
wrote:
 On Friday, 15 January 2021 at 18:11:04 UTC, Bruce Carneal wrote:
 value types of the same size.  This shows up when implementing 
 radix sort where one solution is to map to/from whole numbers 
 (NaN semantics being ignored).
If we accept the following encoding for floating point then it should work out ok? INPUT OUTPUT s exp sig s exp sig 1 1100 1111110 0 0011 0000001 1 0011 0000001 0 1100 1111110 1 0000 0000000 0 1111 1111111 0 0000 0000000 1 0000 1111111
Typo: 0 0000 0000000 1 0000 0000000 Maybe I am overlooking something... This is like 1-2 cycles.
Jan 16 2021
parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Saturday, 16 January 2021 at 08:45:23 UTC, Ola Fosheim Grøstad 
wrote:
 Maybe I am overlooking something... This is like 1-2 cycles.
I haven't tested, so I could be in error, but it seems like these two might work? ulong x = floatingpoint double value; 1: x ^ ((0ULL - (x>>63)) | (1ULL<63)) or 2: x & (1ULL<63) ? ~x : x | (1ULL<63) So basically in generic assembly: 1: shift right, sub, or, xor => 4 very fast ops 2: tst, cmp, (neg / or) => 2 fast ops + 1 branch If this works, then there is really no reason not to use ulong/uint for floating point keys, except -0.0 and 0.0 will be sorted, but that is desirable sometimes, so could be a plus as well. If people don't want that then they should normalize to 0.0 before using the key.
Jan 16 2021
parent reply Bruce Carneal <bcarneal gmail.com> writes:
On Saturday, 16 January 2021 at 09:28:22 UTC, Ola Fosheim Grøstad 
wrote:
 On Saturday, 16 January 2021 at 08:45:23 UTC, Ola Fosheim 
 Grøstad wrote:
 Maybe I am overlooking something... This is like 1-2 cycles.
I haven't tested, so I could be in error, but it seems like these two might work? ulong x = floatingpoint double value; 1: x ^ ((0ULL - (x>>63)) | (1ULL<63)) or 2: x & (1ULL<63) ? ~x : x | (1ULL<63) So basically in generic assembly: 1: shift right, sub, or, xor => 4 very fast ops 2: tst, cmp, (neg / or) => 2 fast ops + 1 branch If this works, then there is really no reason not to use ulong/uint for floating point keys, except -0.0 and 0.0 will be sorted, but that is desirable sometimes, so could be a plus as well. If people don't want that then they should normalize to 0.0 before using the key.
Here's a link on the topic: http://stereopsis.com/radix.html. Excerpts from that for the 32 bit float case follow: Converting over: uint32 mask = -int32(f >> 31) | 0x80000000; return f ^ mask; and back: uint32 mask = ((f >> 31) - 1) | 0x80000000; return f ^ mask; This form is branchless and is easily (SIMD) vectorized. As you've noted, there are some oddities that arise when using unsigned relops against the mapped values. Negative and positive zeroes are distinct and NaNs take on a definite, and split, ordering. For my current work I prefer this to traditional float behavior and find it a useful bijection overall. YMMV.
Jan 16 2021
parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Saturday, 16 January 2021 at 17:03:41 UTC, Bruce Carneal wrote:
 Converting over:
 	uint32 mask = -int32(f >> 31) | 0x80000000;
 	return f ^ mask;
 and back:
 	uint32 mask = ((f >> 31) - 1) | 0x80000000;
 	return f ^ mask;
Thanks, this is the same I have as option 1 above.
 This form is branchless and is easily (SIMD) vectorized.
Yes, and perhaps also better for the CPU pipeline.
 split, ordering.  For my current work I prefer this to 
 traditional float behavior and find it a useful bijection 
 overall.  YMMV.
Out of curiosity, what do you need to sort floats for?
Jan 16 2021
parent reply Bruce Carneal <bcarneal gmail.com> writes:
On Saturday, 16 January 2021 at 19:43:53 UTC, Ola Fosheim Grøstad 
wrote:
 On Saturday, 16 January 2021 at 17:03:41 UTC, Bruce Carneal 
 wrote:
 Converting over:
 	uint32 mask = -int32(f >> 31) | 0x80000000;
 	return f ^ mask;
 and back:
 	uint32 mask = ((f >> 31) - 1) | 0x80000000;
 	return f ^ mask;
Thanks, this is the same I have as option 1 above.
Almost certainly the same. Only the most aggressively stupid compiler modes would forego an available/advantageous conversion of the subtract to a mono-operand negation. Even then, once any loop got rolling throughput should be identical. Yes.
 This form is branchless and is easily (SIMD) vectorized.
Yes, and perhaps also better for the CPU pipeline.
Unless the data you're dealing with is nearly sorted, the branch mispredict penalty could really hurt so, yeah, don't go there. For the branchless variant, throughput will come down to issue capability. For a dual issue SIMD CPU it looks like a naive loop should hit throughput of 2 SIMD vectors every three cycles (I don't have real numbers for the standalone case, all my transforms appear within larger basic blocks). SIMT throughput should be memory subsystem limited.
 split, ordering.  For my current work I prefer this to 
 traditional float behavior and find it a useful bijection 
 overall.  YMMV.
Out of curiosity, what do you need to sort floats for?
I've been working on my current project for a few months but I'm still not ready to talk about it in detail. I will say that it does not sort anything (doesn't have the power/compute budget for sorting) and that it does employ the bijection as a type erasure/restoration mechanism.
Jan 16 2021
parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Sunday, 17 January 2021 at 04:03:30 UTC, Bruce Carneal wrote:
 I've been working on my current project for a few months but 
 I'm still not ready to talk about it in detail.  I will say 
 that it does not sort anything (doesn't have the power/compute 
 budget for sorting) and that it does employ the bijection as a 
 type erasure/restoration mechanism.
This "bijection" strategy is interesting. (I assume you think of bijective functions in mathematics?) For some reason I've never really given it much thought. But it makes a lot of sense for encoding keys. Some online services also only accept keys that are either strings or 64bit integers, so it might be useful in other contexts. I've written a generic key type that does this encoding (in C++, but easy to port to D), and are working on a compressing one. So, for instance if you have a tuple of (z,y,x) then you can specify that 1≤x≤31, 1≤y≤12, 1970≤z≤2030 and let it be compressed either by bit shifting or multiplication (slower but tighter). So for instance (2021,1,17) would compress as ((2021-1970)*12+0)*31+16 Is there some way to find out the range (min, max) of an enum? The next step is to cover keys larger than 64 bits, I guess I should cover up to 256 bits.
Jan 17 2021
parent Bruce Carneal <bcarneal gmail.com> writes:
On Sunday, 17 January 2021 at 10:37:10 UTC, Ola Fosheim Grøstad 
wrote:
 On Sunday, 17 January 2021 at 04:03:30 UTC, Bruce Carneal wrote:

 This "bijection" strategy is interesting.  (I assume you think 
 of bijective functions in mathematics?)
Yes, one-to-one and onto (and we've wandered quite a ways OT). Good luck.
Jan 17 2021
prev sibling next sibling parent Petar Kirov [ZombineDev] <petar.p.kirov gmail.com> writes:
On Monday, 11 January 2021 at 18:23:20 UTC, Q. Schroll wrote:
 I don't want to be mistaken. I know what the differences of 

 are. Maybe my question has obvious answers and I just don't see 
 them.

 [...]
I agree with everything you said. Even though I greatly enjoy fully-erased typing. Type-checking has been described as the most successful form of light-weight formal verification and some of my TypeScript usage feels like this (compared to using pure JavaScript). Sometimes I don't need **or want** different code to be generated depending on the generic parameters, I just want the type system semantics and the light-weight formal verification (e.g. implementing units of measure).
Jan 12 2021
prev sibling next sibling parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
Go now has a generics proposal:

https://github.com/golang/go/issues/43651
Jan 13 2021
prev sibling parent sighoya <sighoya gmail.com> writes:
On Monday, 11 January 2021 at 18:23:20 UTC, Q. Schroll wrote:
 The only programming language I know of that has both, 
 templates and generics, is C++/CLI and ones closely related.


 not have generics.
Because OOP could be rarely supported in D without OOP and pure template programming while templates already serve the need for generics excluding the complexity they are introducing.
 A generic class or interface states its requirements (base 
 classes, interfaces, [never seen in the wild:] subclasses, ...) 
 to its type parameters exactly.
D can already do that with where constraints.
 Everything that is part of the implementation is checked when 
 the generic aggregate is defined.
So simply speaking you seek for eagerly checked templates which indeed seems to be an advantage of generics but why not optionally the same for templates? Rather reinventing the wheel, why not modulating the template error system to infer and uppropagate where constraints automatically by traversing type/function bodies? Another issue of generics are the parametrized error messages often disliked by people using them. We could do that, in theory, better just by mentioning the position of the occurred error in the template-expanded code fragment similar to how Python handles type errors. It would remove the barrier of template utilization a lot.
 Generics allow for implicit conversion by covariance and 

 interface to its covariant or contravariant part-interface 
 (that is a supertype of the interface, really). (Java [2, 3])
I don't see any reason why not providing them with templates, too with specialized syntax for example by: //Covariance dlist!(T where T:A || T:B) ==> dlist!(Algebraic!(A,B)) //Contravariance dlist!(T where A:T) ==> dlist!(Object)//where compiler rejects any assignment of A's strict subtypes.
 Using templates, the compiler checks requirements only for 
 specific instances. It might be that the requirements are 
 insufficient, but because no test tried the potentially very 
 specific type argument, it will be unrecognized.
Yep, sorta implicit where constraints would serve the purpose here.
 Also, one feature D doesn't have, is expressing the precise 
 union or intersection of interfaces: Say you have two 
 interfaces I and J.
Isn't a question about templates rather about intersection and union types as language level concept or as library solution.
 The interface (I & J) has all the methods specified by any of 
 them. A class that implements I and J automatically implements 
 (I & J).
 The interface (I | J) has all the methods specified by both of 
 them. A class that implements I or implements J automatically 
 implements (I | J). If you e.g. iterate an (I | J)[] object, 
 you can call any method required by both interfaces. 
 (Typescript [4])
 It might be hard or even impossible to implement this using 
 vtables and stuff.
Personally, I think it can with kinda implicitCoercionOp and Algebraic and some Intersection like variant.
 In D, one can easily create templates intersectInterface(Is...) 
 and unionInterface(Is...) that basically do that.

 It could very well be that D doesn't have them because they 
 have to be implemented and maintained, and the cost/benefit 
 ratio wasn't good enough.
D has already Algebraic for Unions, don't know about Intersection implementations. Note that the implementation for Unions vary. In Scala (and union of two classes is internally just the supertype of both classes and the intersection of interfaces are classes/interfaces implementing/extending all the said interfaces which could implemented with some support of compile time reflection for OOP hierarchies. It has however the disadvantage of overloading ambiguities when two pairs of classes have the same supertype. But I would prefer to support other types than classes and interfaces as well just as it is the case with Algebraic.
Jan 13 2021