Re: [Haskell-cafe] mutually recursive modules

On Fri, 24 Sep 2004, Malcolm Wallace wrote:
Hugs doesn't support mutually-recursive modules at all. Ghc and nhc98 support them only if you hand-write a .hi-boot file to bootstrap the compilation. I would guess that better support from the mainstream implementations is unlikely, because it would be a large amount of effort for a situation which occurs only very rarely, and for which there is a relatively easy workaround.
Namely? The only workaround I know of is putting all type declarations and functions that depend on each other into one module, import them in the modules they logically belong to and document that the objects should be imported from the redirecting modules rather than from the modules they are defined in. I know of two projects where different tasks had to be merged into one module because of the lack of mutually recursive imports, namely functionalMetaPost and Haskore. It's interesting how other languages solve this problem. In Modula-3 it is solved by explicit module interfaces and partial revelation. One can define type T in module A and type T in module B very abstractly as pointers to something and the complete data structures (which may contain references to either interface) are usually revealed in the implementations of the modules. I'm curious how Oberon solves it, because it doesn't need explicit interfaces but derives them from the implementation files. Probably one can say that Oberon extracts something like a .hiboot file from every module file automatically. Why can't GHC and Hugs go this way?

Henning Thielemann
a situation which occurs only very rarely, and for which there is a relatively easy workaround.
Namely? ...
See below.
It's interesting how other languages solve this problem. In Modula-3 it is solved by explicit module interfaces and partial revelation.
Essentially this is how ghc and nhc98 solve it too. You must write an explicit module interface (the .hi-boot file). This is the easy workaround. One benefit of these systems over Modula is that you /only/ need to write explicit interfaces where there is module recursion - in all non-recursive contexts, the interface generation is automatic.
I'm curious how Oberon solves it, because it doesn't need explicit interfaces but derives them from the implementation files. Probably one can say that Oberon extracts something like a .hiboot file from every module file automatically. Why can't GHC and Hugs go this way?
The main obstacle is that Haskell systems generally process one file/module at a time. To extract an interface from each module first, before further processing, would not only require a second pass over the source files, but to load all of them simultaneously, which can be rather space-hungry. Anyway, as Diatchki, Jones, and Hallgren [1] have demonstrated, it is certainly possible to build a Haskell compiler that deals properly with recursive modules. Regards, Malcolm [1] Iavor S. Diatchki, Mark P. Jones, and Thomas Hallgren "A Formal Specification for the Haskell 98 Module System" ACM Haskell Workshop, 2002, Baltimore http://www.cse.ogi.edu/~diatchki/hsmod/

On Fri, 24 Sep 2004, Malcolm Wallace wrote:
The main obstacle is that Haskell systems generally process one file/module at a time. To extract an interface from each module first, before further processing, would not only require a second pass over the source files, but to load all of them simultaneously, which can be rather space-hungry.
I don't see where it is necessary to load more modules than usually. Modula doesn't support cycles in the dependencies. Each implementation depends only on interfaces of other modules and the interfaces must not depend cyclicly. Haskell compilers can reduce the problem to the situation of Modula by extracting the interface from the implementation. Maybe it's better if the user explicitly allows mutually recursive modules with a command line option which will invoke an automatic interface generation. The generation of an interface file should depend only on the corresponding implementation file, as it is the case for manual .hiboot creation. For mutually recursive functions across modules the signature had to be specified by the user to avoid the simultaneous loading of all modules.

