Components are the units of compilation and deployment. The export of a component is a module expression that will be evaluated when executing the component. A component can import other components, which may reside at remote URLs. Imported components are loaded and evaluated lazily. This linking operation includes a dynamic type check between import and export signatures.
An application is defined by a set of components and a designated root component that represents its main body. Linking is performed and controlled by user-definable component managers, enabling the definition of sandboxes for evaluating untrusted components.
Syntactically, a component is an SML program headed by a set of import announcements of the form
import imp from "url"
The URL describes where to find the component, while the import description imp denotes what kind of item is imported from that particular component. For example,
import structure Foo from "http://ps.uni-saarland.de/stockhausen/Foo"
An import can contain any language entity, like values, types, structures, signatures, etc. (see below). Imports can be given in plain form like above, in which case the compiler will look up the actual type of the entity in the component itself, or in description form similar to specifications, in which case the compiler verifies that the description matches the actual component:
import signature FOO from "http://ps.uni-saarland.de/stockhausen/FOO-sig" import structure Foo : FOO from "http://ps.uni-saarland.de/stockhausen/Foo"
Each component exports exactly its body environment, i.e. all entities declared on its toplevel. For example, the component
import structure Y from "other" signature S = sig end structure X :> S = struct end
exports a signature item S and a structure X. The export signature of the component will consist of these two items. Structure Y is not exported (but may be by a simple rebinding, if desired).
A compiled component also includes information about all its import signatures. These are determined by the export signatures found in the imported components at compile time.
For convenience, it is possible to abbreviate import announcements as follows in the interactive top-level:
Such an announcement will import all items exported by the corresponding component.
An application is executed by evaluating its root component. In general, a component imports other components. Imported components are loaded and evaluated lazily by the default component manager, which is part of the runtime system. A component that is never actually accessed will not get loaded. URL resolving is handled by the component manager's resolver.
Lazy linking is achieved by representing imports by lazy futures of their result module. A component gets linked when the future is requested. The first step of linking is matching the export signature of the linked component against the corresponding import signature of the component that requested it. If the check is succesful, the component is evaluated concurrently and the future is replaced by a concurrent future of the result of the evaluation.
Every component is loaded and evaluated at most once in a single process. If multiple components request an import from the same (resolved) URL, they will share a reference to the same instantiation of that component.
Linking may fail for several reasons. In any case, the corresponding futures are failed with a Component.Failure exception indicating the respective cause:
This may happen transitively with other components requested indirectly, in which the Component.Failure will propagate through. The exception carries the URL of the component that originally caused the failure.
Components can be created dynamically as first-class values, by means of the following expression syntax:
comp <imports in> specs with decs end
The structure of this expression is similar to that of a compilation unit: it starts with an optional list of imports, and a sequence of declarations represents the export of the created component. However, unlike for compilation units, the export signature has to be given explicitly, in form of a sequence of specifications. Another difference is that import descriptions may not be omitted.
For example, the following program creates a component that will print its creation date when invoked later:
val date = Date.toString(Date.fromTimeLocal(Time.now())) val component = comp import structure TextIO : TEXT_IO from "x-alice:/lib/system/TextIO" in val hello : unit -> unit with fun hello() = TextIO.print("Hello world! Created at " ^ date ^ "\n") end
This component can be evaluated with the component manager:
structure C = unpack ComponentManager.eval(Url.fromString ".", component) : (val hello : unit -> unit) do C.hello()
Alternatively, the component can be saved:
It can then be imported as if it was an ordinary compiled component, using an import announcements:
import val hello : unit -> unit from "/tmp/hello" do hello()
Components can also be created directly from a package, using the function Component.fromPackage. Such components are essentially constant and cannot have any imports.
First-class components may be stored or passed to other processes. A particular use case is distributed programming.
Unlike components generated by the compiler, first-class components are not restricted to ground signatures: using the Component.Create functor components can be created whose export is a higher-order module. Components with higher-order export signatures cannot be imported by import announcements, though. They can be evaluated only through the ComponentManager.Eval functor.
Components generated by the compiler are unevaluated. Linking them will evaluate the contained module expression. Dynamically created components may be evaluated. An evaluated component has no imports and contains a module value. Linking an evaluated components hence does not produce any side effects, nor will it create any new generative entities, like types or exceptions.
Evaluated components stored to a file are called pickles. A pickle is a serialized and closed representation of a value. The library structure Pickle provides convenient operations for handling pickles.
An application's root component is located and loaded by the root component manager. An application may create custom managers using the Component.MkManager functor. By evaluating a first-class component using the Eval functor of a custom manager, all direct and indirect imports of that component will be handled by that respective manager.
User-defined component managers particularly allow setting up custom sandbox environments for untrusted components. The library structure Sandbox contains some infrastructure to readily create sandboxes - see the examples shown there.
Derived forms are marked (*).
|comp <ann in> spec with dec end||first-class component1|
|ann||::=||import imp from string||import announcement|
|import string||import all (*)|
|ann <;> ann|
|infix <d> vid1 ... vidn||(n≥1)|
|infixr <d> vid1 ... vidn||(n≥1)|
|nonfix vid1 ... vidn||(n≥1)|
|imp <;> imp|
|valitem||::=||<op> vid <and valitem>|
|<op> vid : ty <and valitem>|
|typitem||::=||tycon <and typitem>|
|tyvarseq tycon <and typitem>|
|datitem||::=||tycon <and datitem>|
|tyvarseq tycon = conitem <and datitem>|
|conitem||::=||<op> vid <of ty> <| conitem>|
|extitem||::=||tycon <and extitem>|
|tyvarseq tycon <and extitem>|
|econitem||::=||<op> vid <and econitem>|
|exitem||::=||<op> vid <and exitem>||(*)|
|<op> vid of ty <and exitem>||(*)|
|stritem||::=||strid <and stritem>|
|strid : sigexp <and stritem>|
|funitem||::=||strid <and funitem>||(*)|
|strid atstrpat1 ... atstrpatn : sigexp <and funitem>||(n≥1) (*)|
|sigitem||::=||sigid <and sigitem>|
1) The syntax of import items in a first-class component is restricted to the syntactic subclass of the corresponding descriptions, as used in signatures.
|import string||import imp from string 1)|
|exception exitem||constructor exitem|
|functor funitem||structure funitem|
|<op> vid <and exitem>||<op> vid <and exitem>|
|<op> vid of ty <and exitem>||<op> vid of ty : exn <and exitem>|
|strid <and funitem>||strid <and funitem>|
|strid atstrpat1 ... atstrpatn : sigexp <and funitem>||strid : fct atstrpat1 -> ...fct atstrpatn -> sigexp<and funitem>|
1) The imp is generated by enumerating all items exported by the respective component.