aboutsummaryrefslogtreecommitdiff
path: root/docs/MODULES.md
blob: 4f5bb7000e4ff3abc09f8eda7572534594680b13 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# Modules and Namespacing

Puck has a rich module system, inspired by such expressive systems in the ML family of languages, notably including Rust and OCaml. Unlike these systems, however, opening modules i.e. unqualified imports is *encouraged* - even in the global scope - which at first would appear to run contrary to the point of a module system. Such "namespace pollution" is made a non-issue by **type-based disambiguation** (and can be avoided regardless with qualified imports anyway).

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.

```puck
import std/[ascii, unicode]

todo
```

Type-based disambiguation allows for the simultaneous seamless existence of functions with the same name and different type signature: and further, the usage of multiple functions with identical signatures and multiple types with identical names through explicit disambiguation. This is nothing fancy and follows some simple scoping rules:
- A module cannot define two constants with the same name.
- A module cannot define two functions with the same type signature.
- A module cannot define two types with the same name and generic parameter count.
- A module cannot define a constant and a zero-parameter function with the same name.
- A module cannot define an enum and a function with the same name. (note: to be weakened)

These unqualified imports by default may seem like madness to a Python or C programmer: but because Puck is *statically typed*, there is no ambiguity, and because of *uniform function call syntax*, it is usually clear what overloaded function is being called. Puck is also written with the modern tooling approach of language servers in mind: and so, navigating to the function declaration, in any IDE, should be two clicks or a keystroke away.

```puck
```

Multiple modules can be imported in the same scope, and so conflicts may arise on imports. In the pursuit of *some* explicitness, no attempt is made to guess the proper identifier from usage. These must be disambiguated by prefixing the module name, followed by a dot. This disambiguation breaks uniform function call syntax on functions: yet because functions only conflict when both their name and entire function signature overlap, this is a rare occurrence. If so desired, an import followed by `/[]` will force full qualification of all identifiers in the module - yet this is an antipattern and not recommended.

Unrelated to the module system - but note that functions only differing in return type are allowed, albeit discouraged. Extra type annotations may be needed for the compiler to properly infer in such cases.

```puck
```

This document has talked quite a lot about modules so far - with no mention so far of what modules actually *are*.

At their basic level: modules are a scope, defined by the `module` keyword followed by a label, a colon, and the module body. As such, modules may contain other modules, and further identifiers within a nested module are not imported by importing the outer module by default. The `module` keyword itself, however, will often go fairly unused: because an implicit form of modules are **files** in the **filesystem structure**.

The file structure forms an implicit, internal API, addressable by import statements using `..` and the previously-seen `module/submodule` syntax. Paths may be relative with `..` or absolute from the project root with `/`. Moving and renaming files will break imports, of course, so some care must be taken while refactoring. This will hopefully be a non-issue with the aid of lightweight tooling.

```puck
```

When linked as a library, however, the file structure is not visible - and so one must define an "external API", in the main file of the library. This is aided by the `export` statement which can be used to re-export imported modules as submodules of the existing module. Such an external API allows for significant flexibility in defining the structure of a library. Exports may be of a full module (`export module`), effectively merging it with the current module, or of individual items within the external module `export module/[one, two, three]`. Such exports may be placed within a new `module` scope for even more flexibility.

Modules do not export everything in their scope: indeed, all identifiers within a module are **private by default**, and must be explicitly marked public by use of the `pub` keyword. Some languages allow for individual fields of structs to be marked public, in better support of encapsulation. Unfortunately, this fundamentally breaks structural typing for a variety of reasons, and so is not supported. Only a binary distinction between fully visible and fully opaque types is allowed. Identifiers from imported modules are not considered part of the current module unless explicitly exported, of course.

Modules and identifiers from modules may be imported and exported `as` a different name.

```puck
```

todo: explore the relation between module systems and typeclasses. are hot-swappable types in modules (i.e. self Self) worth it, or would such a thing be better accomplished by `int`, `string`, etc being typeclasses?