There is no reliable way to extract the interface from an implementation short of compiling the implementation. So the only "proper" way to handle mutually recursive module is to have hand-written interface files where there is a cycle. This is how Haskell used to be, but alas, hand written interface files fell out of fashion at some point. -- Lennart On Fri, 24 Sep 2004, Henning Thielemann wrote:
Date: Fri, 24 Sep 2004 16:17:24 +0200 (MESZ) From: Henning Thielemann
Reply-To: Henning Thielemann To: Malcolm Wallace Cc: haskell-cafe@haskell.org Subject: Re: [Haskell-cafe] mutually recursive modules On Fri, 24 Sep 2004, Malcolm Wallace wrote:
The main obstacle is that Haskell systems generally process one file/module at a time. To extract an interface from each module first, before further processing, would not only require a second pass over the source files, but to load all of them simultaneously, which can be rather space-hungry.
I don't see where it is necessary to load more modules than usually. Modula doesn't support cycles in the dependencies. Each implementation depends only on interfaces of other modules and the interfaces must not depend cyclicly. Haskell compilers can reduce the problem to the situation of Modula by extracting the interface from the implementation. Maybe it's better if the user explicitly allows mutually recursive modules with a command line option which will invoke an automatic interface generation. The generation of an interface file should depend only on the corresponding implementation file, as it is the case for manual .hiboot creation. For mutually recursive functions across modules the signature had to be specified by the user to avoid the simultaneous loading of all modules.
_______________________________________________ Haskell-Cafe mailing list Haskell-Cafe@haskell.org http://www.haskell.org/mailman/listinfo/haskell-cafe
-- Lennart

G'day all.
Quoting Lennart Augustsson
There is no reliable way to extract the interface from an implementation short of compiling the implementation.
...which strongly suggests a bug in the language.
So the only "proper" way to handle mutually recursive module is to have hand-written interface files where there is a cycle.
At the risk of stating the obvious, there's no reason (apart from incompatibility with the current language, of course) why this has to be a separate file. Cheers, Andrew Bromage

One other thing while I think of it... I wrote:
At the risk of stating the obvious, there's no reason (apart from incompatibility with the current language, of course) why this has to be a separate file.
I should also have used the phrase "double maintenance problem".
Cheers, Andrew Bromage

On 24-Sep-2004, Lennart Augustsson
There is no reliable way to extract the interface from an implementation short of compiling the implementation.
There's a difference between type inference and compilation. -- Fergus J. Henderson | "I have always known that the pursuit Galois Connections, Inc. | of excellence is a lethal habit" Phone: +1 503 626 6616 | -- the last words of T. S. Garp.

The original Haskell design included interface files usually with names like Main.hi IIRC, Yale Haskell expected them to be hand-written while HBC and GHC machine-generated but allowed the careful user to write them by hand. Sometime around Haskell 1.4 or 98, they were dropped because Yale Haskell had died and there seemed little point in specifying the format of machine-generated files. (Hugs' support for modules was added long after the original Hugs design - it was hard getting it to do as much as it does. At one point, I almost had it supporting mutually recursive modules but I couldn't quite get all the dependencies in the typechecker under control so I settled for just supporting module namespaces and qualifiers.) It is _almost_ possible to generate .hiboot files automatically. If you see a data declaration in the .hs file, copy it to the .hiboot file, if you see a function prototype, copy it, etc. That is, you generate a .hiboot file by just deleting all function definitions. One of the tedious bits is that that you need to do a tricky least fixpoint calculation just to figure out what is exported by a set of mutually recursive modules if they each reexport their imports. Generating .hiboot files by just deleting function definitions fails if there is no prototype for an exported function. A crude approach is to assume the type (\forall a. a) for any function with no prototype but, although this is sound (I think), it will cause valid programs to be rejected. This problem could be overcome by specifying it as the correct behaviour :-) (Is it sound to use (\forall a. a) in the presence of higher-kinded types and all those other extensions?) Hmmm, maybe someone familiar with the Haskell-source libraries could quickly hack up a program to generate .hiboot files automatically and make it available - that would quickly find the issues (and also identify any weaknesses in ghc's error checking of .hiboot files :-). -- Alastair Reid
It's interesting how other languages [handle mutually recursive modules] problem. In Modula-3 it is solved by explicit module interfaces and partial revelation. One can define type T in module A and type T in module B very abstractly as pointers to something and the complete data structures (which may contain references to either interface) are usually revealed in the implementations of the modules. I'm curious how Oberon solves it, because it doesn't need explicit interfaces but derives them from the implementation files. Probably one can say that Oberon extracts something like a .hiboot file from every module file automatically. Why can't GHC and Hugs go this way?

Alastair Reid wrote:
Generating .hiboot files by just deleting function definitions fails if there is no prototype for an exported function. A crude approach is to assume the type (\forall a. a) for any function with no prototype but, although this is sound (I think), it will cause valid programs to be rejected.
Huh? How can that ever be sound? Are you sure you didn't mean (\exists a.a)? - Andreas -- Andreas Rossberg, rossberg@ps.uni-sb.de Let's get rid of those possible thingies! -- TB

Alastair:
A crude approach is to assume the type (\forall a. a) for any function with no prototype
Andreas:
Huh? How can that ever be sound?
You're right, it's not - my mistake. I guess that leaves two options: 1) Insist on a prototype for any exported function. 2) Insist on a prototype for any imported function which is used. The latter is more in keeping with Haskell's lazy checking of import clashes. (There's a third option where you assume a type which you don't know anything about like (\exists a. a) but I'd be surprised if this let you typecheck any more useful programs.) -- Alastair

