docs/semantics/encapsulation.md
Encapsulation is the system of hiding certain internal entities (modules, types, methods, constructors, fields) in one project/library from other projects/libraries. This document is an excerpt from the discussion held at https://github.com/orgs/enso-org/discussions/7088.
from Library import all.from Library import Symbol_1, Symbol_2, ....import Library.Public_Module.Public_Type.Library.Public_Module.Public_Type.Let's introduce a private keyword. By prepending (syntax rules discussed
below) private keyword` to an entity, we declare it as project private. A
project-private entity is an entity that can be imported and used in the same
project, but cannot be imported nor used in different projects. Note that it is
not desirable to declare the entities as module private, as that would be too
restrictive, and would prevent library authors using the entity within the
project.
From now on, let's consider project-private and private synonymous, and public as an entity that is not private.
All the entities, except modules, shall be declared private by prepending them
with private keyword. Declaring a module as private shall be done be writing
the private keyword at the very beginning of the module, before all the import
statements, ignoring all the comments before. Fields cannot have private
keyword, only constructors. Types cannot have private keyword as well - only
methods and constructors.
Modules can be specified as private. Private modules cannot be imported from other projects. Private modules can be imported from the same project.
A hierarchy of submodules can mix public and private modules. By hierarchy, we mean a parent-child relationship between modules. It does not make sense to create a public submodule of a private module and export it, but it is allowed. Note that this is because of current limitations of the implementation, this might be more strict in the future.
Types cannot be specified as private, only constructors and methods. A type must have all the constructors private or all the constructors public. This is to prevent a situation when a pattern match can be done on public constructor, but cannot be done on a private constructor from a different project. Mixing public and private constructors in a single type is a compilation error. A type with all constructors public is called an open type and a type with all constructors private is called a closed type.
To make a type private put it into a private module. Then it is hidden, just like anything else in the module.
Methods on types (or on modules) can be specified private. To check whether a private method is accessed only from within the same project, a runtime check must be performed, as this cannot be checked during the compilation.
Conversion and foreign methods cannot be specified as private.
No polyglot foreign code can access private entities. For all the foreign code, private entities are not visible.
Lib/src/Open_Type.enso:
type Open_Type
Constructor field
private priv_method self = ...
pub_method self = self.field.to_text
Lib/src/Closed_Type.enso:
type Closed_Type
private Constructor field
private priv_method self = ...
factory field = Closed_Type.Constructor field
pub_method self = self.field.to_text
Lib/src/Methods.enso:
pub_stat_method x y = x + y
private priv_stat_method x y = x - y
Lib/src/Internal/Helpers.enso:
# Mark the whole module as private
private
Lib/src/Main.enso:
import project.Open_Type.Open_Type
export project.Open_Type.Open_Type
tmp.enso:
from Lib import Open_Type, Closed_Type
import Lib.Methods
import Lib.Internal.Helpers # Fails during compilation - cannot import private module from different project
main =
# This constructor is not private, we can use it here.
obj = Open_Type.Constructor field=42
obj.field # OK - Constructor is public, therefore, field is public
obj.priv_method # Runtime failure - priv_method is private
Open_Type.priv_method self=obj # Runtime failure
obj.pub_method # OK
# This constructor is private, we have to use factory method.
# Note that directly calling `Closed_Type.Constructor` would fail.
opaque = Closed_Type.factory field=42
opaque.field # Runtime failure - Constructor is private, therefore, no getter is generated
opaque.priv_method # Runtime failure - priv_method is private
Closed_Type.priv_method self=opaque # Runtime failure
opaque.pub_method # OK
Methods.pub_stat_method 1 2 # OK
Methods.priv_stat_method # Fails at runtime
There shall be two checks. One check during compilation, that can be implemented as a separate compiler pass, and that will ensure that no private entity is re-exported (exported from a module that is different from the module inside which the entity is defined) and that for every type it holds that either all the constructors are public or all the constructors are private
The second check shall be done during the method/name resolution step. This step happens at runtime, before a method is called. After the method is resolved, there shall be no further checks, so that the peak performance is not affected.
The performance hit on compilation time is minimal, as there are already dozens of different compiler passes. Moreover, in the new compiler pass we shall check only imports and exports statements, no other IR.
The performance hit on runtime, during method resolution, is minimal as well, because it can be as easy as additional lookup in a hash map. Peak performance will not be affected at all, as there are no further checks after method resolution.
Sometimes it is useful to be able to access internal entities. Testing is the
most obvious example. Let's introduce a new CLI flag to the Engine launcher
called --disable-private-check, which will disable all the private checks
during compilation and method resolution.