www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Is there any chance to introduce concepts/traits/typeClasses in dlang?

reply sighoya <sighoya gmail.com> writes:
I know there is already a package available from atila neves 
about concepts, but despite being a neat workaround, it delivers 
not the full opportunities we know from type classes.

Why?
TypeClasses aren't a static solution only, they can also be used 
as existentials allowing dynamic dispatch implying that you can 
change the underlying implementation at runtime:
function(Concept c):Concept{...}

Further, implementing concepts over inner fields is limited, 
usually assoc functions are used (UFCS in case of D) which may 
can be covered with the concepts package of atila over dlang's 
compiletime reflection library "traits".
However, how instance resolution works then? Functions associated 
to types are local only, so if we import the concept from another 
module how can we check satisfyability if the needed 
ufcs/associated functions aren't imported likewise to the concept?

I know, introducing a new entity to existing entities like 
interfaces unnecessarily increases the complexity but how about 
reusing existing interfaces for that purpose?

For instance:

interface A
{
void method1();
}

interface B
{
void method2();
}

class C implements A //structural implements (valid D)
{
void method1() {...}
}

implement B for C //implement is a block keyword like class for 
posthoc implementation
{
method2(){...}
}

Another solution would be a pseudoclass:

class D implements B for C
{
method2(){...}
}

Now passing a C to B requires not only too look for C's 
definition but also for instances either globally in the project 
(preferred) or with special import for instances.

Another point to consider struct implementations via autoboxing, 
is it that hard to implement?
Also allowing fields in interfaces would complete the usefulness 
of concepts.

A possible look how flexible it can be taken from the nim language
Link:
https://nim-lang.org/docs/manual_experimental.html#concepts

I donna want to rant about D, I love it, really. But introducing 
classes separate to interfaces irks me and was in my eyes a 
failure from Java and all languages which follow them.

Why?
Because it introduces a biworld, both concepts overlap and you 
have to decide which to choose which is further complicated by 
availability of abstract classes, so why not unify all of them 
into a single concept?
Class methods were always default methods akin to default methods 
in interfaces and
collision by multiple inheritance wasn't ever an issue which can 
be easily solved by scoping the super interfaces.
Another biworld are final classes and structs which are seemingly 
equal in their purpose, from a type theoretic viewpoint they 
represent the same type, so why have to versions of the same 
abstraction, choosing only structs as value or reference type 
would excitingly solve the issue.

Nevertheless, we can't changed everything in a 
backward-compatible way. I hope some of you are happy to discuss 
ideas about concepts in this thread.
Dec 15 2019
next sibling parent rikki cattermole <rikki cattermole.co.nz> writes:
I have already gone down this path.

My design for named arguments was step one, but sadly DIP 1020 is dead.

My preference is towards signatures from ML.

Pretty simple concept, but in practice it is just as complex as classes 
are and would be just as big.

I don't think Walter has made a decision about this, but for reference: 
if a language had a similar scope to D and it supported them, I would 
probably jump. They would be a major improvement over D today for 
documenting API's especially with meta-programming considered.
Dec 15 2019
prev sibling next sibling parent reply mipri <mipri minimaltype.com> writes:
On Sunday, 15 December 2019 at 16:11:31 UTC, sighoya wrote:
 TypeClasses aren't a static solution only, they can also be
 used as existentials allowing dynamic dispatch
...
 A possible look how flexible it can be taken from the nim
 language
 Link:
 https://nim-lang.org/docs/manual_experimental.html#concepts
From that page: The concept is a match if: - all of the expressions within the body can be compiled for the tested type - all statically evaluable boolean expressions in the body must be true Looks to me like this is just a way of attaching D's static constraints to a type, so that void push(T)(Stack!T stack, T item) { } becomes (something like, for inference's sake S shouldn't be separate I think): void push(S,T)(S!T stack, T item) if (a bunch of !S tests) { } What does this have to do with dynamic dispatch? How is this not already something you can do with an isStack!S test?
 Nevertheless, we can't changed everything in a 
 backward-compatible way. I hope some of you are happy to 
 discuss ideas about concepts in this thread.
I don't understand what you're trying to do, and these A, B, C, D examples take a lot effort to read, vs. Comparable and Stack examples. What I'd prefer is // valid current-D example: ... // but look at (how verbose this is | how expensive this is | // how much effort it'd take to adapt this in Y direction | // &c)! vs. // valid improved-D example:
Dec 15 2019
next sibling parent reply rikki cattermole <rikki cattermole.co.nz> writes:
The easiest D example for this family of language features is ranges IMO.

