Modules and Namespacing

! This section is incomplete. Proceed with caution.

Puck has a first-class module system, inspired by such expressive designs in the ML family.

What are Modules?

pub mod stack =
  pub type Stack[T] = class
    init(static type Self): Stack[T]
    push(mut Self, val: T)
    pop(mut Self): T?
    peek(lent Self): lent T?

  pub mod list =
    type ListStack[T] = list[T]

    pub func init[T](self: static type ListStack[T]): Stack[T] = []
    pub func push[T](self: mut ListStack[T], val: T) = self.push(T)
    pub func pop[T](self: mut ListStack[T]): T? = self.pop
    pub func peek[T](self: lent ListStack[T]): lent T? =
      if self.len == 0 then None else Some(self.last)

use stack.list

let a = ListStack[int].init
print a.len # error: unable to access method on private type outside its module

a.push(5)
print a.pop # Some(5)

Modules package up code for use by others. Identifiers known at compile time may be part of a module: these being constants, functions, macros, types, and other modules themselves. Such identifiers may be made accessible outside of the module by prefixing them with the pub keyword.

Importantly, files are implicitly modules, public and named with their filename. The mod keyword followed by an identifier and an indented block of code explicitly defines a module, inside of the current module. Modules are first class: they may be bound to constants (having the type : mod) and publicly exported, or bound to local variables and passed into functions for who knows what purpose.

Using modules

The use keyword lets you use other modules.

The use keyword imports public symbols from the specified module into the current scope unqualified. This runs contrary to expectations coming from most other languages: from Python to Standard ML, the standard notion of an "import" puts the imported symbols behind another symbol to avoid "polluting the namespace". As Puck is strongly typed and allows overloading, however, we see no reason for namespace pollution to be of concern. These unqualified imports have the added benefit of making uniform function call syntax more widely accessible. It is inevitable that identifier conflicts will exist on occasion, of course: when this happens, the compiler will force qualification (this then does restrict uniform function call syntax). We discuss this more later.

Nonetheless, if qualification of imports is so desired, an alternative approach is available - binding a module to a constant. Both the standard library and external libraries are available behind identifiers without use of use: std and lib, respectively. (FFI and local modules will likely use more identifiers, but this is not hammered out yet.) A submodule - for example, std.net - may be bound in a constant as const net = std.net, providing all of the modules' public identifiers for use, as fields of the constant net. We will see this construction to be extraordinarily helpful in crafting high-level public APIs for libraries later on.

use std.[logs, test]
use lib.crypto, lib.http

Multiple modules can be imported at once. The standard namespaces deserve more than a passing mention. There are several of these: std for the standard library, lib for all external libraries, pkg for the top-level namespace of a project, this for the current containing module... In addition: there are a suite of language namespaces, for FFI - rust, nim, and swift preliminarily - that give access to libraries from other languages. Recall that imports are unqualified - so use std will allow use of the standard library without the std qualifier (not recommended: several modules have common names), and use lib will dump the name of every library it can find into the global namespace (even less recommended).

Implicit Modules

A major goal of Puck's module system is to allow the same level of expressiveness as the ML family, while cutting down on the extraneous syntax and boilerplate needed to do so. As such, access modifiers are written directly inline with their declaration, and the file system structure is reused to form an implicit module system for internal use. This - particularly the former - limits the structure a module can expose at first glance, but we will see later that classes recoup much of this lost specificity.

We mentioned that the filesystem forms an implicit module structure. This begets a couple of design choices. Module names must be lowercase, for compatibility with case-insensitive filesystems. Both a file and a folder with the same name can exist. Files within the aforementioned folder are treated as submodules of the aforementioned file. This again restricts the sorts of module structures we can build, but we will see later that this restriction can be bypassed.

The this and pkg modules are useful for this implicit structure...

Defining interfaces

...

Defining an External API

The filesystem provides an implicit module structure, but it may not be the one you want to expose to users.

...