diff options
author | JJ | 2023-07-24 23:45:46 +0000 |
---|---|---|
committer | JJ | 2023-07-24 23:51:37 +0000 |
commit | 10a46b468addc0fc697a1045a1de513cecf1119f (patch) | |
tree | 652c9241902051762d86ace2802f244752860d5a | |
parent | 3b4cf68650041e1c4be43d7eba58879e37b2c760 (diff) |
docs: flesh out the initial module system
-rw-r--r-- | docs/MODULES.md | 46 |
1 files changed, 46 insertions, 0 deletions
diff --git a/docs/MODULES.md b/docs/MODULES.md new file mode 100644 index 0000000..2aa0333 --- /dev/null +++ b/docs/MODULES.md @@ -0,0 +1,46 @@ +# Modules and Namespacing + +Puck has a rich module system, inspired by such expressive systems in the ML family of languages, most notably including Rust. 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. Puck cleans up such "namespace pollution" by **type-based disambiguation**. <!-- Puck allows for the usage of such unqualified identifiers by **type-based disambiguation**. --> + +```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, of course, 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. In support of encapsulation, fields of types, too, are considered separate from the type itself and also must be `pub` to be accessible. Identifiers from imported modules, of course, are not considered part of the current module unless explicitly exported. + +```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? |