Turn:

auto map(alias func, IR)(IR input) if (...) {
	struct Voldemort {
		...
	}

	return ...;
}

Into:

auto:InputRange map(alias func, IR:InputRange)(IR input) if (...) {
	struct Voldemort {
		...
	}

	return ...;
}

That is the static introspection capabilities.
For dynamic:

// typeof(input) is still templated, its just hidden, since only the 
template arguments need deducing
InputRange!ElementType map(alias func)(auto:InputRange input) if (...) {
	struct Voldemort {
		...
	}

	return ...; // auto construction of an InputRange instance with 
deduction of what ElementType is.
}
Dec 15 2019
parent reply sighoya <sighoya gmail.com> writes:
 That is the static introspection capabilities.
 For dynamic:

 // typeof(input) is still templated, its just hidden, since 
 only the template arguments need deducing
 InputRange!ElementType map(alias func)(auto:InputRange input) 
 if (...) {
 	struct Voldemort {
 		...
 	}

 	return ...; // auto construction of an InputRange instance 
 with deduction of what ElementType is.
 }
Im not quite aware of the meaning of auto:InputRange but in my eyes it is static dispatching too, I find it equivalent too: InputRange!ElementType map(alias func,ElemType)(InputRange!ElemType input){...}
Dec 15 2019
parent reply rikki cattermole <rikki cattermole.co.nz> writes:
On 16/12/2019 9:07 AM, sighoya wrote:
 That is the static introspection capabilities.
 For dynamic:

 // typeof(input) is still templated, its just hidden, since only the 
 template arguments need deducing
 InputRange!ElementType map(alias func)(auto:InputRange input) if (...) {
     struct Voldemort {
         ...
     }

     return ...; // auto construction of an InputRange instance with 
 deduction of what ElementType is.
 }
Im not quite aware of the meaning of auto:InputRange but in my eyes it is static dispatching too, I find it equivalent too: InputRange!ElementType map(alias func,ElemType)(InputRange!ElemType input){...}
Whatever map returns, can be initialized to the InputRange signature. That is what auto:Signature means. Where Signature can be although doesn't have to be template initialized. I think this is quite nice syntax, as it works in the function parameters and it is easily recognized by the parser because keyword auto then ':' then some type. What you changed it to, is using runtime dispatch instead of static. Another form I've come up with for static is ``is(T : Signature)``. With same meaning as ``auto:Signature`` but requires template constraints to mean the same thing.
Dec 15 2019
parent reply sighoya <sighoya gmail.com> writes:
On Monday, 16 December 2019 at 02:35:50 UTC, rikki cattermole 
wrote:
 On 16/12/2019 9:07 AM, sighoya wrote:
 That is the static introspection capabilities.
 For dynamic:

 // typeof(input) is still templated, its just hidden, since 
 only the template arguments need deducing
 InputRange!ElementType map(alias func)(auto:InputRange input) 
 if (...) {
     struct Voldemort {
         ...
     }

     return ...; // auto construction of an InputRange 
 instance with deduction of what ElementType is.
 }
Im not quite aware of the meaning of auto:InputRange but in my eyes it is static dispatching too, I find it equivalent too: InputRange!ElementType map(alias func,ElemType)(InputRange!ElemType input){...}
Whatever map returns, can be initialized to the InputRange signature. That is what auto:Signature means. Where Signature can be although doesn't have to be template initialized. I think this is quite nice syntax, as it works in the function parameters and it is easily recognized by the parser because keyword auto then ':' then some type. What you changed it to, is using runtime dispatch instead of static. Another form I've come up with for static is ``is(T : Signature)``. With same meaning as ``auto:Signature`` but requires template constraints to mean the same thing.
Ah okay, so this is a specialization of the auto kw which is generic T implying T:Signature
Dec 16 2019
parent reply rikki cattermole <rikki cattermole.co.nz> writes:
On 17/12/2019 12:13 AM, sighoya wrote:
 On Monday, 16 December 2019 at 02:35:50 UTC, rikki cattermole wrote:
 On 16/12/2019 9:07 AM, sighoya wrote:
 That is the static introspection capabilities.
 For dynamic:

 // typeof(input) is still templated, its just hidden, since only the 
 template arguments need deducing
 InputRange!ElementType map(alias func)(auto:InputRange input) if 
 (...) {
     struct Voldemort {
         ...
     }

     return ...; // auto construction of an InputRange instance with 
 deduction of what ElementType is.
 }
