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 naming both candidates and offering both spellings. It is reported where the name is written, not at the import — the imports are fine, and only a use site can be ambiguous. Renaming one of the two 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.

See also