Skip to content

Instantly share code, notes, and snippets.

@evancz
Last active April 2, 2024 20:16
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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.

@nukisman
Copy link

nukisman commented Aug 14, 2017

Avoiding module declaration is very good idea - it will save a lot of time (manual work) during module refactoring (replacing the file)! Vote for this! 👍

My thoughts about it:

No code in comments & No comments in code

Negatives

  • It is weird that comments have real meaning. The exposing info is important, so you cannot just throw this comment out!

Maybe this syntax would be nicer?
In this case we have simpler doc comment syntax: --| .. instead of {-| ... -}

--| # My module - Module markdown (optional)
--| ...
module

--| ## Decode Docs - Section markdown (optional)
--| ...
@exposing decoders

--| ## Split Docs into Blocks - Section markdown (optional)
--| ...
@exposing toBlocks, Block(..)

--| ## Other - Rest section markdown (optional)
--| ...
@exposing (..)  -- Expose ALL other things

import Json.Decode exposing (..)

--| Some function markdown 
--| ..
someFun : Int -> Int
someFun x = x + 1 

Simpler export syntax

Instead of strange @ + exposing keyword it would be nicer to have just export. It also looks good as symmetrical word of import:

--| ## Decode Docs - Section markdown (optional)
--| ...
export decoders   -- export single thing

--| ## Split Docs into Blocks - Section markdown (optional)
--| ...
export toBlocks, Block(..)   -- export several things (comma separated)

--| ## Other - Rest section markdown (optional)
--| ...
export .. -- Export ALL other things

Simpler import syntax

We also could simplify import expression avoiding exposing keyword:

import My.Module1
import My.Module2: ..
import My.Module3: fun3
import My.Module4: T4, fun4 -- Import several things (comma separated)

Using "import .. as"

import My.Module1 as M1
import My.Module2 as M2: ..
import My.Module3 as M3: fun3
import My.Module4 as M4: T4, fun4 -- Import several things (comma separated)

Now it has even better symmetry with export expression!

Type constructor access level

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.

It is similar to "access-modifers" in Java: public, private, package private.
So we could have syntax like this:

Export type

Export type just by name. No need for T(Constructor1, Constructor2) or T(..) syntax

export  T   -- Export a type if you want to export any not private constructors

Define type

type T 
    = A1         -- Public constructor. Most popular case. By default.
    | public  A2 -- Private constructor
    | private B  -- Private constructor
    | package C  -- Package private constructor

Or shorter:

type T 
    =   A1 -- Public by default.
    | + A2 -- Public explicitly
    | - B  -- Private
    | # C  -- Package private

@Kesanov
Copy link

Kesanov commented Sep 9, 2017

A fourth option?

There is also a fourth option, which removes the need for option 2 and 3. Thus we will effectively end up with two options again and it can therefore use the Maybe(..) syntax.

  • Maybe(..) exposes Maybe type and its constructors to everyone
  • Maybe exposes Maybe type to everyone and its constructors only to submodules* of current module

*For illustration Lib.Parser.A Lib.Parser.B are sub modules of Lib.Parser while Lib.Util is not!

@itsgreggreg
Copy link

itsgreggreg commented Sep 18, 2017

IMHO the current module system is fine.
Thoughts to expand a little:

  • Having module Name at the top of the file is useful for:
    • People who don't have the filename shown prominent in their editor
    • Quickly copy-pasting the module name to an import
    • The error message you get when your module file isn't at the correct path. With implicit names, figuring out what was wrong would be harder.
  • exports being at the top of the file is a little 1990's but it works just fine and the compiler guides you through it. The compiler could even notice when you're trying to use a non exported function and let you know.
  • If elm ever wants to have scripts we'll have to have a script marker at the top of files to differentiate them from modules.

All in all I don't think the module definition is lacking in any way and the proposed changes feel purely cosmetic and personal opinion. What we have works good enough, is easy to explain, and isn't really a pain point in my elm programming.

@itsgreggreg
Copy link

itsgreggreg commented Sep 20, 2017

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.

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. Consider the following:

thing.elm:

module Thing exposing { Thing, Id(New) }

type Id
  = Id String
  | New

type alias Thing =
  { id : Id
  , name : String
  }

form for thing:

module Form exposing (..)

import Thing exposing (Thing)
import Html exposing (Html)

type alias Model = Thing

init : maybe Thing -> model
init maybeThing =
    case maybeThing of
      Nothing -> {id = Thing.New, name = ""}
      Just thing -> thing

view : Model  -> Html msg
view model = 
  let 
      header = case model.thing.id of
                 Thing.New -> "Create a Thing"
                  _ -> "Edit a thing"
  in
     Html.text header
     ...

@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