Modules & Packages: Defining and Importing
The same small task in every language: split code into a reusable module - a circle module that exports a public area while keeping a square helper private - then import it from a main program and call across the boundary. Watch what a module is (a single file in Guji, Python, and OCaml; a directory of files in Go; a named package in Perl), how a name is exported (pub vs. an uppercase initial vs. an export list vs. nothing at all), and how privacy is enforced (a compile-time error in Guji, Go, and Rust versus mere convention in Python and Perl).
“The whole intent of Perl 5's module system was to encourage the growth of Perl culture rather than the Perl core.” - Larry Wall
# One file is one module (§16.1). `pub` exports a sub across the
# module boundary; a plain `sub` stays private to the module.
pub sub area($radius: Float): Float {
pi() * square($radius)
}
# Private to this module: an importer cannot call these (compile-time
# error), but area() - in the same module - still can.
sub square($x: Float): Float { $x * $x }
sub pi(): Float { 3.14159 }
sub main() {
# Same module, so call directly. From another module you would
# `import geometry::circle` and call `circle::area(2.0)`.
$a = area(2.0)
print("circle area: $a")
}In guji, one file is one module. pub exports a sub across the module boundary; a plain sub is private to its module, so an importer that called square or pi would get a compile-time error - though area, in the same module, still can. Across files you would import geometry::circle and reach the exported members as circle::area(...); here everything lives in one module, so area(2.0) is called directly. The import plus :: path is how the pieces connect once they are separate files.
// geometry/circle.go
package geometry
// Exported: an uppercase initial makes Area visible to importers.
func Area(radius float64) float64 {
return pi() * square(radius)
}
// Unexported: a lowercase initial keeps these package-private.
func square(x float64) float64 { return x * x }
func pi() float64 { return 3.14159 }
// main.go
package main
import (
"fmt"
"example.com/app/geometry"
)
func main() {
a := geometry.Area(2.0)
fmt.Printf("circle area: %v\n", a)
}In Go a package is a directory of files sharing a package clause, and a program imports it by its module path; the final path element (geometry) qualifies its members. Visibility is governed entirely by case: an identifier with an uppercase initial (Area) is exported, a lowercase one (square, pi) is package-private, and this is enforced by the compiler - there is no pub/private keyword. This is the convention Guji deliberately rejects: in Guji casing is style only, and pub controls export.
(* circle.ml - a .ml file is automatically a module named Circle. *)
let area radius = pi () *. square radius
and square x = x *. x
and pi () = 3.14159
(* circle.mli - the interface lists what the module exports.
Only `area` is named here, so `square` and `pi` stay hidden. *)
val area : float -> float
(* main.ml *)
let () =
let a = Circle.area 2.0 in
Printf.printf "circle area: %f\n" aOCaml derives a module from each file automatically: circle.ml becomes the module Circle, addressed as Circle.area with no import statement at all. An optional companion interface file circle.mli is a signature listing exactly what is exported - omitting square and pi from it makes them invisible and unreachable outside the module, the type-checker's equivalent of Guji's missing pub. OCaml's structures, signatures, and functors (Leroy, 1990s) are the most expressive module system of this group.
-- Circle.hs - one file is one module; the export list is the public face.
module Circle (area) where
-- Only `area` is listed above, so `square` and `pit` stay module-private:
-- importing or calling them from elsewhere is a compile-time error.
area :: Double -> Double
area radius = pit * square radius
square :: Double -> Double
square x = x * x
pit :: Double
pit = 3.14159
-- Main.hs --------------------------------------------------
module Main where
import Circle (area)
main :: IO ()
main = putStrLn ("circle area: " ++ show (area 2.0))A Haskell module is a file beginning with module Name (exports) where; the parenthesised export list is the entire public interface, so naming only area keeps square and pit invisible outside the file. The boundary is checked by the compiler - importing an unexported name is a compile-time error, not mere convention. An empty list or omitted list exports everything, and import Circle (area) brings just the chosen name into scope, qualified or unqualified as you like.
# Geometry/Circle.pm - a package in a file matching its name.
package Geometry::Circle;
use strict;
use warnings;
use Exporter 'import';
# @EXPORT_OK is the module's public face: names a caller may request.
our @EXPORT_OK = qw(area);
use constant PI => 3.14159;
sub area { my ($r) = @_; PI * _square($r) }
# A leading underscore is the convention for "private"; not exported.
sub _square { my ($x) = @_; $x * $x }
1; # a module must end with a true value
# --- main.pl -----------------------------------------------
use strict;
use warnings;
use lib '.'; # find modules under the current dir
use Geometry::Circle qw(area); # import the requested name
my $a = area(2.0);
print "circle area: $a\n";A Perl module is a package living in a file whose path matches its name (Geometry::Circle in Geometry/Circle.pm) and ending in a true value (1;). Exporter plus @EXPORT_OK declares which names a caller may pull into its namespace with use; a leading underscore on _square signals "private" by convention only - Perl does not stop anyone from calling Geometry::Circle::_square directly. Running main.pl prints circle area: 12.5663706143592. Compared with Guji, the boundary is social rather than enforced.
# Geometry/Circle.rakumod - a module file.
unit module Geometry::Circle;
# `is export` marks a routine as part of the public interface.
sub area($radius) is export {
pi-of() * square($radius)
}
# No `is export`: visible only inside the module.
sub square($x) { $x * $x }
sub pi-of() { 3.14159 }
# --- main.raku ---------------------------------------------
use lib '.';
use Geometry::Circle;
my $a = area(2.0);
say "circle area: $a";Raku declares a module with unit module Geometry::Circle; and marks each exported routine with the is export trait; anything without it stays private to the module. A program pulls it in with use Geometry::Circle, which makes the exported area directly callable. The :: namespace separator and the explicit opt-in to export mirror Guji closely - Guji's pub keyword plays the same role as Raku's is export trait.
// circle.rs - a file is a module; `pub` exports an item.
pub fn area(radius: f64) -> f64 {
pi() * square(radius)
}
// No `pub`: private to this module.
fn square(x: f64) -> f64 { x * x }
fn pi() -> f64 { 3.14159 }
// main.rs
mod circle; // declare the circle module (loads circle.rs)
fn main() {
let a = circle::area(2.0);
println!("circle area: {a}");
}Rust's module tree is explicit: mod circle; in main.rs pulls in circle.rs as a module, and items are reached with the :: path separator. Everything is private by default and exported with pub - area is callable as circle::area, while square and pi cannot be reached from outside and the compiler enforces it. This pub-keyword, private-by-default design is exactly the model Guji adopts (§16.2), down to the :: paths.
# geometry/circle.py - a module is a .py file; a package is a directory
# (here geometry/ with an __init__.py).
import math
# Public by convention: a plain name is importable.
def area(radius: float) -> float:
return math.pi * _square(radius)
# A leading underscore marks a name "private" by convention; it is not
# enforced, but `from circle import *` skips it.
def _square(x: float) -> float:
return x * x
# --- main.py -----------------------------------------------
from geometry import circle
def main() -> None:
a = circle.area(2.0)
print(f"circle area: {a}")
if __name__ == "__main__":
main()In Python a module is a .py file and a package is a directory (here geometry/, marked by __init__.py); from geometry import circle then exposes members as circle.area. Privacy is purely conventional: a leading underscore on _square signals "internal" and excludes it from import *, but nothing prevents circle._square(2.0) from working. Running main.py prints circle area: 12.566370614359172. Like Perl, the boundary is a social contract rather than a compile-time guarantee as in Guji.