www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - structs, classes, interfaces - Part III, Solution

reply Martin Hinsch <m.hinsch rug.nl> writes:
The basic idea behind my solution is actually really simple. You just have to
realize that (OOP-) inheritance as it is usually done is a combination of
two different concepts - composition and polymorphism. What would happen if
we disentangled these concepts?

My suggestion would be to completely remove virtual functions from the language.
Instead use interfaces to provide ad hoc runtime polymorphism.

As before interfaces define a set of methods a class has to provide. As before
you can let a class derive from an interface to declare (and let the compiler
check) its conformance to the interface. But now this is not obligatory anymore.

There are now two fundamentally different types of pointers in the language:
a) *non-polymorphic* pointers to plain data/classes
b) *polymorphic* pointers to interfaces
At any point a pointer of type a) can be cast to a pointer of type b) *at which
point the conformance to the interface is tested*. This is easily done at 
compile time since in the non-polymorphic world all types are known.

I'll give an example:

---

class A
	{
	void foo(int a);
	}

class B
	{
	void foo(int b);
	}


interface Foo
	{
	void foo(int c);
	}

A a;
B b;

Foo * f1 = &a, f2 = &b;
f1.foo(42);		// calls a.foo

---

You could even introduce a way to construct an interface from a given class so
that existing class hierarchies could remain completely unchanged:

---

interface(B) * i = &b;

---

Internally a pointer to an interface would actually be a struct containing the
vtable and a pointer to the object which would be a tiny bit more costly than
a class reference.

Since polymorphism has now its own separate language structure composition can
be made much more powerful (similar ideas have recently been discussed for 
structs):

---

class A {...}
class B {...}

class C : A,B {...}

// syntactic sugar for

class C
	{
	A super_0;
	B super_1;
	
	use super_0;
	use super_1;
	}
	
---

I think this solves all problems I mentioned above except one, which is slicing.
To be honest I never saw the particular danger in that one in the first place.
Maybe I'm totally missing something but isn't that just another case of a lossy
cast? In that case my proposal solves that problem as well since all potentially
lossy (slicy ;-) casts can be detected at compile time and the user
warned.

I have no illusions concerning the probability of this making it into D.
Still I think it's a nice idea ;-) and I would be really interested in
hearing what people think about it.


-- 
Martin Hinsch

m.hinsch rug.nl
+31 50 363 8097

CEES Centre for Ecological and Evolutionary Studies
Biologisch Centrum
Kerklaan 30
Postbus 14
9750 AA Haren
Sep 01 2007
next sibling parent reply Jari-Matti =?ISO-8859-1?Q?M=E4kel=E4?= <jmjmak utu.fi.invalid> writes:
Martin Hinsch wrote:

 The basic idea behind my solution is actually really simple. You just have
 to realize that (OOP-) inheritance as it is usually done is a combination
 of two different concepts - composition and polymorphism. What would
 happen if we disentangled these concepts?
 
 My suggestion would be to completely remove virtual functions from the
 language. Instead use interfaces to provide ad hoc runtime polymorphism.
 
 As before interfaces define a set of methods a class has to provide. As
 before you can let a class derive from an interface to declare (and let
 the compiler check) its conformance to the interface. But now this is not
 obligatory anymore.

 I think this solves all problems I mentioned above except one, which is
 slicing. To be honest I never saw the particular danger in that one in the
 first place. Maybe I'm totally missing something but isn't that just
 another case of a lossy cast? In that case my proposal solves that problem
 as well since all potentially lossy (slicy ;-) casts can be detected at
 compile time and the user warned.
 
 I have no illusions concerning the probability of this making it into D.
 Still I think it's a nice idea ;-) and I would be really interested in
 hearing what people think about it.

I'm not sure if I understood you correctly, but isn't this exactly the thing that was discussed recently. From language consistency POV
 class A {...}
 class B {...}
 
 class C : A,B {...}

