Domains by Default
The module system in Leema is designed to guide the programmer naturally towards domain-driven design. Data types and behaviors are exposed by one module to another based on how the modules relate to one another.
Types and functions are:
- fully accessible to modules that are direct descendants. As a direct descendant, the module is in the same bounded context and thus can access the implementation details of its ancestors.
- accessible by default to modules that are parents, siblings or descendants of siblings (nieces). As siblings, the modules are within the same domain but in separate subdomains. They should be able to access their public data (the module itself) but none of their private implementation (any submodules).
- inaccessible by default to modules that are grandparents (or further ancestors), siblings of ancestors (aunts) or their descendants (cousins). A module’s parent by default makes its children’s behavior inaccessible to its grandparents or siblings which are in separate domains or too far from the child’s subdomain to have access to implementation details.
The default behavior from 2) and 3) can be overridden and is described in the Import/Export and Local sections below.
Consider this example domain for a marketplace.
As a marketplace, there are products that are for sale and there are buyers and sellers of those products. The buyers and sellers can both view the products, but they access them in different ways and for different purposes. The sellers access the products in an inventory domain while the buyers access the products in a catalog domain.
Given the default accessibilities described above and this example domain:
- Everything in the marketplace module is accessible by all of its descendents
- The seller module is the sibling of the buyer module and the aunt of the catalog module so is accessible from both of them
- The catalog module is a niece of the seller module and the grandchild of the marketplace module so is inaccessible from both of them
While parent models hide the types and behavior of their children by default, that can be overridden with an export statement. Exporting modules also makes them implicitly accessible from within the exporting module.
Given the example above, consider if the following export statement were in the marketplace module:
export -> product buyer/catalog -> load store -- seller/inventory --
This statement exports from the marketplace module:
- the product module
- the load and store functions in the catalog module
- the inventory module from within the seller module
Exported modules must be:
- listed relative to the exporting module
- descendants of the exporting module
- accessible from the exporting module
If another module is using the marketplace module, its exports can be imported with a similar syntax.
import -> marketplace -> product buyer/catalog -> load store -- seller/* -- --
If a module needs to not make something inaccessible to its parent and siblings, that can be achieved with the local keyword.
local func foo:Str :: x:Int a:Str -> ... --
Exporting a child module makes it accessible from within the current module but also makes it accessible externally. In many cases, a module just wants to access a descendant module without making it accessible externally. This can be done with an include statement that uses the same syntax as an export.
import -> product buyer/catalog -> load store -- seller/* --