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:

expect=ok
module Widget exposing (label)

type Label = Label

label : Label
label = Label

A 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.

expect=ok
module Ui.Widget exposing (label)

type Label = Label

label : Label
label = Label

A lowercase segment is not a module name:

expect=parse-error:UnexpectedToken
module ui.Widget exposing (label)

label = 1

The 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:

expect=ok
module Widget exposing ()

label = 1

Leaving the clause out entirely is a syntax error:

expect=parse-error:UnexpectedToken
module Widget

label = 1

The header is one layout block, so the exposing clause may be carried onto later lines as long as they are indented past column 1:

expect=ok
module Widget exposing
  ( label
  , size
  )

type Label = Label

label : Label
label = Label

size : Label
size = Label

Continuing it in column 1 does not work, because a line starting in column 1 begins a new top-level declaration:

expect=parse-error:UnexpectedToken
module Widget
exposing (label)

label = 1

The 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:

EntryExposes
labela value or function declared in this module
Sizea 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.

expect=ok
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 =
  a

The 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:

expect=parse-error:UnexpectedToken
module Widget exposing (Color(Red))

type Color
  = Red
  | Blue

An 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:

expect=canonical-error:ExportNotFound
module Widget exposing ((+))

label = 1

An 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:

expect=parse-error:UnexpectedToken
module Widget exposing (Ui.label)

label = 1

A trailing comma is allowed, and means nothing:

expect=ok
module Widget exposing (label,)

type Label = Label

label : Label
label = Label

This 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:

expect=parse-error:UnexpectedToken
module Widget exposing (.., label)

label = 1

Everything 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.

expect=canonical-error:ExportNotFound
module Widget exposing (missing)

label = 1
expect=okpackage=reexport
module Widget exposing (Size, label)

type Size
  = Small

label : Size
label = Small
expect=canonical-error:ExportNotFoundpackage=reexport
module 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.

expect=okpackage=privacy
module Widget exposing (Size, label)

type Size
  = Small

label : Size
label = Small

hidden : Size
hidden = Small
expect=canonical-error:VariableNotFoundpackage=privacy
module Main exposing (x)

import Widget

x : Widget.Size
x = Widget.hidden

Privacy 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.

expect=canonical-error:ExportedValueNotAnnotatedpackage=unannotated
module Widget exposing (label)

label = 1

Widget 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.

expect=canonical-error:VariableNotFoundpackage=unannotated
module Main exposing (x)

import Widget

x = Widget.label

Imports

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:

expect=parse-error:UnexpectedToken
module Main exposing (x)

import Widget exposing (label) as W

x = 1

An 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:

expect=parse-error:UnexpectedToken
module Main exposing (x)

import Widget as W.Inner

x = 1

Every import brings the imported module's exposed names into scope qualified — under the module's name, or under its alias when it has one:

expect=okpackage=qualified
module Widget exposing (Size, label)

type Size
  = Small

label : Size
label = Small
expect=okpackage=qualified
module Main exposing (x)

import Widget

x : Widget.Size
x = Widget.label

An 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:

expect=okpackage=alias
module Widget exposing (Size, label)

type Size
  = Small

label : Size
label = Small
expect=okpackage=alias
module Main exposing (x)

import Widget as W

x : W.Size
x = W.label
expect=canonical-error:VariableNotFoundpackage=alias
module Other exposing (y)

import Widget as W

y = Widget.label

Where 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).

expect=okpackage=position
module Widget exposing (Size)

type Size
  = Small
expect=okpackage=position
module Main exposing ()

x = 1

import Widget

What 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.

expect=okpackage=unqualified
module Widget exposing (Size, label)

type Size
  = Small

label : Size
label = Small
expect=okpackage=unqualified
module Main exposing (x)

import Widget exposing (Size, label)

x : Size
x = label

Without the entry, the qualified spelling is the only one:

expect=okpackage=unqualified2
module Widget exposing (Size, label)

type Size
  = Small

label : Size
label = Small
expect=canonical-error:VariableNotFoundpackage=unqualified2
module Main exposing (x)

