#Modules & Projects
Every example so far has been one file. This chapter is about splitting a program across several: how a file becomes a module, how one module uses another, and what makes a directory a project.
#A file is a module
There is no module keyword. Every .mdk file is a module, named by its path
relative to the project root: shapes.mdk is the module shapes, and
net/http.mdk is net.http.
A declaration is private to its file unless you mark it. Two keywords cross the boundary:
exportin front of a declaration makes it visible to other modules.importbrings another module's exports into the current file.
#Importing
import has several shapes, which differ in which names they bind and whether the
names are qualified.
-- file: greet.mdk
export hello = "hello"
export bye = "bye"
-- file: main_single.mdk
import greet.hello -- one name
main = println hello
-- file: main_group.mdk
import greet.{hello, bye} -- several names
main = println (hello ++ " / " ++ bye)
-- file: main_wildcard.mdk
import greet.* -- every export
main = println helloDay to day, use import greet.hello and import greet.{hello, bye}. A selective
import documents what the file uses. import greet.* is convenient for a small
module and worth dropping once the module grows: a later addition to greet.mdk
can collide with a name from another import, which is an error at the use site, or
be silently hidden by a local definition with the same name.
A selective member list may not name a data constructor directly —
import colors.{Red} is rejected, since Red is a constructor of Color,
not a type of its own. Write import colors.{Color(..)} to bring in all of
Color's constructors, or alias the module and write C.Red.
#Aliases
Two modules that export the same name collide if both are imported unqualified. An alias resolves it:
-- file: red.mdk
export paint = "red"
-- file: blue.mdk
export paint = "blue"
-- file: main.mdk
import red as R -- module alias: refer to R.paint
import blue.{paint as bluePaint} -- member alias: rename one import
main = println (R.paint ++ "/" ++ bluePaint)red/blueAn alias replaces the unqualified import: import red as R does not also bind a bare
paint. A module alias has to be capitalized, since it is used as a qualifier. A
member alias renames one imported value, and only a value: a type or a constructor
is imported under its own name or not at all.
An alias qualifies everything the module exports, not only its values. A type works in a signature, a constructor in an expression or a pattern, and an interface in a constraint, all spelled with the same prefix:
-- file: colors.mdk
public export data Color = Red | Green
export interface Named a where
nameOf : a -> String
export impl Named Color where
nameOf c = match c
Red => "red"
Green => "green"
-- file: main.mdk
import colors as C
describe : C.Named a => a -> String
describe x = "the " ++ C.nameOf x
first : C.Color
first = C.Red
main = println (describe first)the redA constructor is reachable this way only when its module exports it, with
public export data (see Exporting below). An alias cannot be combined with a group
or wildcard import, since those already bind their names unqualified.
#A bare import binds nothing, but it is not a no-op
import greetThis form binds no names. hello is not in scope afterwards, and greet.hello does
not work either, because only aliases support the Module.name qualifier:
./main.mdk:3:15: Unbound variable: greet. 'greet' is an imported module, not a value — a
bare 'import greet' binds no names. Bind what you need: 'import greet.{name, ...}', or
'import greet as M' then 'M.name'What any import does, including this one, is bring the module's impls into scope
for the rest of the file. If greet.mdk defines impl Display Message, importing
greet in any form is what makes display work on a Message here. That matters
when a type and its implementations live in different files:
-- file: widget.mdk
public export data Widget = Widget Int
-- file: display_widget.mdk
import widget.{Widget}
impl Display Widget where
display (Widget n) = "widget#\{n}"
-- file: main.mdk
import widget.{Widget(..)}
import display_widget
main = println (display (Widget 5))widget#5Delete the import display_widget line and Widget 5 still compiles, since
Widget comes from widget. The display call no longer does, because the only
impl Display Widget in the program is in a file this one no longer imports:
error: ./main.mdk:3:16: No impl of Display for Widget; add 'deriving Display' to the 'Widget' type, or write an 'impl Display Widget'.
|
3 | main = println (display (Widget 5))
| ^#Exporting
export in front of a binding, an interface, an impl, or a type alias makes it
visible under its own name.
data gets one extra distinction, because a type and its constructors are separate
things to expose. public export data exports both. Plain export data exports the
type only and keeps the constructors private, which is called an abstract export.
-- file: account.mdk
public export data Point = Point Int Int -- type and constructors
export data Account = Account Int -- type only; the constructor stays private
export
mkAccount : Int -> Account
mkAccount n = Account n
export
balanceOf : Account -> Int
balanceOf (Account n) = n
-- file: main.mdk
import account.{Point(..), Account, mkAccount, balanceOf}
main = println (balanceOf (mkAccount 100))100Point(..) imports the type with its constructors, and works because Point was
exported with public. Trying the same on Account is refused at the import:
./main.mdk:1:16: 'Account' exports no constructors from module 'account' (exported abstractly). Remove `(..)`, or export them: declare 'Account' a `public export data` where it is defined, and name it `Account(..)` in any `export import` that re-exports it (`public` is a parse error on `import`)An abstract export is how a module keeps control of a type's representation.
account.mdk can change what an Account is made of later, and nothing that only
ever called mkAccount and balanceOf has to change.
To re-export something you imported, write export import:
export import list.{reverse, take}Importers of this module then see reverse and take as if it had defined them.
A re-export carries whatever the import spelling beside it carries, constructors
included: export import account.{Point(..)} re-exports Point with its
constructors, and export import account.{Point} re-exports it abstractly. There is
no separate spelling for the first — public applies to data declarations, never
to an import.
#medaka.toml and project layout
A directory becomes a project by containing a medaka.toml. Import paths resolve
relative to that directory, and medaka finds it by walking up from the file you
give it. medaka new creates one:
$ medaka new expenses
Created expenses/expenses/
├── .gitignore
├── README.md
├── main.mdk
└── medaka.toml[package]
name = "expenses"
version = "0.1.0"
entry = "main.mdk"The generated main.mdk runs as is:
main : <IO> Unit
main = println "Hello, Medaka"Hello, MedakaThere is no required directory layout. A small project keeps its .mdk files next
to medaka.toml. A larger one can nest them, since a module's name comes from its
path. medaka.toml can also list [dependencies] on sibling projects and
[foreign-libraries] to link against, neither of which this guide covers.
The last chapter, Tooling & Workflow, is about the commands you run while writing Medaka: checking, formatting, linting, and testing.