Skip to content

Instantly share code, notes, and snippets.

@evancz
Last active April 2, 2024 20:16
Show Gist options
  • Save evancz/67679082fbb92eebadc9e06651a2d882 to your computer and use it in GitHub Desktop.
Save evancz/67679082fbb92eebadc9e06651a2d882 to your computer and use it in GitHub Desktop.
Idea for module declaration

Do we need module declarations?

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:

proposal

Notice that I am suggesting syntax highlighting to emphasize the exposed values. I think this idea is much worse without that affordance.

Corner Cases

  • No module declaration — Right now if you leave of the module declaration, it means that the module name is Main 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 no module declaration, how can you have a port module declaration? We could just start the file with port module anyway. That would be the first line, then the documentation comment.

  • effect module — If there is no module declaration, how can you have a effect module declaration? We could just start the file with effect module { command = MyCmd } anyway.

Review

Positives

  • When importing X.elm, the module name in module 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.

Negatives

  • 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 hide type information, and this could make that even worse.



Extra Idea

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?

Does this cover everything?

Well, there are certain times when writing packages where this feels too limiting. In my parsing library:

  1. You have a type Parser that should be opaque to users. No constructors exposed. It lives in the Parser module.
  2. You have the Parser.LanguageKit module that defines a bunch of Parser 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.

A third option?

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.

@xtian
Copy link

xtian commented Sep 23, 2017

Not sure it's a convincing reason to keep this but my team uses it in our current project to restrict creation of a resource id to the resource's module, while permitting other modules to know if a resource is "new" or not. […]

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 an Id 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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment