Modules, exposing and imports
Every Zelkova source file is exactly one module, and every module is exactly one source
file. A module names itself, says which of its declarations the rest of the world may
see, and names the other modules it draws on. Those three things are the module header
and the import declarations under it.
There is no other unit of encapsulation. Zelkova has no nested modules, no namespaces within a file, and no way to reopen a module from somewhere else — so "where does this name come from" and "who is allowed to use this" are both answered at the top of one file.
The module header
The first token of a file is the keyword module, in column 1 — Layout
carries that rule and the reasoning for it. What follows is the module's name and its
exposing list:
module Widget exposing (label)
type Label = Label
label : Label
label = LabelA module name is one or more segments separated by ., each an uppercase-initial
identifier. The dots are punctuation inside one name rather than a path through
anything: Ui.Widget is a single identifier, and there is no module Ui implied by it —
importing Ui.Widget gives Ui no meaning of its own. Where the file lives does mirror
those dots, which is the next section, but that is a rule about files and not about how
the name resolves.
module Ui.Widget exposing (label)
type Label = Label
label : Label
label = LabelA lowercase segment is not a module name:
module ui.Widget exposing (label)
label = 1The exposing clause is not optional. A module that means to expose nothing writes an
empty list, so that a reader can tell an empty interface from a forgotten one:
module Widget exposing ()
label = 1Leaving the clause out entirely is a syntax error:
module Widget
label = 1The header is one layout block, so the exposing clause may be carried onto later lines
as long as they are indented past column 1:
module Widget exposing
( label
, size
)
type Label = Label
label : Label
label = Label
size : Label
size = LabelContinuing it in column 1 does not work, because a line starting in column 1 begins a new top-level declaration:
module Widget
exposing (label)
label = 1The name and the file
Not implemented: a module's declared name must match the path of the file that holds
it, relative to the package's source root, with each . in the name standing for a
directory separator and .zel appended. module Ui.Widget lives in Ui/Widget.zel and
nowhere else. Two files may not declare the same module name.
src/
Widget.zel module Widget exposing (…)
Ui/Widget.zel module Ui.Widget exposing (…)
That rule is what makes an import answerable without searching: a reader who sees
import Ui.Widget knows which file to open, and a compiler that has to resolve it can
find the file without parsing every module in the package first. Today neither half is
checked — a file at Sub/Thing.zel may declare module Elsewhere and be imported as
Elsewhere, and two files may both declare module Same with the second silently
replacing the first. docs/tickets/lang-6.md is the ticket. The
rule cannot be shown as a tagged block here at all: a block is source text with no path
behind it, so there is nothing for an example to violate.
The exposing list
An exposing list is either (..), which exposes everything the module declares, or a
parenthesised list of entries. There are exactly four kinds of entry:
| Entry | Exposes |
|---|---|
label | a value or function declared in this module |
Size | a type, but not its constructors — an opaque type |
Size(..) | a type together with every one of its constructors |
(+) | an operator, which must have an infix declaration in this module |
A class is exposed by its bare name as well, written as an opaque type's is, and that entry exposes its members with it.
module Widget exposing (Color, Shape(..), label, (+))
type Color
= Red
| Blue
type Shape
= Round
type Label = Label
label : Label
label = Label
infix left 6 (+) = add
add a b =
aThe Size / Size(..) split is the whole of Zelkova's data hiding. Exposing a type
without its constructors gives other modules a name they can write in a type annotation
and values they can pass around, while leaving them unable to build one or take one
apart — so the module's own functions are the only way in, and the representation stays
changeable. Exposing Size(..) gives that up deliberately and permanently.
There is no way to expose some of a type's constructors. A type is opaque or it is not:
module Widget exposing (Color(Red))
type Color
= Red
| BlueAn operator entry names the operator, in parentheses, and the module must carry the
infix declaration that gives it a meaning. Operators are not built in — exposing (+)
from a module that never declared it is an error:
module Widget exposing ((+))
label = 1An entry is always a bare name. A qualified name in an exposing list is a syntax
error, since a module can only expose what it declares itself:
module Widget exposing (Ui.label)
label = 1A trailing comma is allowed, and means nothing:
module Widget exposing (label,)
type Label = Label
label : Label
label = LabelThis is a deliberate divergence — most languages of this family reject it. A
one-name-per-line exposing list is the normal way to write a long one, and with no
trailing comma every addition to the end of such a list touches two lines instead of one.
(..) is all or nothing. It cannot be combined with named entries:
module Widget exposing (.., label)
label = 1Everything exposed must be declared here
Every entry in a module's own exposing list names one of that module's declarations.
A module may not re-export something it imported: a name that reaches other modules
through Widget is a name Widget declared.
module Widget exposing (missing)
label = 1module Widget exposing (Size, label)
type Size
= Small
label : Size
label = Smallmodule Facade exposing (label)
import Widget exposing (label)Exposing is what other modules can see
A module's exposing list is the complete list of what any other module may reach,
qualified or not. A declaration left out of it is private to the module.
module Widget exposing (Size, label)
type Size
= Small
label : Size
label = Small
hidden : Size
hidden = Smallmodule Main exposing (x)
import Widget
x : Widget.Size
x = Widget.hiddenPrivacy is a property of the boundary: hidden is an ordinary value inside Widget, and
label may call it.
The reverse matters too: a value the module does expose still has to actually cross the boundary, which is only possible once the checker knows its type. An exposed declaration with no type annotation is rejected at the declaration itself — an exposed declaration must be annotated.
module Widget exposing (label)
label = 1Widget still publishes an interface for Main to resolve against, and label is not in
it: the import resolves, and the one name inside it is what fails.
module Main exposing (x)
import Widget
x = Widget.labelImports
An import declaration names a module and, optionally, an alias for it and a list of
names to bring into scope unqualified:
import <ModuleName> [as <Alias>] [exposing (<list>)]
The order is fixed: the module name, then as, then exposing. Writing them the other
way round is a syntax error, so there is one shape for an import line and a reader
scanning a column of them is never re-reading one:
module Main exposing (x)
import Widget exposing (label) as W
x = 1An alias is a single uppercase-initial identifier. It has no dots in it — an alias introduces one new prefix, and a dotted one would introduce a prefix that looks like a module name of its own:
module Main exposing (x)
import Widget as W.Inner
x = 1Every import brings the imported module's exposed names into scope qualified — under the module's name, or under its alias when it has one:
module Widget exposing (Size, label)
type Size
= Small
label : Size
label = Smallmodule Main exposing (x)
import Widget
x : Widget.Size
x = Widget.labelAn alias replaces the module's own name rather than adding to it. After
import Widget as W, W.label resolves and Widget.label does not — there is one
spelling for one module in one file:
module Widget exposing (Size, label)
type Size
= Small
label : Size
label = Smallmodule Main exposing (x)
import Widget as W
x : W.Size
x = W.labelmodule Other exposing (y)
import Widget as W
y = Widget.labelWhere imports go
Not implemented: every import in a file sits between the module header and the
first other declaration. An import after a value, type or infix declaration is a syntax
error. The list of what a module depends on is a property of the module rather than of
the point it is written at.
Known gap: the grammar treats import as an ordinary top-level declaration and
accepts one anywhere among the others, which is what the second block below shows
(docs/tickets/lang-5.md).
module Widget exposing (Size)
type Size
= Smallmodule Main exposing ()
x = 1
import WidgetWhat an import's exposing list does
An import's exposing list takes the same four entry forms as a module header, and does
one extra thing: it makes those names available unqualified, in addition to the
qualified spelling every import already provides.
module Widget exposing (Size, label)
type Size
= Small
label : Size
label = Smallmodule Main exposing (x)
import Widget exposing (Size, label)
x : Size
x = labelWithout the entry, the qualified spelling is the only one:
module Widget exposing (Size, label)
type Size
= Small
label : Size
label = Smallmodule Main exposing (x)
import Widget
x : Widget.Size
x = labelSize and Size(..) differ on the import side exactly as they do on the export side:
the first brings the type's name into scope, the second brings its constructors too.
module Widget exposing (Size(..))
type Size
= Smallmodule Main exposing (x)
import Widget exposing (Size(..))
x : Size
x = Smallmodule Other exposing (y)
import Widget exposing (Size)
y : Size
y = SmallConstructors are the only thing the bare entry gives up. The type's name is in scope
unqualified, so a module can name it in its own signatures while leaving every way of
building one to Widget:
module Shape exposing (grow)
import Widget exposing (Size)
grow : Size -> Size
grow s =
sAn exposing (..) on an import brings in everything the module exposes, unqualified. A
bare import Widget with no exposing clause brings in nothing unqualified, and means
the same as import Widget exposing (). Widget.Size is then the only spelling of the
type, and the bare Size names nothing:
module Narrow exposing (grow)
import Widget
grow : Size -> Size
grow s =
sAn entry naming something the imported module does not expose is an error, and the
diagnostic points at the entry rather than at the whole import line:
module Widget exposing (Size, label)
type Size
= Small
label : Size
label = Smallmodule Main exposing (x)
import Widget exposing (missing)
x = 1A bare Size entry is checked the same way, and so is Size(..): whether the
constructors come along is the only difference between the two.
module Widget exposing (Size, label)
type Size
= Small
label : Size
label = Smallmodule Main exposing ()
import Widget exposing (Missing)
x = 1The constructors are the only difference between the two entries, so a Size(..) entry is an
error against a module that exposes Size without them:
module Widget exposing (Size, small)
type Size
= Small
small : Size
small = Smallmodule Main exposing (x)
import Widget exposing (Size(..))
x : Size
x = Widget.smallOperators
An operator has no qualified spelling: Widget.(+) is a syntax error, and Widget.+ is
not a qualified operator either — it is the constructor Widget and an operator named
.+. So an operator entry in an import's exposing list is the only way to use one from
another module. Naming the operator is enough on its own: the function that its infix
declaration points at need not be in scope.
An exposing (..) import brings in every operator the module exposes, the same way it
brings in every value.
module Widget exposing (Size, one, (+), add)
type Size
= Small
one : Size
one = Small
infix left 6 (+) = add
add : Size -> Size -> Size
add a b =
amodule Main exposing (x)
import Widget exposing (Size, one, (+))
x : Size
x = one + onemodule Other exposing (y)
import Widget exposing (..)
y : Size
y = one + oneTwo imports exposing the same name
A name brought in unqualified by two different imports is not an error at the import
line — it becomes one at each place it is used, and only there. Importing two modules
that both expose label is fine right up until something writes label.
module Widget exposing (Size, label)
type Size
= Small
label : Size
label = Smallmodule Gadget exposing (label)
type Label = Label
label : Label
label = Labelmodule Main exposing (x)
import Widget exposing (label)
import Gadget exposing (label)
x = labelThe qualified spelling is unaffected: Widget.label and Gadget.label both still
resolve, and are the fix.
Name resolution and scoping is where
the general rule lives, including what shadows what.
One module, one import
A module may be imported at most once in a file, an alias may name at most one module, and an alias may not collide with the name of another imported module. Each of the three would otherwise let one prefix mean two things at once.
Not implemented: none of the three is checked at the import line
(docs/tickets/lang-7.md), and the two that quietly succeed are
the worse pair. An alias colliding with another imported module's name merges the two
namespaces, so Gadget. in the fourth block below reaches into both Widget and
Gadget and nothing says so — that block is tagged expect=ok for exactly that reason.
Known gap: importing the same module twice does fail today, but not as a duplicate
import. Every one of that module's names is registered twice over, so each use of one
is reported as ambiguous between two modules that happen to be the same module — the
third block below is tagged for an error whose message names Widget twice, at the use
site rather than at the import line.
Known gap: a module importing itself is rejected too, and also for the wrong reason:
a module's own interface does not exist while it is being checked, so import Alone
inside module Alone is reported as a module that cannot be found. The last block is
tagged for that. Both are lang-7.
module Widget exposing (Size, label)
type Size
= Small
label : Size
label = Smallmodule Gadget exposing (Size, volume)
type Size
= Loud
volume : Size
volume = Loudmodule Main exposing (x)
import Widget
import Widget
x = Widget.labelmodule Other exposing ()
import Widget as Gadget
import Gadget
y = Gadget.label
z = Gadget.volumemodule Alone exposing (x)
import Alone
x = 1Imports may not form a cycle
The import declarations of a package form a graph, and that graph must be acyclic. Two
modules that import each other, or any longer loop, is an error reported before any of
the modules involved is checked — there is no order in which they could be, since each
needs the other's exposed types to make sense of its own.
The error names every module on the cycle and points at the import line that forms each
edge, so breaking it is a matter of picking one of those lines.
module Left exposing (x)
import Right
x = 1module Right exposing (y)
import Left
y = 2The default imports
Every module behaves as though it began with these eight imports, whether they are written or not:
import Basics exposing (..)
import List exposing (List)
import Maybe exposing (Maybe(..))
import Result exposing (Result(..))
import Task exposing (Task)
import Char exposing (Char)
import String exposing (String)
import Tuple
So Int, Bool, String, True, + and <| are in scope in every module with nothing
written at the top of it, Maybe and Just likewise, and List.map, Char.toUpper and
String.length are reachable under their qualified names. Nothing else is: a module that
wants Dict imports it.
The list is chosen so that the types appearing in ordinary type annotations are always
writable. Maybe and Result are exposed with their constructors because matching on
them is the ordinary way to use them, and a qualified Maybe.Just in every case branch
would spell out a module name on one of the most common patterns in the language.
List, Char and String are exposed as bare types because their modules' functions read
better qualified — List.map and String.length, not map and length. Task is exposed the same way and for
the same reason, and it is on the list because main names it in an
annotation every program has to write.
Failure does not come with it: the modules
naming that type are the ones declaring or consuming an
effectful facade, and a module that names it imports it.
An import of one of the eight replaces the implicit one, the way an alias replaces
a module's own name: writing a line of the list out verbatim changes nothing,
and import Maybe as M means M.map and no unqualified Maybe.
A default entry participates in ambiguity exactly as a written import does.
zelkova-core is the exception. No module of the package the eight belong
to receives any of them — not the
eight themselves, and not the modules beside them. Basics cannot import Basics, Maybe and
Result would import each other, and a facade Basics is built from cannot import Basics
back: each is an import cycle. Core's modules write every
import they use, the way Basics.zel, Maybe.zel and Bitwise.zel already do. That exception
belongs to zelkova-core alone, so a package of any other
name declaring a module under one
of the eight is rejected.
module Basics exposing (Int, (+), add)
type Int = Int
infix left 6 (+) = add
add : Int -> Int -> Int
add a b =
amodule Main exposing (x)
x : Int
x = 1 + 2Main is a module of the same package as Basics here, so it gets none of the eight and has
to write import Basics exposing (..) for + to resolve, the way Bitwise.zel writes
import Basics exposing (Int) for Int alone.
Every module of every other package receives all eight, whatever it imports and whatever imports it. Core is a dependency of every package and dependencies run one way, so nothing you write can end up underneath one of the eight, and no package but core may declare a module under one of their names. Why the exception is scoped to a package rather than judged from the import graph is DEC-17.
A module of zelkova-core receives the scalar type names without an import. Int,
Float, Char, String and Bool are in its scope with nothing written at the top of the
file, bound to the same declarations Basics exposes. That is not one of the eight entries
coming back: the compiler knows each of the five by qualified
name, so it supplies them without reading Basics.
What arrives is the five type names and nothing else. A module reaching Bool this way writes
it in a signature and cannot write a True: the constructors are Basics' values, and no rule
brings a value down. The case it exists for is a facade underneath Basics,
which has no bodies to write one in.
module Basics exposing (Int)
import Below
type Int = Intmodule foreign Below exposing (twice)
unsafe twice : Int -> IntKnown gap: std/core ships six of the eight, so List and Char bring nothing: a
program naming Char.toUpper or annotating a List is rejected where the name is written. Each entry starts working on the day its module
compiles.
Packages
A module belongs to exactly one package, and its name is unique within it. An import
names a module of the package being compiled or a public module of one of that package's
direct dependencies — the second under that dependency's namespace, so the name written is
longer than the name the module has at home. What a package is, which of its modules it keeps
private, how its dependencies are declared, and how a namespace is derived and when a
depending package may drop one are the subject of
Packages and source layout.