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

  • export in front of a declaration makes it visible to other modules.
  • import brings 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 hello
Not runnable in the playground: multi-file project — the playground runs a single source buffer

Day 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)
Not runnable in the playground: multi-file project — the playground runs a single source buffer
red/blue

An 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)
Not runnable in the playground: multi-file project — the playground runs a single source buffer
the red

A 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 greet
Not runnable in the playground: a fragment, not a standalone program

This 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))
Not runnable in the playground: multi-file project — the playground runs a single source buffer
widget#5

Delete 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))
Not runnable in the playground: multi-file project — the playground runs a single source buffer
100

Point(..) 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}
Not runnable in the playground: defines no top-level `main`, so there is no program to run

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, Medaka

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