-
Notifications
You must be signed in to change notification settings - Fork 5
Concepts, properties and messages
Falcon2 object model is based on Aspect Oriented Programming; AOP itself refers to Object Oriented Programming, but focuses on set of common operations called aspects, which cross-cut multiple isntances, or classes, depending on the flavor of AOP.
The base entity of OOP in Falcon2 is the concept. Concepts are similar to isntances in traditional OOP, the main difference being that they don't have a class; instead, they can have a prototype, which describes their default behavior, and which can be specifically overridden on an object-by-object base.
Concepts are defined by:
- the prototype(s) they are based on;
- properties, which define their structure;
- values, which describe what a concept is worth in a C-rel computation.
- A direct, and optionally a recursive coalesce function, which describes how multiple incoming values are coalesced in a C-rel computation;
- Optionally a retroaciton function, describing how the direct and recursive coalesce functions interact.
Notice the prominent absence of methods. Properties can store functions, and some ways to evaluate those functions will create an environment similar to what methods usually observe in a OOP model, but the mechanism for method invocation used by Falcon2 is somewhat different.
Defining a concept is simply executing an expression set in a concept creation context; every symbol defined in that context becomes a property of the concept.
For example:
x = 100
concept NAME {
prop0 = x * 10
prop1 = "some value"
prop2 = {() ...}
function prop3 {() ...}
}
// equivalent to
NAME = :concept(NAME, {
...
})
The concept definition happens inline with the rest of the program flow, and respects the default visibility rules. Notice that this includes the fact that the :concept() function will be greedy about any symbol defined during its evaluation, i.e. the symbols defined in the body will not be visible outside :concept() evaluation.
If you need temporary variables within the concept definition, use the my declaration; this will make the symbol local to the {} expression sequence block, and invisible to :concept().
Naming a concept is optional, and mainly used for debugging and reporting. For all intent and purpose, a concept can be referenced through the symbol it is assigned to.
Concepts can receive three type of messages to:
- setting a property value
- getting a property value
- evaluating a property
// Getting a property value
printl(cp.prop0) // or
printl(:getp(cp, "prop0"))
// Setting a property value
cp.prop0 = value // or
printl(:setp(cp, "prop0", value))
// evaluating a property
cp.prop1(1,2,3 ...) // or
:send(cp, "prop1", 1, 2, 3...)
Instead of passing the property name to
:send
,:setp
and:getp
as a string, it's possible to use a symbolic name through the "$" symbol prefix, as i.e.$prop1
.
In case of property evaluation, the :send() function sets a special symbol called self, that will be visible in the property evaluation context, representing the concept on which the evaluation is performed. For example:
cp = :concept({
name = "A concept"
display = {(prompt)
print(prompt, self.name)
}
})
:send(cp, $display, "My name is:") // or cp.display("My name is:")
By default, it's an error to send any message for an unexisting property; so, normally, setting the value of a property that doesn't exist would cause an error. However, this behavior can be overridden.
Sending a message for a property equates to getting the value of the property, setting the
self
context and evaluating it. As everything can be evaluated, it is not necessary to know in advance whether the property receiving the message is a function or any other entity. For example:
concept C {
p0 = "Hello"
p1 = {() return "World"}
}
printl(C.p0) // "Hello"
printl(C.p0()) // still "Hello"
printl(:send(C, $p0)) // again "Hello"
printl(C.p1) // it's a function
printl(C.p1()) // "World"
It is possible to access proeprties bypassing the message system, by using the double dot operator '..', or using the :property() function.
// Direct property access
print(cp..prop0) // or
:property(cp, "prop0")
// Direct property assignment
cp..prop1 = "New value" // or
:property(cp, "prop1", "New Value")
This mechanism bypassees any message control (handling and delegation), allowing the user to overwrite properties masked by accessors or redirected away from an object. Also, conversely to :setp(), setting :property() will normally create a new property, if it wasn't previously defined.
While all concepts can define new message handlers that were not originally present, not all concepts can be given different properties from the original ones. These are called fixed concepts.
Concepts can specifically handle any message passed to them, that is, redirect the message to a handling
function, which will receive the self
concept as if it was invoked from a property.
The function :hset
redirects the proeprty setting message, :hget
redirects the property
retreival and :hsend
redirects the evaluation message. They return an handler that can
be used to invoke the previous handler, or the base behavior if that was the first handler,
and to remove it with clear.
h = :hset(cpt, "property", handler)
cpt.property = 1 // invokes handler(1)
h(2) // sets the property to 2
h.clear() // removes the handler
h = :hget(cpt, "property", handler)
print(h()) // gets the property
h = :hsend(cpt, "func", handler)
h(1,2,3) // invokes cpt.func(1,2,3)
This functions declare a local symbol called hmsg
, that can be closed by the handler in order
to access the pre-existing handlers/underlying properties. For example
:hsend(cpt, $prop, {(param)[hmsg]
printl("My parameter was ", param)
hmsg(param)
})
The functions :hhget(cpt, prop)
, :hhset(cpt, prop)
and :hhsend(cpt, prop)
return
respectively the current (topmost) set, get and send topmost handler for that property.
This allow to access the handler from common functions, handliong properties for multiple
objects, or obviate the need for creating a closure for the hmsg
local variable.
The handler might also invoke
hmsg.clear()
to remove itself from the chain of handlers.
A handler can also access the underlying property using ..
on self
, but this will
bypass the existing chain of handelrs, if any.
The function :fwd
can evaluate an expression/function keeping the same self
and
parameters received the current evaluation.
For example:
counter = {
count = 0
function inc{(amount=1) self..count += amount}
}
:hsend(counter, "inc", {(amount=1)
printl("Incrementing ", self..count, " of ", amount)
// With :fwd, the function in self..inc will inherit the current self.
:fwd(self..inc)
})
:fwdp
maintains theself
context, but allows to send arbitrary parameters.
It is possible to create handlers for multiple messages, or for messages directed to non-existing properties, using the regular-expression match handlers.
Passing a string or regular expression to the :hset
, :hget:
and :hsend
function,
in place of a symbol, results in message handling by name:
concept cpt {}
:hset(cpt, ".*",
{(prop, val)[hmsg] printl("cpt.", prop, "=", val)}
)
:hget(cpt, ".*",
{(prop)[hmsg] printl("cpt.", prop)}
)
:hsend(cpt, ".*",
{(msg, ...)[hmsg] /* handle msg(...) */ }
)
Notice that this version of the functions require the handler to provide an extra parameter, which will receive the property/message that was invoked in a string.
Concepts can be based on one or more prototypes, which provides them with basic functionalities. In practice, every message not found in the topmost concept will be passed to its prototypes, first last and recursively down to their prototypes, for them to reply.
To rely on prototypes, a concept needs to expose a prototypes
property, which will contain a single
concept, or a vector of concepts. It is then said that the concept is derived from the exposed
prototypes.
Whenever the system wants to derive a new concept out of an existing one, it invokes its derive
message.
A concept planning to be a prototype should expose a deerive
proeperty in order to configure newly derived
concepts.
The derive
message is invoked with the new concept being formed as its first parameter,
and with other initialization parameters.
The :proto
function will automatically store its first parameter in the prototypes
proprty
of the current self
and evaluate the derive
property of the prototype, passing its other
parametrs directly to it.
concept base {
whoami = {() printl("I am ", self.name)}
derive = {(ncnt, name) ncnt..name = name }
}
concept derived {
:proto(base, "Derived")
/* This is equivalent to:
prototypes = base
base.derive(self, "Derived")
*/
}
derived.whoami() // "I am Derived"
Although it's possible to manipulate the prototypes
property directly,
it's strongly advised to use the :proto
function, as it will also check for constraints,
in particular for the dependency graph to be acyclic.
Creating cyclic dependencies might result in endless loops when sending
The :new
function generates a derived concept,
for which the original one becomes the prototype. This process is called rubber stamping.
concept base {
whoami = {() printl("I am ", self.name)}
derive = {(name) self..name = name }
}
derived = :new(base, "a new concept")
derived.whoami() // prints "I am a new concept"
Using :new
is the preferred way to create an "instance" out of base concept. If multiple
base concepts are involved (what is usually addressed through multiple inheritance in OOP),
an intermediate prototype should be derived via :proto
from the bases, and then each
final concept should be created through :new
concept B1 {
fromB1 = {() printl("from B1") }
}
concept B2 {
fromB2 = {() printl("from B2") }
}
concept entry {
:proto(B1)
:proto(B2) # Or proto(B1, B2)
callMe = {() self.fromB1(); self.fromB2()}
}
e1 = :new(entry)
e1.callMe()
e1.fromB1()
e1.fromB2()