An anecdotal note - hbcc (the front end to the pH and Eager Haskell compilers, and also of GRIN) contained several mutually recursive modules both in the compiler and in the prelude. One of the best things we ever did was get rid of the mutual recursion. The resulting refactoring helped us to group related pieces which had (for historical reasons) ended up scattered. It also cut our rebuild time dramatically, and let us do cross-module inlining and optimization safely. In short, using mutual recursion was probably a bad idea, and we found we were better off without it. -Jan-Willem Maessen

On 24-Sep-2004, Jan-Willem Maessen
An anecdotal note -
hbcc (the front end to the pH and Eager Haskell compilers, and also of GRIN) contained several mutually recursive modules both in the compiler and in the prelude.
One of the best things we ever did was get rid of the mutual recursion. The resulting refactoring helped us to group related pieces which had (for historical reasons) ended up scattered. It also cut our rebuild time dramatically, and let us do cross-module inlining and optimization safely.
In short, using mutual recursion was probably a bad idea, and we found we were better off without it.
Here's another anecdotal note. When refactoring the Cryptol compiler, in particular when abstracting some interfaces to use type classes, I found that (1) In the intermediate stages of the refactoring, I needed to use mutual recursion in quite a few different places. This was a pain because it required manually writing .hi-boot files. This was also problematic because my colleagues were reluctant to have our sources depend on a rather poorly supported feature of Haskell, and were also reluctant to have implementation-specific files like .hi-boot files in our CVS repository. As a result, I did not check in the intermediate stages of the refactoring into our CVS repository. These factors made the development process more difficult, made the code review process more difficult, and increased the likelihood of a CVS conflict. (2) Although most of the mutual recursion occurred only in the intermediate stages of the refactoring, some of the mutual recursion remained at the end of the refactoring, forcing two modules with only the smallest degree of coupling to be combined into a single module. The two modules in question were (a) the module which defines the calling interface between the Cryptol front-end and the various Cryptol back-ends, and (b) the module which defines the structure which records the settings of command-line options. Here (a) depends on (b) because one of the parameters which is passed to the back-ends is the command-line option settings, and (b) depends on (a) because one of the option settings is the currently selected back-end (represented using an existentially quantified typeclass-constrained type). So while I agree that unrestrained use of mutual recursion can lead to poor structuring, I think that in many cases mutual recursion _is_ useful, either as an intermediate stage during refactoring or as a final stage. A compiler warning about mutual recursion (with some way to suppress it in individual cases) might be useful, but a compiler error or the requirement to manually write separate .hi-boot files is not helpful, IMHO. -- Fergus J. Henderson | "I have always known that the pursuit Galois Connections, Inc. | of excellence is a lethal habit" Phone: +1 503 626 6616 | -- the last words of T. S. Garp.