import Widget

x : Widget.Size
x = label

Size 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.

expect=okpackage=variants
module Widget exposing (Size(..))

type Size
  = Small
expect=okpackage=variants
module Main exposing (x)

import Widget exposing (Size(..))

x : Size
x = Small
expect=canonical-error:VariantNotFoundpackage=variants
module Other exposing (y)

import Widget exposing (Size)

y : Size
y = Small

Constructors 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:

expect=okpackage=variants
module Shape exposing (grow)

import Widget exposing (Size)

grow : Size -> Size
grow s =
  s

An 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:

expect=canonical-error:TypeNotFoundpackage=variants
module Narrow exposing (grow)

import Widget

grow : Size -> Size
grow s =
  s

An 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:

expect=okpackage=missing-entry
module Widget exposing (Size, label)

type Size
  = Small

label : Size
label = Small
expect=canonical-error:EnvironmentErrorspackage=missing-entry
module Main exposing (x)

import Widget exposing (missing)

x = 1

A bare Size entry is checked the same way, and so is Size(..): whether the constructors come along is the only difference between the two.

expect=okpackage=missing-type
module Widget exposing (Size, label)

type Size
  = Small

label : Size
label = Small
expect=canonical-error:EnvironmentErrorspackage=missing-type
module Main exposing ()

import Widget exposing (Missing)

x = 1

The constructors are the only difference between the two entries, so a Size(..) entry is an error against a module that exposes Size without them:

expect=okpackage=opaque-entry
module Widget exposing (Size, small)

type Size
  = Small

small : Size
small = Small
expect=canonical-error:EnvironmentErrorspackage=opaque-entry
module Main exposing (x)

import Widget exposing (Size(..))

x : Size
x = Widget.small

Operators

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.

expect=okpackage=operators
module Widget exposing (Size, one, (+), add)

type Size
  = Small

one : Size
one = Small

infix left 6 (+) = add

add : Size -> Size -> Size
add a b =
  a
expect=okpackage=operators
module Main exposing (x)

import Widget exposing (Size, one, (+))

x : Size
x = one + one
expect=okpackage=operators
module Other exposing (y)

import Widget exposing (..)

y : Size
y = one + one

Two 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.

expect=okpackage=ambiguous
module Widget exposing (Size, label)

type Size
  = Small

label : Size
label = Small
expect=okpackage=ambiguous
module Gadget exposing (label)

type Label = Label

label : Label
label = Label
expect=canonical-error:AmbiguousVariablespackage=ambiguous
module Main exposing (x)

import Widget exposing (label)
import Gadget exposing (label)

x = label

The 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.

expect=okpackage=duplicates
module Widget exposing (Size, label)

type Size
  = Small

label : Size
label = Small
expect=okpackage=duplicates
module Gadget exposing (Size, volume)

type Size
  = Loud

volume : Size
volume = Loud
expect=canonical-error:AmbiguousVariablespackage=duplicates
module Main exposing (x)

import Widget
import Widget

x = Widget.label
expect=okpackage=duplicates
module Other exposing ()

import Widget as Gadget
import Gadget

y = Gadget.label

z = Gadget.volume
expect=canonical-error:EnvironmentErrors
module Alone exposing (x)

import Alone

x = 1

Imports 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.

expect=dependency-errorpackage=cycle
module Left exposing (x)

import Right

x = 1
expect=dependency-errorpackage=cycle
module Right exposing (y)

import Left

y = 2

The 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.

expect=okpackage=core-shaped
module Basics exposing (Int, (+), add)

type Int = Int

infix left 6 (+) = add

add : Int -> Int -> Int
add a b =
  a
expect=canonical-error:VariableNotFoundpackage=core-shaped
module Main exposing (x)

x : Int
x = 1 + 2

Main 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.

expect=okpackage=below
module Basics exposing (Int)

import Below

type Int = Int
expect=okpackage=below
module foreign Below exposing (twice)

unsafe twice : Int -> Int

Known 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.