Proto
Proto is a mechanism to define something like an object class on the fly. It contains an Environment, into which you put values (corresponding to instance variables) and functions (corresponding to methods). Now the object acts essentially like an object of a particular class, except that the class can be defined and changed at runtime.
Any messages sent to a Proto object that are not listed below will be sent to the environment by retrieving the environment variable corresponding to the message selector. If that value is a function, it will be evaluated using the arguments supplied (if any) and the result returned. Otherwise, the value of the variable will be returned. If no variable exists, nil will be returned.
In other words:
- if you assign a function to an environment variable, it acts like a method of a class;
- if you assign any other value to an environment variable, it acts like an instance variable of a class.
// ~methodName.(arg list) is a way to enter a different method while inside a method function
~method2 = { |a, b| ~method1.(a) * b };
n = Proto({
~mul = { |x, y| [x, y, x * y] };
});
n.mul(5, 4);
--> [ 5, 4, 20 ]
There is a small performance cost, primarily associated with switching the current environment for practically every access. There is also a lookup cost from using environment variables rather than hard-coded variables. This cost is negligible, and acceptable for a structure that needs maximum flexibility.
*new(initFunc, env, parentKeys)
Create a new instance of Proto, containing only the instance variables and functions defined in the initFunc. A base environment may be given as env. Functions (methods) will automatically be moved to the environment's parent. Other variables may be moved to the parent by listing the variable names in an array of symbols passed in as parentKeys. Moving them to the parent makes them analogous to class variables.
a = Proto({
~a = 1;
~b = 2;
~mulAB = { ~a * ~b };
}, parentKeys: #[b]);
a.env
Environment[ (a -> 1) ]
a.env.parent
Environment[ (mulAB -> a Function), (b -> 2) ]
init(func)
Throw away the environment of this Proto, and create a new environment with the instance variables and functions defined in the function argument. Should generally not be called by the user.
copy
Create an instance.
clone(modFunc, parentKeys)
.clone creates a copy of the receiving Proto and executes the modFunc to change method and variable definitions. It is like creating a subclass of an Proto, with new methods and variables.
Parent environments are nested for purposes of inheritance.
a = Proto({
~a = 1;
~b = 2;
~mulAB = { ~a * ~b };
}, parentKeys: #[b]);
b = a.clone({
~c = 3;
~divAC = { ~a * ~c };
});
b.env
Environment[ (c -> 3), (a -> 1) ]
b.env.parent
Environment[ (divAC -> a Function) ]
b.env.parent.parent
Environment[ (mulAB -> a Function), (b -> 2) ]
~a is an instance variable and remains in the lowest-level environment in the clone. ~c is a new instance variable defined in the clone and is naturally in the lowest-level environment. ~divAC is a new method and goes in the clone's parent. The clone (b) also inherits from a; thus a's parent becomes the grandparent. At the lowest level, all variables and methods are usable. If you change the value of ~b in a's parent, the new value will be seen in all Protos that inherit from a (exactly as a class variable should do).
Normally you do not need to worry about this structure.
import(objectKeyDict, parentKeys)
You can steal methods from another Proto, as a means of multiple inheritance. With a and b as above,
c = Proto({
~addAB = { ~a + ~b };
});
Note that c does not inherit from a or b. To make a composite containing all methods.
d = b.copy.import(((`c): #[addAB]));
objectKeyDict is of the form (object_reference: key_list, object_reference: key_list, ...). Object_reference may be:
- a Proto or Dictionary: look up the imported keys in the object directly.
- a Symbol: look up the keys in the chucklib process prototype named for the symbol -- PR(object_reference)
- a Ref: a syntax shortcut, used above to protect a variable name from becoming a symbol in the () dictionary syntax.
The parentKeys argument behaves as in *new and clone.
at(key)
put(key, value)
putAll(... dictionaries)
Long form to access or change environment variables.
parent
Access the environment's parent.
use(func)
Enter the environment and execute code.
next(... args)
value(... args) // calls ~next(*args)
reset
update
asStream
asPattern
free
Evaluates the function contained in the corresponding environment variable with the arguments supplied. If no function is given, the call returns nil. This is to allow Protos to behave as streams.
Note: 'next' and 'value' both call the function stored under ~next. For Streams, 'next' and 'value' are synonyms; likewise, they are implemented the same in Proto.
perform(selector, ... args)
tryPerform(selector, ... args)
respondsTo(selector)
As in Object.
Self-documentation:
listVars
listMethods
help
Trivial example (sc output in purple):
(
n = Proto.new({
// a variable
~stream = Pxrand([0, 2, 4, 5, 7, 9, 11], inf).asStream;
// a "method"
~next = { ~stream.next };
});
// keep the method, but change the variable
o = n.clone({ ~stream = Pseq([0, 1, 2], inf).asStream });
)
10.do({ [n.next, o.next].postln }); // .next evaluates ~next, returning ~stream.next
// stream is different for both nodes
[ 11, 0 ]
[ 0, 1 ]
[ 4, 2 ]
[ 5, 0 ]
[ 7, 1 ]
[ 4, 2 ]
[ 11, 0 ]
[ 5, 1 ]
[ 11, 2 ]
[ 5, 0 ]
n.stream // stream is a "soft" method "defined" in the environment (a getter)
a Routine
Mimicking object-oriented behavior using Proto
(
a = Proto({
// class/instance variables are defined dynamically, here and at run time
// distinction between class and instance variables appears later
~a = 1;
~b = 2;
// all functions are moved to the parent environment to be shared between instances
~times = { |mul|
mul.notNil.if({ mul }, { ~a }) * ~b
};
// env: you can merge an existing environment into the new Proto
// parentKeys: moves non-function values into the parent environment, making them class variables
}, env: nil, parentKeys: #[b]);
)
a.times;
a.times(5);
// Make a new instance with .copy
b = a.copy;
b.a = 3;
b.times;
// Change class variable -- syntax is not so convenient
// Class variables are possible but tricky to manage
// Easier to change in the Proto that defined the class variable (propagates to clones/copies)
// Later I might make it better
b.parent.b = 5;
b.times(10);
a.times(10);
// Inheritance with .clone
c = b.clone({
~div = { |denom|
~a / denom.notNil.if({ denom }, { ~b });
};
});
a.div; // nil response, method not defined
c.div;
c.times; // times method is inherited from b
a.parent.b = 10;
c.div; // change to a's class variable affects c
// currently user must know at which level the variable resides -- I may change this later
// Polymorphism -- d overrides c's definition of ~div
d = c.clone({
~div = { |num|
num.notNil.if({ num }, { ~a }) / ~b
};
});
c.div(50);
d.div(50);
// Multiple inheritance
// clumsy syntax
e = a.clone.import(Dictionary[c -> #[div]]);
// nicer syntax, using Ref for c variable
e = a.clone.import(((`c): #[div]));
// e now has a copy of c's div method, though it inherits directly from a
e.div;