Local modules#283
Conversation
|
I consider #205 obsoleted by this proposal. My only hope is that we have a volunteering implementor sooner, rather than later! |
|
The only part from #205 that is missing is details of module namespace merging & reexports, which could potentially be migrated over, I think. I mean, surely we want to be able to allow the qualified names to be transparently re-exported, right? |
| 2. Introduce a new declaration form (allowed only at the top level of a | ||
| module) to declare new modules called *local modules*. Here is the BNF:: | ||
|
|
||
| decl ::= ... | [ 'import' ] 'module' modname [ export_spec ] 'where' decls |
There was a problem hiding this comment.
Is the [ 'import' ] correct? I imagine import and where do not go together. There are a few other suspicious [ 'import' ]s below as well.
|
I've just started to read the proposal, but ran into this:
That sort of change simply isn't local, particularly because a class and a type family are genuinely different. I imagine you can repair this bullet point. |
|
@goldfirere Thanks for writing this proposal and working in this direction! I appreciate a lot the work on UX improvements to imports and reexports. However, when reading this proposal, I couldn't get rid of the feeling that this is two proposals in one:
I personally care a lot about the following feature from your proposal and from the previous proposal about Structured imports. module MyPrelude ( qualified module BL
, qualified module BS
, Set
, qualified module Set ) where
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString as BS
import Data.Set ( Set )
import qualified Data.Set as SetAs a maintainer of one of the popular alternative preludes, I find this feature extremely valuable. This can be a 2x improvement to my daily programming in Haskell. I could even say it's a 10x improvements to keep up with modern trends. I just can't put into words how important this minor syntactic feature and how much more convenient it makes to develop programs in Haskell. On the other hand, I have some troubles understanding the motivation and usefulness behind local modules and what exact problem do they solve. And the proposal is mostly about this local modules feature (hence the name). However, this feature looks complicated with a lot of specification tasks. And according to my experience, there probably going to be a long discussion on improving specs for this feature to cover various corner cases, bikeshedding the syntax, discussing a motivation behind such a compiler complication, etc. All this can stop such a useful feature as qualified reexports from being merged which is a shame. Would it make sense to split this proposal into two?
I don't see how the part of |
|
I've wanted One of my use cases then was autogenerated datatypes to correspond to tables of a database -- each a record, with fields named for each of the columns. Two tables would want to share column names. But to do so, each would have to be declared in its own module, each designed to be imported qualified. So a single schema would potentially give rise to many files, one per table. With a proposal like this, each table can get its own local module within the same actual file -- much nicer! The ability to define "hidden" helper datatypes is great too for modules designed to be imported unqualified. I've often written modules where I want to export "all but three" things or the like, and its irritating to have to write a whole export list instead of just keeping those three helper things entirely local. I think that in fact many modules I've worked on have this characteristic, and in all those cases, this will really cut down on verbosity. |
nomeata
left a comment
There was a problem hiding this comment.
I miss some discussion about the interaction with Backback. At least backpack also has a multiple-module-per-file feature, right?
| 7. Every ``class``, ``data``, ``newtype``, ``data instance``, and ``newtype | ||
| instance`` declaration implicitly creates a new local module. The name of | ||
| the local module matches the name of the declared type. All entities (e.g., | ||
| method names, constructors, record selectors) brought into scope within the | ||
| declaration, including the type itself, are put into this local module. |
There was a problem hiding this comment.
This seems to be a nice convenience feature on top of the existing proposal, but is not crucial to it, right, as you can emulate that behavior with a few extra lines of wrapping the type in a module.
(Not that I dislike it, just asking for better understanding of the design space.)
There was a problem hiding this comment.
I agree that this seems orthogonal to the main proposal, and perhaps best included as an additional extension on top of it.
| If the ``import`` keyword is included, then all entities brought into | ||
| scope qualified are also brought into scope unqualified. |
There was a problem hiding this comment.
Does this unqualify one level of module nesting depth? If Foo.hs exports module M1 which exports M2 which exports bar, and I write
import Foo ( import module M1 )
is M2.bar or bar in scope?
|
|
||
| * Other than corner cases around ambiguity, this proposal is backward compatible; it is not "fork-like". | ||
|
|
||
| * Proposal `#160`_ allows users to suppress field selectors, thus ameliorating a small part |
There was a problem hiding this comment.
| * Proposal `#160`_ allows users to suppress field selectors, thus ameliorating a small part | |
| * Proposal `#160`_ allows users to suppress field selectors, thus `ameliorating <https://www.merriam-webster.com/dictionary/ameliorate>` a small part |
michaelpj
left a comment
There was a problem hiding this comment.
I am enthusiastically in favour of this proposal. I have a half-written draft of almost exactly the same proposal sitting on my hard drive...
As an anecdotal point in its favour, I implemented a very similar scheme for another programming language and it was wildly successful. The simple approach of "modules have names like everything else, and you can import them and export them as normal" turns out to be very flexible.
I also found that it formalizes quite nicely as a set of deduction rules for constructing the relations in question. (The existing formalization paper also reads to me like an algorithmic version of some deduction rules.) Such a formalization might be a nice contribution for this proposal, and would make it nice and clear what the delta is.
| 7. Every ``class``, ``data``, ``newtype``, ``data instance``, and ``newtype | ||
| instance`` declaration implicitly creates a new local module. The name of | ||
| the local module matches the name of the declared type. All entities (e.g., | ||
| method names, constructors, record selectors) brought into scope within the | ||
| declaration, including the type itself, are put into this local module. |
There was a problem hiding this comment.
I agree that this seems orthogonal to the main proposal, and perhaps best included as an additional extension on top of it.
| of the enclosing ``instance`` declaration: the ``data``\/\ ``newtype`` | ||
| module is *not* nested within the class module. | ||
|
|
||
| 8. Local modules may be extended via the declaration of another local module |
There was a problem hiding this comment.
I'm not sure why we need this, but I have a guess: it's to preserve the existing behaviour when modules are from different sources are renamed to the same name (e.g. import F as N; import G as N). If that is the main reason for including this, then I think we should instead consider simply banning this implicit merging if LocalModules is enabled, or have it give an ambiguous name error.
| Haskell currently has three related restrictions: | ||
|
|
||
| * *Modules*, a set of declarations that share a namespace and perhaps are | ||
| surrounded by an abstraction barrier, coincide with *source files*, the |
There was a problem hiding this comment.
I think it would help to have a term for "modules which are a source file". Perhaps "file module"?
I would also mildly prefer it if we chose our terminology such that we could use "module" unqualified to refer to the namespacing concept, and some other term for the rather special modules which you get when declaring them at the top-level of a source file.
|
|
||
| Note that the declaration form includes the word ``module`` to distinguish | ||
| it from a normal ``import`` which induces a dependency on another file. An | ||
| ``import module`` declaration cannot induce a dependency. |
There was a problem hiding this comment.
Big 👍
In an ideal world I would love it if we had two keywords, something like open (brings names from a module into scope, corresponding to import module) and link(incurs a dependency on a file module and brings the fully qualified module name into scope), with our current import being a combination of the two.
| 5. A local module declaration brings into scope names listed in its export | ||
| list. These names are always brought into scope qualified by the local | ||
| module name, unless that module name is ``_``. If the declaration includes | ||
| the ``import`` keyword, the names are also brought into scope unqualified. |
There was a problem hiding this comment.
Do we need the optional import on a module declaration? All it does is save one line to go import <modname>.
| y = T.x | ||
|
|
||
| There will be two identifiers ``T.x`` in scope: both the one imported from ``T`` and the record selector | ||
| in the type ``T``. This situation will lead to an error, as do other sources of ambiguity. |
There was a problem hiding this comment.
👍
I think there is a good design principle here, something like:
Allow ambiguous names to be declared but not referenced, and rely on module renaming to let the user resolve them if necessary.
| There will be two identifiers ``T.x`` in scope: both the one imported from ``T`` and the record selector | ||
| in the type ``T``. This situation will lead to an error, as do other sources of ambiguity. | ||
|
|
||
| * The ability to detect dependencies of a module by parsing only a prefix of the module is retained. |
|
|
||
| However, it would be nice to separate the treatment of compilation units and source files, | ||
| as well. This would allow, for example, the inliner and specializer to make decisions with | ||
| respect to more definitions (if the compilation unit is larger than the source file). |
There was a problem hiding this comment.
It would also allow a much simpler implementation of mutually recursive modules: the modules in a SCC form a compilation unit and are processed together, using the usual fixpoint computation.
|
|
||
| 2. Haskell currently requires three distinct concepts to coincide: *compilation units* are the | ||
| chunks that go through the compiler all at once, *source files* are distinct files on disk, | ||
| and *modules* are groups of related definitions and can define an abstraction barrier. |
|
|
||
| I see a few future directions along these lines, but I leave it to others to flesh these out. | ||
|
|
||
| 1. We can imagine *parameterized local modules*, where all the functions defined therein share |
There was a problem hiding this comment.
In particular, I think this proposal is a common subset of almost any serious parameterized module proposal, so is a pretty reasonable thing to implement.
I hope this isn't out of discouragement! Many of the ideas here grew due to my (quiet) following of that proposal.
Do you have an example? I'm not sure what this means. Feel free to link to an example in your proposal or discussion instead of creating one fresh.
Yes. The
I disagree. Imagine we have class C a where
meth :: a -> aI might later prefer type family C a :: Constraint where
C Int = ...
C Bool = ...
C a = ...
meth :: C a => a -> a
meth = ...From users' standpoint, these are used equivalently (if users don't write instances -- maybe that's the part that should be clarified).
Very good observation. Somehow, this all formed in my head as one idea, but I agree with you that it's really two proposals. But they do interact. For example, if we just had qualified-exports, you couldn't export locally defined identifiers qualified. You would need a shim module just to rejigger the exports. And if you want multiple different qualifications (imagine a module exporting a datatype, some common operations, and then both
I don't think so. Backpack seems all about
Thanks for the link! I was not familiar with that paper. I cannot say I'm going to run off and formalize this proposal in the style of that paper right now, but I agree that such a thing would be nice. I would welcome and review closely a contribution in this form from another member of the community.
It was to allow mixing other definitions with e.g. constructors of a type declaration, so that the other definitions could be in the implicit module created along with the type. I've added some text about this. This feature is very inessential. I believe I have responded to other points in an update I am about to push. Thanks for all the commentary! |
As a starting example, let's discuss entries #6 and #8 in the exports table: https://github.com/deepfire/ghc-proposals/blob/master/proposals/0000-structured-imports.rst#2124comparative-case-analysis The idea is that if we want first-class support for qualified names, we should be able to re-export them without explicitly mentioning -- just as for the regular names. |
|
Line 6: module M ( qualified O ) where
import N
import PYou suggest that this should export all the identifiers qualified with I believe that same behavior is included in my proposal, as part of specifications 8 and 11. (8) says that local modules may be extended. Line 8: module M ( module N ) where
import N
import qualified P as NYou suggest this should export all of I actually disagree with this behavior, as it clashes with the existing semantics for exporting a One thing my proposal does not provide is the ability to export a qualified module as unqualified. Individual identifiers may, of course, be listed in an export list, but I offer no way to unqualify an entire module in an export list. Perhaps we could add Have I accurately captured your examples? Thanks for including them! |
I do think it would be good to consider it in the related work at least.
Very fair. I will try and find some time to write something down. |
Citing (8) from "Structured imports/exports":
So
That's a fair point, I agree that the #205 made a mistake of including the unqualified names when they weren't imported from their origin module.
Sounds great, that point is covered as well, then. This raises one question though -- would we still export |
|
I found the structure of the proposal quite hard to understand because it starts from some perceived problems rather than describing the solution with an example. I find section 1 too high-level and section 2 too low-level. In the motivation section, point 2 seems quite unrelated to point 1. There seems to be two different things going on,
These should be separate proposals in my opinion and I am sympathetic to both ideas. However, I wonder how this correct treatment of records interacts with the many other proposals to do with field selectors which appear to move in the other direction. Specific comments
Overall I am really keen to see this proposal move forwards, I think it would be a large improvement to the language and seems to be quite elegant. In order for things to move forward I think the proposal needs to be clarified significantly to separate the two distinct points. |
|
I also didn't read any of the comments so if these questions are just from reading the proposal. |
The compilation unit is still the file, even thought there may be multiple modules in the file. I think that means that:
|
|
My apologies, but in the interest of moving things forward -- Do we have consensus on that this proposal should be split? If yes, what should be the split? From what I can see there are several components: Citing @mpickering:
..and also, the part that concerns me the most -- allowing modules (or, depending on your perspective, qualified names) to travel across the import/export boundary -- the part that #205 was covering. |
|
Just a quick note to say I'm on holiday this week and will update this next week. Sorry to lose momentum here! |
|
See the impending update to the proposal for further incorporation of your remarks.
Yes. I've added some clarification in this regard.
I agree that these are separable. But they go nicely together. I have added a point under Alternatives that suggests dropping the second point above. I think the proposal is fine without this extra.
I've added some text about this at the end of the Effects section.
You can nest as deeply as you like. Note that it says "top level of a module", not "top level" or "top level of a file". The point here is that you cannot declare a local module within a
This is a good point. But Java, C++, and C# (at least) do this all the time. (Yes, I know it's not semantically significant there. The conventional style for these languages do it anyway.) I'm happy to consider alternative syntaxes, but given the success of long indented regions elsewhere, I'm not going to worry unduly about this.
Very true -- which is why I don't volunteer to implement. The good news is that the entire proposal deals only with the renamer. Indeed, I imagine if we gave GHC's datatype More broadly, the lack of concern of implementation is intentional. This proposal strikes me as a convenient, compositional approach to modules. If it's hard to implement, then perhaps that means that the implementation is not working as well as it can for us. There is also the possibility of partial, future-compatible implementations of this proposal that would be easier to fit with today's GHC. One choice is made for practicality: the fact that the new
The proposal doesn't mention this because it's not true. Local modules do not affect compilation order or dependency. This allows local modules to be mutually recursive without issue. I've added an Alternative considering your implied proposed design.
This question seems to suggest that you're thinking of a world where "module" = "compilation unit". This proposal moves us away from this coincidence.
I think the second point above is really confined to one bullet (point 7) and can easily be removed. Perhaps also the introduction bit (above Motivation) is overlong, and threw you off. Are there other areas of clarification needed?
Hooray! :) I don't yet feel motivated to split this proposal. Instead, I have now included a menu of Alternatives; many of these suggest dropping individual points of the specification in this proposal. I personally like the proposal in its current form, but I'm fine with enacting these Alternatives. /remind me in a week to submit this if there has been silence. |
|
@goldfirere set a reminder for Nov 6th 2019 |
|
@goldfirere I have yet to really dive into this, but I do feel the need for some caution. With the Dependent Haskell work, we have your thesis to lay out the overall trajectory in great detail so I have no problems with individual proposals along that path. As much as I love the overall thrust of the proposal and the future work, I don't really know exactly where we are going and am worried about making mistakes we will regret. I'm not saying we need another thesis but....I would like to have something. If there are ways we can improve the "compilation units = modules = files" assumptions and tech debt in GHC without committing to new interfaces, that is also good. [For example, I think Haskell's structured module names today could well be viewed as a mistake. It would be much cleaner to say " I really hate being any less than completely ecstatic about this; I really do think this is the most important way to develop the language along with Dependent Haskell. |
|
There is no grand plan (that I have) around modules and compilation units. (Maybe that's the problem you're worried about!) But perhaps what you're suggesting is that we should work out the compilation unit stuff before committing to this proposal, to make sure it all works together. I'm fine with that. But unless someone stands up to say they are going to do that (and in a somewhat timely fashion), I don't want to stop this proposal indefinitely for some grand master plan to appear. |
|
I think this a proposal is great. I have a specific question regarding the behavior of the flag. Does it need to be enabled to import qualified modules? I consider a user having the following import: right now the user can be sure that this will never include something like So: The LocalModules flag is certainly needed for exporting qualified modules. But will it also be required for importing qualified modules? If yes this will tend to force the extension on library users. If no this will change the possible meanings of an import statement in Haskell quite a bit. Someone not familiar with this change might get very confused by a e.g. It‘s probably a cost worth paying. But I hadn‘t seen it mentioned before. |
|
I know this is dormant, but I didn't see this feedback noted anywhere prior... It appears that most the examples of problematic imports are of the form: I don't like the solution of declaring such a pattern at the module exports level, because it doesn't actually clarify the problem, namely that Haskell has no way to associate functions with the types they work over. Also, note that in order to use a function, I have to look at the declaring module's exports, which I think is not great design. How about something like (modulo syntax): Such a pattern has worked fantastically in other languages (namely object oriented languages) because it aids in code organisation and discoverability. The biggest thing I am seeing in the comments here is this pattern - and I think something like this solution is (a) minimal (b) incremental (c) a good design. |
|
The problem with this is (as is in OOP languages) that all functions need to be declared at the site of the datatype definition. With local modules and the merging of modules that Haskell does, you can extend the functions associated with Set |
|
For such extensions, you could simply add a new qualified import no? |
|
As a user yes, but you could not export |
|
I wonder how this would interact with |
|
Not at all. |
module A ( module M1, module M2, module qualified M3, module qualified M4, module A ) wherecould be module A ( module M1, module M2, module M3 qualified, module M4 qualified, module A ) wherewhich would allow sorting without lumping qualifieds somewhere in-between: module A
( module A
, module M1
, module M2
, module qualified M3
, module qualified M4
, module X
, module Y
) where |
|
Thanks to @goldfirere for all his hard work trying to push this through! I'm very interested in this proposal succeeding, but I wanted to try out a last point in the design space. It's available in #564. The proposal, among other things, tries to solve the remaining two problems in (#283 (comment)) by disallowing multiple local modules (here named namespaces) with the same name. |
|
Hello everyone! I just wanted to write to signal my intentions to officially take over this proposal and help get it over the finish line. To quote another member of the community, "I'm sort of the village idiot" amongst you all, but I believe this proposal would be immensely helpful for Haskell and I want to give it my best shot. I'm not committing to taking this on just yet, I want to first read through the entire rendering of the proposal and some of the discussion to make sure I understand things first. But just so it's known that this is being thought about behind the scenes Thanks all! I'll chime back in when I've finished wrapping my mind around it all |
|
Regarding point 1 as raised by Richard:
Shouldn't this just result in an error such as I think this in line with what would happen in current Haskell if you tried to declare the same bindings multiple times in a top-level-module. This is also how other languages handle this situation. E.g: In c# namespace Foo {
class Bar {
int x;
}
}
namespace Foo {
class Bar {
int y;
}
}The following code yeilds the error
I guess what I'm saying is that I disagree with this particular statement. Local modules should be allowed to declare variables without regard for the scope of other modules; but if you're extending a module, you should be bound by the rules of not re-using variables within that modules scope, the same as if you were working directly within a single module declaration Academically, I'm quite naive, so I assume I may be missing some crucial understanding here, but this seems like the straight forward path to handling this situation as far as I can tell |
|
@vanceism7 thanks for help. I'm willing to advise on this proposal. Get in touch if you have questions. |
|
Hey everyone, I wanted to chime back in with an update. I spent a good amount of time reading through this proposal, thinking about it, thinking up alternatives, and just generally chewing on the whole thing. @aspiwack and I also had a bit of discourse on this proposal over email, but unfortunately, my life circumstances have changed and I find myself no longer having time to put efforts towards this for the time being. I wish I could've helped at-least move this proposal closer to passing the finish line, but I'm hugely greatful to @aspiwack for spending so much time discussing this with me. If anyone else decides to take this on or is curious, I'm happy to summarize some of the key points of what we talked about. Thanks again everyone! |
|
I agree with #283 (comment) I think it's easy to see how (qualified) module re-exports can be a feature all of their own. And I think splitting this aspect into it's own piece of work would be more likely to make both things happen eventually as well. However I think richard gave a very detailed treament of the semantics such re-exports should have and I think it should be workable as presented here without bringing local modules into the mix. They are good to have! |
|
It probably is implied by the formalities here, but I wondered if that means we allow ambiguous module re-exports. That is today we allow: But under this proposal what about: I admit I have not worked through the formalities in depth, but I think as written this would be allowed, as it would expose the declarations of Assuming that's the case I think it would be better to disallow this. That is treat exports of Modules similar to exports of symbols. The exact restriction being that a qualified re-export most refer to one specific module. If a module name refers to multiple modules attempts to re-exports are rejected. In other words we should allow re-exports of modules. But not of module names. I think it has some advantages for tooling and developers in terms of being able to trace where exports originate from. Potentially leading to better error messages. And making it easier for things like Haddock to generate links to the appropriate sources/modules. But it also means we don't shut the door on the ideas of the modules as first class entities proposal. The downside being that sometimes one will have to define an additional module to bundle declarations just to re-export it. Rather then bundling those declaration ad-hoc under a module name and exporting that module name. But that doesn't seem that bad. Especially if we get a full implementation for LocalModules. It's also backwards compatible to relax that restriction down the road should we decide to do so. |
|
I have been tracking agda/agda#7656 for a while, and I think the implementation of modules in Agda can be used as a reference to predict what might go wrong here in Haskell. As we can see in the linked issue, qualified imports in Agda currently behaves slightly differently from a local module. Most notably, it is allowed to import with the same alias AFAIK qualified imports cannot be reexported in Agda, you have to explicitly define a public local module. I do not fully understand the exact reasons, but I feel it has to do with the deferred naming conflict report. I guess this might also stand in favour of the idea that reexport of "module names" should be disallowed. |
I agree, at least you shouldn’t be able to reexport ambiguous names. If both If
An alternative is to add a way to disambiguate. I don’t propose we add this, but the simplest solution that comes to mind is to just allow reexport of conflicting names as long as alternate paths to the same things are also exported. Then overlapping APIs, such as |
It certainly would be a nice feature on its own. And I'll reiterate my earlier offer: while I don't have the time to beat such a proposal into shape (either the full proposal or just the re-export), I'm happy to serve as a council. I don't have the details paged in at the moment, so I don't remember if the difficulties that were time-consuming to iron out would be absent of a pure re-export proposal. |
This is an alternative to #205 that appears to be more compositional.
Rendered