Module System

March has an Elixir-inspired module system. Modules are the primary unit of code organization, and all definitions live inside a module.


Declaring a Module

Every March file begins with a mod declaration:

mod MyApp do
  -- definitions here
end

Modules can be dotted for hierarchical organization:

mod MyApp.Router do
  -- router logic
end

mod MyApp.Templates.Layout do
  -- layout templates
end

Modules can also be nested inline:

mod Outer do
  mod Inner do
    fn greet() do println("from Inner") end
  end

  fn main() do
    Inner.greet()    -- qualified access
  end
end

Visibility

By default, all definitions are public (accessible from outside the module). To make something private, use pfn for functions or ptype for types:

mod Passwords do
  -- Public API:
  fn verify(plain : String, stored : String) : Bool do
    hash(plain) == stored
  end

  -- Private implementation detail:
  pfn hash(s : String) : String do
    Crypto.sha256(s)
  end
end

pfn functions cannot be called from outside their module. ptype makes both the type name and its constructors private.

For types that should expose the name but hide the constructors, use opaque:

mod Token do
  opaque type Token = Token(String)

  fn make(raw : String) : Token do Token(raw) end
  fn value(t : Token) : String do
    match t do Token(s) -> s end
  end
end

-- Outside Token: can use Token as a type, but cannot construct Token(_) directly
fn process(t : Token.Token) : () do
  println(Token.value(t))
end

Qualified Access

Call functions or access types from another module using .:

mod Math do
  fn square(n : Int) : Int do n * n end
  fn cube(n : Int) : Int do n * n * n end
end

mod Main do
  fn main() do
    let s = Math.square(4)   -- 16
    let c = Math.cube(3)     -- 27
    println(int_to_string(s + c))
  end
end

Nested module access chains:

MyApp.Router.dispatch(conn, request)

import

import brings names from a module into the current scope. It works like Elixir’s import:

-- Import all public names from MathUtils:
import MathUtils

fn demo() do
  let s = square(5)   -- no module prefix needed
  let c = cube(3)
  s + c
end

Import only specific names:

import MathUtils, only: [square, cube]
import String, only: [length, split, upcase]

Import everything except specific names:

import String, except: [dangerous_fn]

Dotted import with brace selector:

import String.{length, split}
import MyApp.Utils.{format, parse}

import statements can appear anywhere inside a module body. Their scope is the rest of the module from that point.


use

use is the other import mechanism. It brings names into scope but is more explicit about source:

use List.*                    -- import all from List
use List.{map, filter}        -- import specific names
use List.map                  -- import single name
use A.B.C.*                   -- dotted path, all names

The difference between use and import is primarily stylistic — import is Elixir-style with keyword options (only:, except:), while use is ML-style with glob and brace selectors.


alias

alias gives a module a shorter name for the rest of the scope:

alias Very.Long.Module.Name as Short

fn demo() do
  Short.do_something()
end

Elixir-style comma form:

alias Very.Long.Module.Name, as: Short

Auto-alias to last segment:

alias MyApp.Data.Repository
-- Now Repository is available as the alias

Aliases are useful when a module name is long or conflicts with another name in scope.


A Full Example

From examples/modules.march:

mod Example do

  mod MathUtils do
    fn square(x : Int) : Int do x * x end
    fn cube(x : Int) : Int do x * x * x end
    fn abs_val(n : Int) : Int do
      if n < 0 do 0 - n else n end
    end
  end

  mod Greet do
    fn prefix() : Int do 1000 end
  end

  -- 1. Qualified access
  fn demo_qualified() : Int do
    let a = MathUtils.square(4)
    let b = MathUtils.cube(3)
    a + b      -- 43
  end

  -- 2. Import all
  import MathUtils

  fn demo_import_all() : Int do
    square(5) + cube(2)   -- 33
  end

  -- 3. Import specific names only
  import MathUtils, only: [abs_val]

  fn demo_import_only() : Int do
    abs_val(0 - 7)   -- 7
  end

  -- 4. Alias
  alias MathUtils, as: M

  fn demo_alias() : Int do
    M.square(6)   -- 36
  end

  fn main() : Int do
    demo_qualified() + demo_import_all() + demo_import_only() + demo_alias()
  end

end

Module Signatures

A sig declaration defines an abstract interface for a module — a named signature separate from the implementation:

sig Collection do
  type Elem
  fn insert : Elem -> List(Elem) -> List(Elem)
  fn member : Elem -> List(Elem) -> Bool
end

Signatures are used for compile-time abstraction and caching — downstream code that depends on a sig only needs to recompile when the signature changes, not when the implementation changes.


Multi-File Projects

In a forge project, each file typically contains one module. Files are discovered automatically via MARCH_LIB_PATH.

my_app/
├── src/
│   ├── my_app.march          -- mod MyApp do ... end
│   ├── my_app/router.march   -- mod MyApp.Router do ... end
│   └── my_app/templates.march-- mod MyApp.Templates do ... end

Build with:

MARCH_LIB_PATH=src ./_build/default/bin/main.exe --compile -o my_app src/my_app.march

forge build handles this automatically.

Module names map to file paths by convention: MyApp.Routermy_app/router.march, MyApp.Templates.Layoutmy_app/templates/layout.march.


Module-Level Constants

let at module level defines a constant accessible throughout the module and (if public) from outside:

mod Config do
  let version   = "1.0.0"
  let max_items = 1000
  let base_url  = "https://api.example.com"
end

-- Access from outside:
println(Config.version)

Next Steps