On Mon, Sep 27, 2004 at 10:46:25AM -0700, Fergus Henderson wrote:
(2) Although most of the mutual recursion occurred only in the intermediate stages of the refactoring, some of the mutual recursion remained at the end of the refactoring, forcing two modules with only the smallest degree of coupling to be combined into a single module.
The two modules in question were (a) the module which defines the calling interface between the Cryptol front-end and the various Cryptol back-ends, and (b) the module which defines the structure which records the settings of command-line options. Here (a) depends on (b) because one of the parameters which is passed to the back-ends is the command-line option settings, and (b) depends on (a) because one of the option settings is the currently selected back-end (represented using an existentially quantified typeclass-constrained type).
As a programmer not necessarily speaking about Haskell, I also find that mutually dependent modules are often natural in practice, and that avoiding them requires excessive and awkward factoring. Eg, a configuration module C and a database module D, in which D depends on C because the configuration contains the database to use, and C depends on D because configuration data (other than which database to use!) can come from the database. I think that on principle, haskell implementors should not doubt that programmers will find good use for mutually recursive modules if they are available in a convenient form. Andrew

On Monday 27 Sep 2004 8:27 pm, Andrew Pimlott wrote:
On Mon, Sep 27, 2004 at 10:46:25AM -0700, Fergus Henderson wrote:
(2) Although most of the mutual recursion occurred only in the intermediate stages of the refactoring, some of the mutual recursion remained at the end of the refactoring, forcing two modules with only the smallest degree of coupling to be combined into a single module.
The two modules in question were (a) the module which defines the calling interface between the Cryptol front-end and the various Cryptol back-ends, and (b) the module which defines the structure which records the settings of command-line options. Here (a) depends on (b) because one of the parameters which is passed to the back-ends is the command-line option settings, and (b) depends on (a) because one of the option settings is the currently selected back-end (represented using an existentially quantified typeclass-constrained type).
As a programmer not necessarily speaking about Haskell, I also find that mutually dependent modules are often natural in practice, and that avoiding them requires excessive and awkward factoring.
This is a subject I'd been meaning to whine about, so now seems like a good time for a "me too" post. I agree with Henning,Fergus,Andrew. It's annoying when what should be a fairly straight forward exercise in adding a new function to a module turns out to far less straight forward because you've introduced circular dependency, forcing you to either refactor modules which have become "old and familiar friends" (you know where everything is) for no good reason or start mucking about with hiboot files. I don't agree with the view that mutual recursion between modules is symptomatic of a poor design which badly needs refactoring anyway. On the contrary, I think perfectly natural and useful for modules which are part of the same package. If anything this problem _makes_ me produce a poor design by putting some functions in the "wrong" module. OK, it doesn't really make me do this. There are alternatives, but I suspect that in reality most programmers will take the path of least resistance (certainly I will, given the opportunity :-) Regards -- Adrian Hey

G'day all.
Quoting Henning Thielemann
Why can't GHC and Hugs go this way?
As Alastair noted, the problem is that Haskell allows you to export symbols from a module whose types are unknown unless you type-check modules that it imports. Simple example: module A where import B a = b module B where import A b = a This is only a problem for exported symbols which have no type declarations. As Alastair said:
I guess that leaves two options:
1) Insist on a prototype for any exported function. 2) Insist on a prototype for any imported function which is used.
The latter is more in keeping with Haskell's lazy checking of import clashes.
That's true, but you have to ask why you're exporting a function if you're not going to use it. On the other hand, in the presence of Haskell's "export everything" feature, it makes a certain amount of sense. Cheers, Andrew Bromage
participants (11)
-
Adrian Hey
-
ajb@spamcop.net
-
Alastair Reid
-
Andreas Rossberg
-
Andrew Pimlott
-
Fergus Henderson
-
Fergus Henderson
-
Henning Thielemann
-
Jan-Willem Maessen
-
Lennart Augustsson
-
Malcolm Wallace