5 ms·
A key part of the design is the way environments are implemented. An environment in Kernel consists of two parts: A local map of bindings from symbols to expres
by sparkie 6y ago
A key part of the design is the way environments are implemented. An environment in Kernel consists of two parts: A local map of bindings from symbols to expressions, and an ordered list of references to parent environments. Importantly, the references to parent environments may not be accessed directly, and the parent list is immutable.
When a symbol is looked up in an environment, it first checks the local bindings, and if not found, it will then look through the bindings of the parents, in order. This forms a depth-first search through a potential DAG of environments, with the current environment at the root.
The constructor (make-environment) takes an optional list of parents. The order specified is the order in which they will be searched when a lookup occurs.
Mutation is only possible for the local bindings of the root environment. You cannot mutate the bindings of a parent environment. This is one of the key improvements over fexprs, which would be able to mutate anything in the dynamic environment. Since it is only possible to mutate the local bindings, and you can only do so if given a direct reference to an environment, this places a constraint on the potential side-effects an operative could perform. An operative can only mutate the local bindings of its caller, unless explicitly given reference to some other environment.
- sparkie 6y agoLast part unless you have any questions and I will expand I mentioned all types in Kernel are disjoint, but didn't mention that they are encapsulated. All primitive types are encapsulated, but you can also create your own encapsulations. There is no bolts-attached object or records libraries like in lisp or scheme to achieve this: just one simple primitive: (make-encapsulation-type), which takes no arguments. The primitive returns a 3-tuple consisting of a constructor, a predicate to test if an object is of the encapsulated type, and an elimination function to access the value with which it was constructed. Generally, you would expose the predicate by binding it to a symbol in an accessible environment, but you would not reveal the constructor and eliminator - you would use them only inside the static environment where defining your encapsulation. To give and example, lets use the compound operative type I referred to above - which is essentially a 4-tuple. I used the functions `get-static-env`, `get-parameter-tree` etc without defining them earlier. I will define them here. ($provide! ($vau get-static-env get-parameter-tree get-symbol-for-dynamic-env get-body operative?) ($define! (make-operative operative? eliminate-operative) (make-encapsulation-type)) ($define! $vau ($vau-primitive (formals symbol body) static-env (make-operative (list formals symbol body static-env)))) ($define! get-static-env ($lambda (operative) ($let ((#ignore #ignore #ignore static-env) (eliminate-operative operative)) static-env))) ($define! get-parameter-tree ($lambda (operative) ($let ((formals #ignore #ignore #ignore) (eliminate-operative operative)) formals))) ($define! get-symbol-for-dynamic-env ($lambda (operative) ($let ((#ignore symbol #ignore #ignore) (eliminate-operative operative)) symbol))) ($define! get-body ($lambda (operative) ($let ((#ignore #ignore body #ignore) (eliminate-operative) operative)) body))) $provide! here binds the list of symbols in its first operand into the dynamic environment where $provide! was called. The remaining operands to $provide! mutate the local environment of $provide! but not the dynamic environment from which $provide! was called. This means that the definitions of 'make-operative' and 'eliminate-operative' are only accessible from within the local scope of provide, but are no longer accessible directly once $provide! has been evaluated. Note that $provide! itself is not a language primitive, but a function in the standard library. It is a compound operative implemented using $define! The user of this 'library' can only use the 6 combiners provided at the top. The details of how operatives are implemented (as a 4-tuple) are hidden from any use of these combiners. This clever way in which environments, encapsulations and operatives work together is truly a gem. The decision to forbid mutating parent environments, and the ability to encapsulate objects fixes the problems of old fexprs, which were very unintuitive to work with.
- oxymoron 6y agoThis is excellent. Thanks for putting in the time of writing it up!