Skip to content

Modules

One module per file; the file path is the module name. Reuse across files goes through module / import.

Module header

An optional header names the module and lists what it exposes. It sits at column zero, above the declarations.

module Fleet exposing (internalNetwork, endpoint)
internalNetwork : String
internalNetwork = "10.0.0.0/8"
endpoint : String -> Int -> String
endpoint address p = "${address}:${String.fromInt p}"

exposing (..) exposes everything. An explicit list names values and types; a type may expose its constructors with Type(..):

module Roles exposing (Role(..), glyphsFor)

A file with no module line is a valid entry module that exposes everything.

An exposing list names only what the module itself declares. Exposing a name that arrived through an import is a compile error, so a module is never a relay for another module’s surface — importers reach a name from the module that declares it. An uppercase item is always a type: to expose a constructor, expose its type with Type(..).

Imports

An import line, also at column zero, brings a module into scope.

import Fleet
import Fleet as F
import Fleet exposing (internalNetwork)
  • import Fleet — qualified access only (Fleet.endpoint).
  • import Fleet as F — qualified under the alias (F.endpoint).
  • import Fleet exposing (internalNetwork) — the named values usable unqualified.

Only exposed names are importable. A type exposed with Type(..) brings its constructors into the importer — usable both to build values and to pattern-match, with the exhaustiveness checker seeing the full constructor set. A type exposed without (..) keeps its constructors private, so it cannot be matched in the importer.

Names that two modules share

A type and a constructor are both identified by the module that declares them. IntBox’s Thing is IntBox.Thing, and Shapes’s Circle is Shapes.Circle, so two modules may each declare a Thing or a Circle and be imported together.

Names are written bare, and a bare name resolves whenever exactly one thing in scope answers to it. When two do, write the one you mean in full — in an annotation, in an expression, or in a pattern:

import IntBox exposing (Thing(..)) -- declares Thing, whose variant is Wrap
import StrBox exposing (Thing(..)) -- also declares Thing, also with a Wrap
label : IntBox.Thing -> String
label value =
case value of
IntBox.Wrap n -> String.fromInt n

A bare Thing or a bare Wrap in that module is a compile error. The imports themselves are fine — only a use site can be ambiguous — and the two errors differ in how much they can tell you. A bare constructor is reported at the reference and offers both qualified spellings. A bare type names both owners in its message, but carries no span — it is reported at the top of the file — and its note offers one qualified spelling and the alternative of importing only one of the two. Renaming one of them remains available and is often the clearer answer.

A type reached through two imports is one type, not a collision: two modules may both re-expose a third module’s type. Constructors do not travel that way — a module cannot re-expose a constructor it did not declare, so a constructor’s qualifier is always the module that declared it, and one module reached through two import lines contributes one name.

Constructors are also gated tighter than types. Only a Type(..) export puts a constructor in an importer’s scope, so a type exposed without (..) puts no constructor name in play at all. A type name, by contrast, is in play even when the module keeps the type itself unexposed and only mentions it in an exposed signature, because the importer’s inference still unifies on it.

Qualified access

Alias.name (or Module.name) reaches an exposed member. This reuses the same dotted-name resolution as the built-in modules (List.map, String.fromInt). See the prelude.

The case of what follows the last dot is what picks the namespace: Limesurvey.Database.config is the value config, and Limesurvey.Database.Config is the type or constructor Config. An as alias stands in for the module name in all three.

The main entry point

Exactly one module — the entry — declares main : List Scroll. Every other module is a library. A library that declares main is a compile error.

main : List Scroll
main =
[ scroll { name = "web", glyphs = [ aptPackage { name = "nginx" } ] } ]

The library search path

import Foo resolves Foo.emet over a search path, first match winning.

  1. The entry file’s own directory.
  2. Each source-directories entry of the nearest emet.json.

The nearest emet.json is found by walking up the entry file’s ancestor directories. Without an emet.json, resolution is entry-directory-only.

{ "source-directories": ["lib"] }

With that emet.json at the repo root, any entry resolves import Quadlet to lib/Quadlet.emet. The entry directory keeps precedence over source-directories.

A dot in a module name is a directory separator

A module name may be dotted, and each segment is one directory level. module Limesurvey.Database lives at Limesurvey/Database.emet, found by joining the segments onto each search-path root in the same order as before:

lib/
Limesurvey.emet -- module Limesurvey
Limesurvey/
Database.emet -- module Limesurvey.Database
import Limesurvey.Database exposing (containerName)
import Limesurvey.Database as Db

The segments must touch: Limesurvey.Database is one name, Limesurvey . Database is not. That holds in the header, in import, and in qualified access, where Limesurvey.Database.containerName reads as the member containerName of the module Limesurvey.Database.

Nesting is naming and nothing else. Limesurvey.Database is an ordinary module that happens to have a dot in its name: it sees nothing of Limesurvey that it did not import, it gets no privileged access to it, and Limesurvey.emet need not exist at all.

See also