Jump to page: 1 2
Thread overview
Is there a way to forward-declare interfaces to avoid module interdependencies?
Nov 11, 2010
Per Ångström
Nov 11, 2010
Jacob Carlborg
Nov 11, 2010
Per Ångström
Nov 14, 2010
Per Ångström
Nov 28, 2010
Per Ångström
Nov 30, 2010
Per Ångström
Re: Is there a way to forward-declare interfaces to avoid module
Nov 11, 2010
bearophile
Nov 11, 2010
Jonathan M Davis
November 11, 2010
I hope we can all agree that minimizing module interdependencies is a good thing, that a highly interdependent set of modules is generally harder to understand and more difficult to test than if the module-dependency chain is acyclic.

With that in mind, suppose I have the following C++ code, organized with a separation of  interface and implementation, with no circular include dependencies:

// IStudent.h
class ITeacher; // forward reference

class IStudent {
public:
	virtual void ask(ITeacher &) = 0;
	virtual void learn(ITeacher &, const char * knowledge) = 0;
};

// ITeacher.h
class IStudent; // forward reference

class ITeacher
{
public:
	virtual void teach(IStudent &) = 0;
};

// Student.h
#include "IStudent.h"
class Student : public IStudent {
public:
	void ask(ITeacher &);
	void learn(ITeacher &, const char * knowledge);
};
// Teacher.h
#include "ITeacher.h"
class Teacher: public ITeacher {
public:
	void teach(IStudent &);
};
// Student.cpp
#include "Student.h"
#include "ITeacher.h"

void Student::ask(ITeacher & teacher)
{
	teacher.teach(*this);
}

void Student::learn(ITeacher &, const char * )
{
}

// Teacher.cpp
#include "Teacher.h"
#include "IStudent.h"

void Teacher::teach(IStudent & student)
{
	student.learn(*this, "knowledge");
}

// main.cpp
#include "Student.h"
#include "Teacher.h"
int main()
{
	Student student;
	Teacher teacher;
	student.ask(teacher);
	return 0;
}

Below is my attempt at porting the code to D2. I hope I'm missing something due to my limited experience with D, but it seems D forces me to create circular dependencies between modules ITeacher and IStudent, since I cannot find a way to forward-declare types external to the current module in D like in C/C++.

// IStudent.d
import ITeacher; // would like to forward-declare the interface instead

interface IStudent {
	void ask(ITeacher);
	void learn(ITeacher, string knowledge);
}

// ITeacher.d
import IStudent;

interface ITeacher
{
	void teach(IStudent);
}
// Student.d
import IStudent;

class Student : IStudent {
	void ask(ITeacher teacher)
	{
		teacher.teach(this);
	}
	void learn(ITeacher teacher, string  knowledge)
	{
	}	
}
// Teacher.d
import ITeacher;
import IStudent;

class Teacher: ITeacher {
	void teach(IStudent student)
	{
		student.learn(this, "knowledge");
	}
}

// main.d
import Student;
import Teacher;
void main()
{
	auto student = new Student;
	auto teacher = new Teacher;
	student.ask(teacher);
}

So my question is: Am I missing something, or is this the D way to do it?

