Aaron observed that the module
declaration contains exclusively redundant information. The module name is determined by the file name, and the exposed values are determined by the module comment. So why have both?!
Here is the proposed module header in this world:
Notice that I am suggesting syntax highlighting to emphasize the exposed values. I think this idea is much worse without that affordance.
-
No
module
declaration — Right now if you leave of themodule
declaration, it means that the module name isMain
and it exposes everything. If you leave off the module documentation, the module name would be the file name and it would expose everything. -
port module
— If there is nomodule
declaration, how can you have aport module
declaration? We could just start the file withport module
anyway. That would be the first line, then the documentation comment. -
effect module
— If there is nomodule
declaration, how can you have aeffect module
declaration? We could just start the file witheffect module { command = MyCmd }
anyway.
- When importing
X.elm
, the module name inmodule X exposing (..)
never gets out of sync with the file name. - If you move a file, you do not need to change the module name as well.
- You do not duplicate all the exposed values in
exposing
and in the module documentation.
- It is weird that comments have real meaning. The
exposing
info is important, so you cannot just throw this comment out! - The concept of a
module
is not really introduced. People will just see files. People already miss the importance of using modules to hidetype
information, and this could make that even worse.
We also ended up talking about exposing type
constructors.
Right now you can say module Maybe exposing (Maybe(Just))
, only exposing one constructor. So users have a constructor, but they can never pattern match on it. I cannot see where this is desirable.
So as we discussed this scenario, we decided that we currently have two reasonable scenarios: you have everything exposed or nothing exposed. So why not restrict it to those with Maybe(..)
and Maybe
?
Well, there are certain times when writing packages where this feels too limiting. In my parsing library:
- You have a
type Parser
that should be opaque to users. No constructors exposed. It lives in theParser
module. - You have the
Parser.LanguageKit
module that defines a bunch ofParser
values and needs access to the constructors.
The current solution is to create a Parser.Internal
module with a type Parser
that exposes all constructors. From there it can be used by all modules within the package. Then in the Parser
module, you can say
type alias Parser a = Parser.Internal.Parser a
So people from outside can see the type but not the constructors. This does not feel amazing, but it works.
So we were thinking, what if there is actually a third scenario? A type
can be (1) no constructors exposed, (2) all constructors exposed to everyone, or (3) all constructors exposed within this package. Now you can structure your package code however you want and still hide the information from users.
Proposal: We could have three ways to expose a type
in this new world: Maybe
for hidden, Maybe(..)
for exposed to everyone, and Maybe(..local..)
for exposed within the package. I am not satisfied with the syntax for the third case, and would be curious to find something better. In the end, it will only be useful in relatively sophisticated packages (never applications) so it is alright if it is a bit odd.
Note: I think inability to do (3) is the root cause of this proposal. Having reexports is a huge problem for creating reasonable documentation. If I just expose a whole module, do I just put a link to that module? That is what Haskell does and it makes docs like these incredibly difficult to read and understand. So maybe we take the module doc for the rexport and just jam it in there?
My point here is just that I think (3) solves the underlying problem that gives rise to this request in all the cases I can think of. It would be worthwhile to think of cases where this is not true.
To elaborate on this:
In our app, every server-backed resource is represented by a module with
type Id = New | Id String
. The only way to create anId String
for a given module is via the exposed URL param decoder or JSON decoder. This guarantees that IDs can't be created arbitrarily and are only coming from contexts where there is some explicit type information (e.g., in a/things/1
URL or a{"type": "thing", "id": "1"}
JSON payload).