digitalmars.D - Why does D not have generics?
- Q. Schroll (55/55) Jan 11 2021 I don't want to be mistaken. I know what the differences of
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (8/14) Jan 11 2021 I think you are really talking about generics in Java and is
- Q. Schroll (25/38) Jan 12 2021 Yes. But I'm not saying: Do exactly as Java does. I'm saying:
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (11/24) Jan 12 2021 Right. D has to chose something that blends well with what is
- Paul Backus (6/15) Jan 12 2021 C++ concepts, as far as I'm aware, don't cause templates to be
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (3/7) Jan 12 2021 Not sure what you mean? C++ use SFINAE. If it fails it will look
- Paul Backus (9/16) Jan 12 2021 As far as I'm aware, C++ does not prevent you from writing code
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (9/12) Jan 12 2021 Ok, I get what you say. Concepts is more for library implementors
- Paul Backus (7/12) Jan 12 2021 You can do it in D too, if you use an interface or a type-erasure
- Q. Schroll (3/8) Jan 12 2021 I'll have a closer look at Tardy. I previously misunderstood it
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (40/40) Jan 12 2021 So, this is one, perhaps clumsy way of doing it:
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (23/31) Jan 12 2021 An improvement is to define a helper function:
- Paul Backus (8/17) Jan 12 2021 [...]
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (2/7) Jan 12 2021 Hm? pop_second requires StackConcept!(T,E) ?
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (13/20) Jan 12 2021 Works fine for me:
- jmh530 (11/33) Jan 12 2021 What about
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (4/13) Jan 12 2021 Yes, something like that! Except we wrap it up as
- Paul Backus (8/20) Jan 12 2021 The goal is to make it produce a compile-time error given only
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (3/6) Jan 12 2021 That's a bit pedantic. And it can never work because of static
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (6/8) Jan 12 2021 Well, I guess it could work, but it would be very challenging
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (4/7) Jan 12 2021 Maybe you want it to just bypass it to allow overloading? Then
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (9/16) Jan 12 2021 Btw, when I really want stuff like this in C++ I create a class
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (5/6) Jan 12 2021 In C++ it would obviously be:
- Dukc (9/10) Jan 12 2021 I recommend you check out Atila's Github profile. I think that he
- Petar Kirov [ZombineDev] (9/19) Jan 12 2021 Generics is a pure type system feature and no library can emulate
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (4/6) Jan 12 2021 The term "generics" just means parametric types. I think you guys
- Adam D. Ruppe (32/34) Jan 12 2021 Yeah, in the D context since we obviously have generic templates,
- jmh530 (12/30) Jan 12 2021 Not just classes? Could you explain a bit more about what you are
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (33/50) Jan 12 2021 I haven't looked very closely at Phobos, but it seems like it is
- Elronnd (4/18) Jan 13 2021 There was a recent paper about doing this kind of transformation
- Bruce Carneal (3/6) Jan 13 2021 Thanks for the link. The paper looks like a major advance, one
- Imperatorn (4/10) Jan 13 2021 Nice paper đŸ“„
- Guillaume Piolat (5/14) Jan 13 2021 A practice I've heard from game industry people was to
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (11/15) Jan 15 2021 Yes, that is what I suggested also. I implemented it yesterday
- Paulo Pinto (4/16) Jan 15 2021 If it is instantiated T = void* then it would only be safe to use
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (11/14) Jan 15 2021 That's a minor detail though.
- Bruce Carneal (8/22) Jan 15 2021 Type erasure can be tricky, even when it is restricted to basic
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (4/8) Jan 15 2021 Yes, I think maybe it can work if one cannot retrieve the key.
- Bruce Carneal (16/24) Jan 15 2021 You can transform to a canonical relop form wherever you'd like,
- Ola Fosheim Grostad (4/9) Jan 15 2021 The problem is little endian. Ascii strings will have to be
- Bruce Carneal (8/18) Jan 15 2021 It sounds like mappings to arbitrary precision big endian
- Ola Fosheim Grostad (11/17) Jan 15 2021 More like converting to ulong snd ucent. But CPUs have
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (5/12) Jan 15 2021 Or, more precisely, make it architecture dependent. I guess that
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (17/20) Jan 16 2021 If we accept the following encoding for floating point then it
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (5/17) Jan 16 2021 Typo:
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (16/17) Jan 16 2021 I haven't tested, so I could be in error, but it seems like these
- Bruce Carneal (16/33) Jan 16 2021 Here's a link on the topic: http://stereopsis.com/radix.html.
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (4/14) Jan 16 2021 Yes, and perhaps also better for the CPU pipeline.
- Bruce Carneal (19/34) Jan 16 2021 Almost certainly the same. Only the most aggressively stupid
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (17/22) Jan 17 2021 This "bijection" strategy is interesting. (I assume you think of
- Bruce Carneal (4/8) Jan 17 2021 Yes, one-to-one and onto (and we've wandered quite a ways OT).
- Petar Kirov [ZombineDev] (11/16) Jan 12 2021 I agree with everything you said. Even though I greatly enjoy
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (2/2) Jan 13 2021 Go now has a generics proposal:
- sighoya (44/79) Jan 13 2021 Because OOP could be rarely supported in D without OOP and pure
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
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
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: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.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?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
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
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: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.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.
Jan 12 2021
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
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: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.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
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
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
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
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
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
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
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
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: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)); }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
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:What about auto pop_second(T)(T* obj) if (__traits(compiles, { T input = T.init; auto stack = input.as_StackConcept(); })) { ... }On Tuesday, 12 January 2021 at 22:23:51 UTC, Paul Backus wrote: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)); }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
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
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: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.On Tuesday, 12 January 2021 at 22:23:51 UTC, Paul Backus wrote:Works fine for me: [...]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
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
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
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
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
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
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
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: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?[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
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
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 onYeah, 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
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
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
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
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.pdfThanks for the link. The paper looks like a major advance, one that should strongly influence future compiler architectures.
Jan 13 2021
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:Nice paper đŸ“„ (Well, the contents of the paper. Which is not actual paper.. *IQ increases*)[...]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
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
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
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: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.[...]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
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
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: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.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
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
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: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.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
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
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: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.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
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
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:Or, more precisely, make it architecture dependent. I guess that is the crux. The library has to provide inline-able conversion operations.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.
Jan 15 2021
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
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:Typo: 0 0000 0000000 1 0000 0000000 Maybe I am overlooking something... This is like 1-2 cycles.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
Jan 16 2021
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
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: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.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
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
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: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.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.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.This form is branchless and is easily (SIMD) vectorized.Yes, and perhaps also better for the CPU pipeline.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.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
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
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:Yes, one-to-one and onto (and we've wandered quite a ways OT). Good luck.This "bijection" strategy is interesting. (I assume you think of bijective functions in mathematics?)
Jan 17 2021
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
Go now has a generics proposal: https://github.com/golang/go/issues/43651
Jan 13 2021
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