is a bit problematic because D doesn't have multiple inheritance. A trait class construct could be possible though, but I cannot say if the added complexity to the core language is good. However, the interface compliance could be enforced that way (and Walter seems to like it too, see slides). Implementation could be mixed in with an extended mixin syntax: interface IA { void foo(); } interface IB { int bar(); } struct A : IA { void foo() { writefln("hello"); } struct B : IB { int bar() { return 42; } } struct C : IA, IB { mixin A; mixin B; } I've noticed mixin is a powerful mechanism. MI is more or less a combination of interface part and mixed in implementation and provides a lot less flexibility.
Sep 01 2007
next sibling parent reply Daniel Keep <daniel.keep.lists gmail.com> writes:
Jari-Matti Mäkelä wrote:
 ...
 
 interface IA { void foo(); }
 interface IB { int bar(); }
 
 struct A : IA { void foo() { writefln("hello"); }
 struct B : IB { int bar() { return 42; } }
 
 struct C : IA, IB {
   mixin A;
   mixin B;
 }

I believe this will be possible using: struct C : IA, IB { private A a; private B b; alias a this; alias b this; } -- Daniel
Sep 01 2007
parent reply Jari-Matti =?ISO-8859-1?Q?M=E4kel=E4?= <jmjmak utu.fi.invalid> writes:
Daniel Keep wrote:

 
 
 Jari-Matti Mäkelä wrote:
 ...
 
 interface IA { void foo(); }
 interface IB { int bar(); }
 
 struct A : IA { void foo() { writefln("hello"); }
 struct B : IB { int bar() { return 42; } }
 
 struct C : IA, IB {
   mixin A;
   mixin B;
 }

I believe this will be possible using: struct C : IA, IB { private A a; private B b; alias a this; alias b this; }

I don't doubt it isn't possible, but it needs to be implemented first too :) One problem I see in the new aliasing syntax is that it introduces two unnecessary symbols C.a and C.b as an side effect. (but it could allow you to choose between a.foo and b.foo if foo happens to conflict, though) Another way to implement this could be to allow "template inheritance". In that case only non-virtual inheritance model needs to be implemented, no new mixins or aliases:
 template A : IA { void foo() { writefln("hello"); }
 template B : IB { int bar() { return 42; } }
 
 struct C : IA, IB {
   mixin A!(); // or maybe even mixin A; when there are no arguments
   mixin B!();
 }


Sep 01 2007
parent reply Reiner Pope <some address.com> writes:
Jari-Matti Mäkelä wrote:
 Daniel Keep wrote:
 
 Jari-Matti Mäkelä wrote:
 ...

 interface IA { void foo(); }
 interface IB { int bar(); }

 struct A : IA { void foo() { writefln("hello"); }
 struct B : IB { int bar() { return 42; } }

 struct C : IA, IB {
   mixin A;
   mixin B;
 }

struct C : IA, IB { private A a; private B b; alias a this; alias b this; }

I don't doubt it isn't possible, but it needs to be implemented first too :) One problem I see in the new aliasing syntax is that it introduces two unnecessary symbols C.a and C.b as an side effect. (but it could allow you to choose between a.foo and b.foo if foo happens to conflict, though)

I wonder if we can extend the alias this syntax to get rid of the unnecessary symbols. Since, conceptually, aliasing just "binds" the second name to the first, giving the first variable a new name, why don't we just allow variables to be named "this". The code above becomes struct C : IA, IB { A this; B this; } To me it's quite consistent with "alias b this". -- Reiner
Sep 01 2007
parent reply Jari-Matti =?ISO-8859-1?Q?M=E4kel=E4?= <jmjmak utu.fi.invalid> writes:
Reiner Pope wrote:
 
 I wonder if we can extend the alias this syntax to get rid of the
 unnecessary symbols. Since, conceptually, aliasing just "binds" the
 second name to the first, giving the first variable a new name, why
 don't we just allow variables to be named "this". The code above becomes
 
 struct C : IA, IB {
      A this;
      B this;
 }
 
 To me it's quite consistent with "alias  b this".

Maybe I'm missing something, but why should alias be used in the first place? Currently it's only a weak typedef (extended to other symbols) with some overloading semantics. Adding mixin-like behavior inside structs feels a bit inconsistent to me at least. Would it also allow aliasing some symbols outside the struct? Doesn't it complicate metaprogramming a bit by mixing runtime and compile time code?
Sep 02 2007
parent Reiner Pope <some address.com> writes:
Jari-Matti Mäkelä wrote:
 Reiner Pope wrote:
  
 I wonder if we can extend the alias this syntax to get rid of the
 unnecessary symbols. Since, conceptually, aliasing just "binds" the
 second name to the first, giving the first variable a new name, why
 don't we just allow variables to be named "this". The code above becomes

 struct C : IA, IB {
      A this;
      B this;
 }

 To me it's quite consistent with "alias  b this".

Maybe I'm missing something, but why should alias be used in the first place? Currently it's only a weak typedef (extended to other symbols) with some overloading semantics. Adding mixin-like behavior inside structs feels a bit inconsistent to me at least. Would it also allow aliasing some symbols outside the struct? Doesn't it complicate metaprogramming a bit by mixing runtime and compile time code?

I think it makes sense in the first place because one of alias's meanings is "give the first symbol another name." In the case of "alias B this", you are giving B the name, "this". Since all member accesses and member functions are of the form this.foo or this.func() (but the "this." may be omitted) it makes sense that if B gets the name "this", then it behaves like a member (as far as calling goes). -- Reiner
Sep 04 2007
prev sibling parent Martin Hinsch <m.hinsch rug.nl> writes:
On Sat, 01 Sep 2007 16:20:44 +0300, Jari-Matti Mäkelä wrote:

 Martin Hinsch wrote:
 
 The basic idea behind my solution is actually really simple. You just
 have to realize that (OOP-) inheritance as it is usually done is a
 combination of two different concepts - composition and polymorphism.
 What would happen if we disentangled these concepts?
 
 My suggestion would be to completely remove virtual functions from the
 language. Instead use interfaces to provide ad hoc runtime polymorphism.
 
 As before interfaces define a set of methods a class has to provide. As
 before you can let a class derive from an interface to declare (and let
 the compiler check) its conformance to the interface. But now this is
 not obligatory anymore.

 I think this solves all problems I mentioned above except one, which is
 slicing. To be honest I never saw the particular danger in that one in
 the first place. Maybe I'm totally missing something but isn't that just
 another case of a lossy cast? In that case my proposal solves that
 problem as well since all potentially lossy (slicy ;-) casts can be
 detected at compile time and the user warned.
 
 I have no illusions concerning the probability of this making it into D.
 Still I think it's a nice idea ;-) and I would be really interested in
 hearing what people think about it.

I'm not sure if I understood you correctly, but isn't this exactly the thing that was discussed recently. From language consistency POV
 class A {...}
 class B {...}
 
 class C : A,B {...}

is a bit problematic because D doesn't have multiple inheritance. A trait class construct could be possible though, but I cannot say if the added complexity to the core language is good. However, the interface compliance could be enforced that way (and Walter seems to like it too, see slides). Implementation could be mixed in with an extended mixin syntax: interface IA { void foo(); } interface IB { int bar(); } struct A : IA { void foo() { writefln("hello"); } struct B : IB { int bar() { return 42; } } struct C : IA, IB { mixin A; mixin B; } } I've noticed mixin is a powerful mechanism. MI is more or less a combination of interface part and mixed in implementation and provides a lot less flexibility.

Yes, as I said, the "enhanced composition" part has been discussed recently. However only for structs and only in a very specific way. Anyways the point of my proposal is not that but rather that run-time polymorphism is completely separated from (and thus made orthogonal to) constructing classes from other classes (aka derivation). In the *second step* this takes a big burden from the class hierarchy and makes it possible to be much more expressive with glueing various bits and pieces together to create a new class. The great thing is that as soon as derivation *only* does composition you can treat it as a special case of a general class of patterns which involve taking pieces of data + functions and stick them together like lego blocks to build something new. This kind of pattern is actually quite common nowadays in C++, I believe. You take a bunch of light-weight classes and combine them as data representation, algorithm, etc. parts of a bigger class. You add run-time polymorphism by adding in an ABC which declares the required methods. My idea would make this last step much more elegant and simple. -- Martin Hinsch m.hinsch rug.nl +31 50 363 8097 CEES Centre for Ecological and Evolutionary Studies Biologisch Centrum Kerklaan 30 Postbus 14 9750 AA Haren
Sep 01 2007
prev sibling next sibling parent reply Sean Kelly <sean f4.ca> writes:
Martin Hinsch wrote:
 
 I have no illusions concerning the probability of this making it into D.
 Still I think it's a nice idea ;-) and I would be really interested in
 hearing what people think about it.

To sidestep the issue for a moment, do templates solve your problem for arrays of structs, or is there some reason you need run-time polymorphism? Sean
Sep 01 2007
parent reply Martin Hinsch <m.hinsch rug.nl> writes:
On Sat, 01 Sep 2007 09:34:31 -0700, Sean Kelly wrote:

 Martin Hinsch wrote:
 
 I have no illusions concerning the probability of this making it into D.
 Still I think it's a nice idea ;-) and I would be really interested in
 hearing what people think about it.

To sidestep the issue for a moment, do templates solve your problem for arrays of structs, or is there some reason you need run-time polymorphism? Sean

I feel a bit dumb but I have no idea what you mean... The basic problem is that I want to have a proper OOP type but with all the speed and memory advantages value semantics offer PLUS the option of painlessly "upgrading" it later to something even more OOPishy (i.e. rt-polymorphism, reference semantics). -- Martin Hinsch m.hinsch rug.nl +31 50 363 8097 CEES Centre for Ecological and Evolutionary Studies Biologisch Centrum Kerklaan 30 Postbus 14 9750 AA Haren
Sep 01 2007
parent Sean Kelly <sean f4.ca> writes:
Martin Hinsch wrote:
 On Sat, 01 Sep 2007 09:34:31 -0700, Sean Kelly wrote:
 
 Martin Hinsch wrote:
 I have no illusions concerning the probability of this making it into D.
 Still I think it's a nice idea ;-) and I would be really interested in
 hearing what people think about it.

arrays of structs, or is there some reason you need run-time polymorphism?

I feel a bit dumb but I have no idea what you mean... The basic problem is that I want to have a proper OOP type but with all the speed and memory advantages value semantics offer PLUS the option of painlessly "upgrading" it later to something even more OOPishy (i.e. rt-polymorphism, reference semantics).

See my other post "implicit template function overloading broken?" for an example. However, templates limit polymorphism to compile-time. Sean
Sep 01 2007
prev sibling parent BCS <ao pathlink.com> writes:
Reply to Martin,

 The basic idea behind my solution is actually really simple. You just
 have to realize that (OOP-) inheritance as it is usually done is a 
 combination of two different concepts - composition and 
 polymorphism. What would happen if we disentangled these concepts?
 My suggestion would be to completely remove virtual functions from the
 language. Instead use interfaces to provide ad hoc runtime polymorphism.
 

An interesting idea. However I agree that it's not likely to make it in. However another option that can be taken from the "oop is two things" standpoint is to allow composition in structs (the forthcoming "alias foo this;" gets much of that) You can already do this (I think) in a some what hackish manner in that you can treat a pointer to a struct as a pointer to the type of it's first member. A cleaner way to do things would be to allow a struct to inherent from another struct but with the restriction that the new struct can't override anything from the base struct and that casting to the derived type is an unsafe operation.
 Internally a pointer to an interface would actually be a struct
 containing the vtable and a pointer to the object which would be a
 tiny bit more costly than a class reference.
 

I've wanted this for a LONG time. It would allow all sorts of fun things to implement an interface (functions, structs, maybe even basic types). Alas it would break COM or require a totally incompatible special case. (I darn to heck whoever defined the COM standard that way!)
Sep 01 2007