Cheers,
-- 
Per Å.
November 11, 2010
On 2010-11-11 13:10, Per Ångström wrote:
> I hope we can all agree that minimizing module interdependencies is a
> good thing, that a highly interdependent set of modules is generally
> harder to understand and more difficult to test than if the
> module-dependency chain is acyclic.
>
> With that in mind, suppose I have the following C++ code, organized with
> a separation of interface and implementation, with no circular include
> dependencies:
>
> // IStudent.h
> class ITeacher; // forward reference
>
> class IStudent {
> public:
> virtual void ask(ITeacher &) = 0;
> virtual void learn(ITeacher &, const char * knowledge) = 0;
> };
>
> // ITeacher.h
> class IStudent; // forward reference
>
> class ITeacher
> {
> public:
> virtual void teach(IStudent &) = 0;
> };
>
> // Student.h
> #include "IStudent.h"
> class Student : public IStudent {
> public:
> void ask(ITeacher &);
> void learn(ITeacher &, const char * knowledge);
> };
> // Teacher.h
> #include "ITeacher.h"
> class Teacher: public ITeacher {
> public:
> void teach(IStudent &);
> };
> // Student.cpp
> #include "Student.h"
> #include "ITeacher.h"
>
> void Student::ask(ITeacher & teacher)
> {
> teacher.teach(*this);
> }
>
> void Student::learn(ITeacher &, const char * )
> {
> }
>
> // Teacher.cpp
> #include "Teacher.h"
> #include "IStudent.h"
>
> void Teacher::teach(IStudent & student)
> {
> student.learn(*this, "knowledge");
> }
>
> // main.cpp
> #include "Student.h"
> #include "Teacher.h"
> int main()
> {
> Student student;
> Teacher teacher;
> student.ask(teacher);
> return 0;
> }
>
> Below is my attempt at porting the code to D2. I hope I'm missing
> something due to my limited experience with D, but it seems D forces me
> to create circular dependencies between modules ITeacher and IStudent,
> since I cannot find a way to forward-declare types external to the
> current module in D like in C/C++.
>
> // IStudent.d
> import ITeacher; // would like to forward-declare the interface instead
>
> interface IStudent {
> void ask(ITeacher);
> void learn(ITeacher, string knowledge);
> }
>
> // ITeacher.d
> import IStudent;
>
> interface ITeacher
> {
> void teach(IStudent);
> }
> // Student.d
> import IStudent;
>
> class Student : IStudent {
> void ask(ITeacher teacher)
> {
> teacher.teach(this);
> }
> void learn(ITeacher teacher, string knowledge)
> {
> }
> }
> // Teacher.d
> import ITeacher;
> import IStudent;
>
> class Teacher: ITeacher {
> void teach(IStudent student)
> {
> student.learn(this, "knowledge");
> }
> }
>
> // main.d
> import Student;
> import Teacher;
> void main()
> {
> auto student = new Student;
> auto teacher = new Teacher;
> student.ask(teacher);
> }
>
> So my question is: Am I missing something, or is this the D way to do it?
>
> Cheers,

First you have to import ITeacher in Student.d (if I'm reading this right). Second, D has (generally) no problems with circular references. You only get problems when two modules is a part of a circular reference and both have module constructors. If you porting C++ code you will not have this problem since C++ doesn't have module constructors.

BTW, you generally don't separate your interface and implementation in D (you can to that if you want to hide your implementation).

The import/module system in D is more like the one in Java than the one in C/C++.

-- 
/Jacob Carlborg
November 11, 2010
On 2010-11-11 15:51, Jacob Carlborg wrote:
> First you have to import ITeacher in Student.d (if I'm reading this
> right).

No, the code works fine as is, but if I move the import of ITeacher from IStudent.d to Student.d, IStudent.d won't compile:

IStudent.d(4): Error: identifier 'ITeacher' is not defined
IStudent.d(4): Error: ITeacher is used as a type

> Second, D has (generally) no problems with circular references.

No, but I have! ;-) To be honest, I would have wanted D to outright outlaw circular references.

> You only get problems when two modules is a part of a circular reference
> and both have module constructors. If you porting C++ code you will not
> have this problem since C++ doesn't have module constructors.

Actually, I'm more thinking whether D would be a good language for building really big systems. If the language more or less requires many modules to import each other, that's a big drawback in my opinion, since that makes a mess of the dependency graph. It also makes independent testing of individual modules a lot harder.

> BTW, you generally don't separate your interface and implementation in D
> (you can to that if you want to hide your implementation).

I think information hiding is essential for a large-scale design to be comprehensible as a whole. Agreed, the module system in D hides a lot of implementation detail from being included in each compilation unit, but we must also consider not overloading the programmer with needless information.

But then again, maybe I just need to stop thinking in C++.

> The import/module system in D is more like the one in Java than the one
> in C/C++.

OK, I'm more familiar with C++. But I really like the concept of importing modules instead of file inclusion.

Cheers,
-- 
Per Å.
November 11, 2010
On Thu, 11 Nov 2010 11:07:42 -0500, Per Ångström <d-news@autark.se> wrote:

> On 2010-11-11 15:51, Jacob Carlborg wrote:
>> First you have to import ITeacher in Student.d (if I'm reading this
>> right).
>
> No, the code works fine as is, but if I move the import of ITeacher from IStudent.d to Student.d, IStudent.d won't compile:
>
> IStudent.d(4): Error: identifier 'ITeacher' is not defined
> IStudent.d(4): Error: ITeacher is used as a type
>
>> Second, D has (generally) no problems with circular references.
>
> No, but I have! ;-) To be honest, I would have wanted D to outright outlaw circular references.