Im not quite aware of the meaning of auto:InputRange but in my eyes it is static dispatching too, I find it equivalent too: InputRange!ElementType map(alias func,ElemType)(InputRange!ElemType input){...}
Whatever map returns, can be initialized to the InputRange signature. That is what auto:Signature means. Where Signature can be although doesn't have to be template initialized. I think this is quite nice syntax, as it works in the function parameters and it is easily recognized by the parser because keyword auto then ':' then some type. What you changed it to, is using runtime dispatch instead of static. Another form I've come up with for static is ``is(T : Signature)``. With same meaning as ``auto:Signature`` but requires template constraints to mean the same thing.
Ah okay, so this is a specialization of the auto kw which is generic T implying T:Signature
Yeah, basically it tells the reader that the return value is guaranteed to be X. A small but what I think would be a very nice documentation addition especially for Phobos which is hard to get into.
Dec 16 2019
parent reply sighoya <sighoya gmail.com> writes:
I thought about your InputRange!T example and took a look at 
https://dlang.org/library/std/typecons/wrap.html

I quite nice solves the problem with passing structures to 
interfaces, though it would still be nice if this is done 
automatically.
Dec 16 2019
next sibling parent sighoya <sighoya gmail.com> writes:
On Monday, 16 December 2019 at 16:45:12 UTC, sighoya wrote:
 I thought about your InputRange!T example and took a look at 
 https://dlang.org/library/std/typecons/wrap.html

 I quite nice solves the problem with passing structures to 
 interfaces, though it would still be nice if this is done 
 automatically.
Damn, it only works for classes but it will probably introduced for structs, too: https://dlang.org/phobos/std_experimental_typecons.html
Dec 16 2019
prev sibling parent reply jmh530 <john.michael.hall gmail.com> writes:
On Monday, 16 December 2019 at 16:45:12 UTC, sighoya wrote:
 I thought about your InputRange!T example and took a look at 
 https://dlang.org/library/std/typecons/wrap.html

 I quite nice solves the problem with passing structures to 
 interfaces, though it would still be nice if this is done 
 automatically.
wrap only works for interfaces and classes, not structs.
Dec 16 2019
parent sighoya <sighoya gmail.com> writes:
On Monday, 16 December 2019 at 16:58:22 UTC, jmh530 wrote:
 On Monday, 16 December 2019 at 16:45:12 UTC, sighoya wrote:
 I thought about your InputRange!T example and took a look at 
 https://dlang.org/library/std/typecons/wrap.html

 I quite nice solves the problem with passing structures to 
 interfaces, though it would still be nice if this is done 
 automatically.
wrap only works for interfaces and classes, not structs.
Oh we overlapped to the same time, but yes unfortunately it is true, only experimental d allows for it I think.
Dec 16 2019
prev sibling parent sighoya <sighoya gmail.com> writes:
 Looks to me like this is just a way of attaching D's static
 constraints to a type, so that

   void push(T)(Stack!T stack, T item) { }

 becomes (something like, for inference's sake S shouldn't be
 separate I think):
   void push(S,T)(S!T stack, T item) if (a bunch of !S tests) { }
The point is that this only works for: 1.) static dispatch (compile time dispatch) 2.) for structural matches (is there any kind to constraint a type on its ufcs/assoc methods?), structural methods can't be added after the type is defined, however ufcs/assoc/extension methods can be defined later and pulled automatically into function scope of the method taking the implementing type of the concept: void fun(C:Concept)(C c) <==> void fun(C)(C c) if (C has all ufcs/assoc functions listed in the concept C) How to express the latter?
 What does this have to do with dynamic dispatch? How is this
 not already something you can do with an isStack!S test?
Dynamic dispatch mean you have an runtime uncertainty of the implementing type of a concept like the implementing type of a interface, so you can write: fun(Concept c) akin to fun(Interface i) which is different to: fun(C:Concept)(C c) as the latter method-set for the implementing type is passed at compile time and for the former at runtime.
Dec 15 2019
prev sibling parent sighoya <sighoya gmail.com> writes:
To clarify a bit:

Passing a struct containing all the methods in the constraint:

fun(Struct s) if(s.containsMethodsOf(Concept))

is no problem, as the instance (the set of required methods in 
the if constraint is delivered by the struct itself when it is 
passed as argument to fun), but for the case of type 
classes/concepts, the instance is separated from the passed type 
and must be implicit passed by the compiler, you need compiler 
magic for that akin to Rust or you do it manually akin to the 
ugly variant in Scala, even Nim uses magic for this.
Dec 15 2019