First, you can't forward-declare classes in one file that are defined in another file, instead of importing.  The reason is because in D, the module is the namespace that the class is declared in.

So for instance, when you define IStudent in IStudent.d, it's full name is IStudent.Istudent

if you did something like:

interface IStudent;

in ITeacher.d, then it's full name would be ITeacher.IStudent, not IStudent.IStudent.  So it's not the same.

Second, you are only having fits about forward declarations because you aren't used to it :)  That being said, D has had many forward reference bugs (I believe there are still a few active ones), so it doesn't always *just work*.  Plus there's the issue of circular imports causing code to fail at runtime due to module construction dependencies.

But in any case, you shouldn't worry about it, just import away, and the compiler can take it.

>> You only get problems when two modules is a part of a circular reference
>> and both have module constructors. If you porting C++ code you will not
>> have this problem since C++ doesn't have module constructors.
>
> Actually, I'm more thinking whether D would be a good language for building really big systems. If the language more or less requires many modules to import each other, that's a big drawback in my opinion, since that makes a mess of the dependency graph. It also makes independent testing of individual modules a lot harder.

um... Java anyone?  I think there's been one or two big systems built with Java, but I might be guessing :)

>> BTW, you generally don't separate your interface and implementation in D
>> (you can to that if you want to hide your implementation).
>
> I think information hiding is essential for a large-scale design to be comprehensible as a whole. Agreed, the module system in D hides a lot of implementation detail from being included in each compilation unit, but we must also consider not overloading the programmer with needless information.
>
> But then again, maybe I just need to stop thinking in C++.

Yes :)

-Steve
November 11, 2010
Jacob Carlborg:

> The import/module system in D is more like the one in Java than the one in C/C++.

To me the D module system seems closer to the Python one than to the Java one.

Bye,
bearophile
November 11, 2010
On Thursday, November 11, 2010 09:42:02 bearophile wrote:
> Jacob Carlborg:
> > The import/module system in D is more like the one in Java than the one in C/C++.
> 
> To me the D module system seems closer to the Python one than to the Java one.

It may be - I haven't really used python, so I can't say - but in comparison to C++, the D and Java module systems are almost identical. Sure, there _are_ differences (like how D's modules don't have to have exactly one public class declaration which has the same name as the module), but they're very similar. Python is probably very similar as well but with fewer differences. Regardless however, in comparison to C++, Java's module system is very close to D's.

- Jonathan M Davis
November 14, 2010
On 2010-11-11 17:21, Steven Schveighoffer wrote:
> First, you can't forward-declare classes in one file that are defined in
> another file, instead of importing. The reason is because in D, the
> module is the namespace that the class is declared in.
>
> So for instance, when you define IStudent in IStudent.d, it's full name
> is IStudent.Istudent
>
> if you did something like:
>
> interface IStudent;
>
> in ITeacher.d, then it's full name would be ITeacher.IStudent, not
> IStudent.IStudent. So it's not the same.

I get it now. So there is no way out for me: I have to import any module whose interfaces I use, even though I'm only referring to their names.

> Second, you are only having fits about forward declarations because you
> aren't used to it :)

Actually, I would say it's the other way around: I am used to them (being able to refer to an unknown identifier as long as I don't use it) in C and C++ and find it lacking in D.

> That being said, D has had many forward reference
> bugs (I believe there are still a few active ones), so it doesn't always
> *just work*. Plus there's the issue of circular imports causing code to
> fail at runtime due to module construction dependencies.
>
> But in any case, you shouldn't worry about it, just import away, and the
> compiler can take it.

But I still think cutting down on intermodule dependencies is a good thing, for ease of comprehension and unit testing.

Fortunately, by separating interface and implementation, one can at least keep the implementation modules free from interdependencies. It's not perfect but it's not a complete mess either.

Cheers,
-- 
Per Å.
November 15, 2010
On Sun, 14 Nov 2010 05:28:29 -0500, Per Ångström <d-news@autark.se> wrote:

> On 2010-11-11 17:21, Steven Schveighoffer wrote:
>> First, you can't forward-declare classes in one file that are defined in
>> another file, instead of importing. The reason is because in D, the
>> module is the namespace that the class is declared in.
>>
>> So for instance, when you define IStudent in IStudent.d, it's full name
>> is IStudent.Istudent
>>
>> if you did something like:
>>
>> interface IStudent;
>>
>> in ITeacher.d, then it's full name would be ITeacher.IStudent, not
>> IStudent.IStudent. So it's not the same.
>
> I get it now. So there is no way out for me: I have to import any module whose interfaces I use, even though I'm only referring to their names.
>
>> Second, you are only having fits about forward declarations because you
>> aren't used to it :)
>
> Actually, I would say it's the other way around: I am used to them (being able to refer to an unknown identifier as long as I don't use it) in C and C++ and find it lacking in D.

I said that wrong, I meant forward *references* not forward *declrations*.  That is, referring to things that aren't declared yet.

>
>  > That being said, D has had many forward reference
>> bugs (I believe there are still a few active ones), so it doesn't always
>> *just work*. Plus there's the issue of circular imports causing code to
>> fail at runtime due to module construction dependencies.
>>
>> But in any case, you shouldn't worry about it, just import away, and the
>> compiler can take it.
>
> But I still think cutting down on intermodule dependencies is a good thing, for ease of comprehension and unit testing.
>
> Fortunately, by separating interface and implementation, one can at least keep the implementation modules free from interdependencies. It's not perfect but it's not a complete mess either.

It is one way to keep dependencies down.  And less dependencies means less coupling, meaning importing one module doesn't make you import (and include the code for) many other modules.  The compiler isn't yet mature enough to trim out some code that is never used.

-Steve
November 28, 2010
On 2010-11-15 16:52, Steven Schveighoffer wrote:
> On Sun, 14 Nov 2010 05:28:29 -0500, Per Ångström <d-news@autark.se> wrote:
>> Fortunately, by separating interface and implementation, one can at
>> least keep the implementation modules free from interdependencies.
>> It's not perfect but it's not a complete mess either.
>
> It is one way to keep dependencies down. And less dependencies means
> less coupling, meaning importing one module doesn't make you import (and
> include the code for) many other modules. The compiler isn't yet mature
> enough to trim out some code that is never used.

I have found a way to detect unwanted interdependencies between implementation modules, by declaring a static constructor in those modules that should not depend on other implementation modules.

// Student.d
import IStudent;
import ITeacher;

class Student : IStudent {
// ...
	void ask(ITeacher teacher)
	{
		teacher.teach(this);
	}
	static this() {}
}

// Teacher.d
import ITeacher;
import IStudent;

class Teacher: ITeacher {
	void teach(IStudent student)
	{
		student.learn(this, "knowledge");
	}
	static this() {}
}

Now if Teacher and Student were to accidentally import each other, I would get a runtime error:
object.Exception: Cyclic dependency in module Student

However, I wish the situation could be detected at compile-time.
-- 
Cheers,
Per Å.
November 29, 2010
On Sun, 28 Nov 2010 09:58:42 -0500, Per Ångström <d-news@autark.se> wrote:

> On 2010-11-15 16:52, Steven Schveighoffer wrote:
>> On Sun, 14 Nov 2010 05:28:29 -0500, Per Ångström <d-news@autark.se> wrote:
>>> Fortunately, by separating interface and implementation, one can at
>>> least keep the implementation modules free from interdependencies.
>>> It's not perfect but it's not a complete mess either.
>>
>> It is one way to keep dependencies down. And less dependencies means
>> less coupling, meaning importing one module doesn't make you import (and
>> include the code for) many other modules. The compiler isn't yet mature
>> enough to trim out some code that is never used.
>
> I have found a way to detect unwanted interdependencies between implementation modules, by declaring a static constructor in those modules that should not depend on other implementation modules.
>
> // Student.d
> import IStudent;
> import ITeacher;
>
> class Student : IStudent {
> // ...
> 	void ask(ITeacher teacher)
> 	{
> 		teacher.teach(this);
> 	}
> 	static this() {}
> }
>
> // Teacher.d
> import ITeacher;
> import IStudent;
>
> class Teacher: ITeacher {
> 	void teach(IStudent student)
> 	{
> 		student.learn(this, "knowledge");
> 	}
> 	static this() {}
> }
>
> Now if Teacher and Student were to accidentally import each other, I would get a runtime error:
> object.Exception: Cyclic dependency in module Student
>
> However, I wish the situation could be detected at compile-time.

It could only be detected at link-time, and the linker doesn't do any custom processing, it just links.

But in any case, I still don't understand why you want to avoid interdependencies.  D is supposed to handle them well.

-Steve
« First   ‹ Prev
1 2