#
cp.prop
This is a utility library for helping keep track of single-value property states. Each property provides access to a single value. Must be readable, but may be read-only. It works by creating a table which has a get
and (optionally) a set
function which are called when changing the state.
#
Features:
#
1. Callable
A prop
can be called like a function once created. Eg:
local value = true
local propValue = prop.new(function() return value end, function(newValue) value = newValue end)
propValue() == true -- `value` is still true
propValue(false) == false -- now `value` is false
#
2. Togglable
A prop
comes with toggling built in - as long as the it has a set
function. Continuing from the last example:
propValue:toggle() -- `value` went from `false` to `true`.
Note: Toggling a non-boolean value will flip it to nil
and a subsequent toggle will make it true
. See the
#
3. Watchable
Interested parties can 'watch' the prop
value to be notified of changes. Again, continuing on:
propValue:watch(function(newValue) print "New Value: "...newValue) end) -- prints "New Value: true" immediately
propValue(false) -- prints "New Value: false"
This will also work on
#
4. Observable
Similarly, you can 'observe' a prop as a cp.rx.Observer
by calling the observe
method:
propValue:toObservable():subscribe(function(value) print(tostring(value) end))
These will never emit an onError
or onComplete
message, just onNext
with either nil
or the current value as it changes.
#
5. Combinable
We can combine or modify properties with AND/OR and NOT operations. The resulting values will be a live combination of the underlying prop
values. They can also be watched, and will be notified when the underlying prop
values change. For example:
local watered = prop.TRUE() -- a simple `prop` which stores the current value internally, defaults to `true`
local fed = prop.FALSE() -- same as above, defautls to `false`
local rested = prop.FALSE() -- as above.
local satisfied = watered:AND(fed) -- will be true if both `watered` and `fed` are true.
local happy = satisfied:AND(rested) -- will be true if both `satisfied` and `happy`.
local sleepy = fed:AND(prop.NOT(rested)) -- will be sleepy if `fed`, but not `rested`.
-- These statements all evaluate to `true`
satisfied() == false
happy() == false
sleepy() == false
-- Get fed
fed(true) == true
satisfied() == true
happy() == false
sleepy() == true
-- Get rest
rested:toggle() == true
satisfied() == true
happy() == true
sleepy() == false
-- These will produce an error, because you can't modify an AND or OR:
happy(true)
happy:toggle()
You can also use non-boolean properties. Any non-nil
value is considered to be true
.
#
6. Immutable
If appropriate, a prop
may be immutable. Any prop
with no set
function defined is immutable. Examples are the prop.AND
and prop.OR
instances, since modifying combinations of values doesn't really make sense.
Additionally, an immutable wrapper can be made from any prop
value via either prop.IMMUTABLE(...)
or calling the myValue:IMMUTABLE()
method.
Note that the underlying prop
value(s) are still potentially modifiable, and any watchers on the immutable wrapper will be notified of changes. You just can't make any changes directly to the immutable property instance.
For example:
local isImmutable = propValue:IMMUTABLE()
isImmutable:toggle() -- results in an `error` being thrown
isImmutable:watch(function(newValue) print "isImmutable changed to "..newValue end)
propValue:toggle() -- prints "isImmutable changed to false"
#
7. Bindable
A property can be bound to an 'owning' table. This table will be passed into the get
and set
functions for the property if present. This is mostly useful if your property depends on internal instance values of a table. For example, you might want to make a property work as a method instead of a function:
local owner = {
_value = true
}
owner.value = prop(function() return owner._value end)
owner:isMethod() -- error!
To use a prop
as a method, you need to attach
it to the owning table, like so:
local owner = { _value = true }
owner.isMethod = prop(function(self) return self._value end, function(value, self) self._value = value end):bind(owner)
owner:isMethod() -- success!
owner.isMethod() -- also works - will still pass in the bound owner.
owner.isMethod:owner() == owner -- is true~
You can also use the
local owner = { _value = true }
prop.bind(o) {
isMethod = prop(function(self) return self._value end)
}
owner:isMethod() -- success!
The cp.prop
values it finds:
local owner = prop.extend({
_value = true,
isMethod = prop(function(self) return self._value end),
})
owner:isMethod() -- success!
The bound owner
is passed in as the last parameter of the get
and set
functions.
#
8. Extendable
A common use case is using metatables to provide shared fields and methods across multiple instances. A typical example might be:
local person = {}
function person:name(newValue)
if newValue then
self._name = newValue
end
return self._name
end
function person.new(name)
local o = { _name = name }
return setmetatable(o, { __index = person })
end
local johnDoe = person.new("John Doe")
johnDoe:name() == "John Doe"
If we want to make the name
a property, we might try creating a bound property like this:
person.name = prop(function(self) return self._name end, function(value, self) self._name = value end):bind(person)
Unfortunately, this doesn't work as expected:
johnDoe:name() -- Throws an error because `person` is the owner, not `johnDoe`.
johnDoe.name() == nil -- Works, but will return `nil` because "John Doe" is applied to the new table, not `person`
The fix is to use prop.extend
when creating the new person. Rewrite person.new
like so:
person.new(name)
local o = { _name = name }
return prop.extend(o, person)
end
Now, this will work as expected:
johnDoe:name() == "John Doe"
johnDoe.name() == "John Doe"
The prop.extend
function will set the source
table as a metatable of the target
, as well as binding any bound props that are in the source
to target
.
#
Tables
Because tables are copied by reference rather than by value, changes made inside a table will not necessarily
trigger an update when setting a value with an updated table value. By default, tables are simply passed in
and out without modification. You can nominate for a property to make copies of tables (not userdata) when
getting or setting, which effectively isolates the value being stored from outside modification. This can be
done with the
local value = { a = 1, b = { c = 1 } }
local valueProp = prop.THIS(value)
local deepProp = prop.THIS(value):deepTable()
local shallowProp = prop.THIS(value):shallowTable()
-- print a message when the prop value is updated
valueProp:watch(function(v) print("value: a = " .. v.a ..", b.c = ".. v.b.c ) end)
deepProp:watch(function(v) print("deep: a = " .. v.a ..", b.c = ".. v.b.c ) end)
shallowProp:watch(function(v) print("shallow: a = " .. v.a ..", b.c = ".. v.b.c ) end)
-- change the original table:
value.a = 2
value.b.c = 2
valueProp().a == 2 -- modified
valueProp().b.c == 2 -- modified
shallowProp().a == 1 -- top level is copied
shallowProp().b.c == 2 -- child tables are referenced
deepProp().a == 1 -- top level is copied
deepProp().b.c == 1 -- child tables are copied as well
-- get the 'value' property
value = valueProp() -- returns the original value table
value.a = 3 -- updates the original value table `a` value
value.b.c = 3 -- updates the original `b` table's `c` value
valueProp(value) -- nothing is printed, since it's still the same table
valueProp().a == 3 -- still referencing the original table
valueProp().b.c == 3 -- the child is still referenced too
shallowProp().a == 1 -- still unmodified after the initial copy
shallowProp().b.c == 3 -- still updated, since `b` was copied by reference
deepProp().a == 1 -- still unmodified after initial copy
deepProp().b.c == 1 -- still unmodified after initial copy
-- get the 'deep copy' property
value = deepProp() -- returns a new table, with all child tables also copied.
value.a = 4 -- updates the new table's `a` value
value.b.c = 4 -- updates the new `b` table's `c` value
deepProp(value) -- prints "deep: a = 4, b.c = 4"
valueProp().a == 3 -- still referencing the original table
valueProp().b.c == 3 -- the child is still referenced too
shallowProp().a == 1 -- still unmodified after the initial copy
shallowProp().b.c == 3 -- still referencing the original `b` table.
deepProp().a == 4 -- updated to the new value
deepProp().b.c == 4 -- updated to the new value
-- get the 'shallow' property
value = shallowProp() -- returns a new table with top-level keys copied.
value.a = 5 -- updates the new table's `a` value
value.b.c = 5 -- updates the original `b` table's `c` value.
shallowProp(value) -- prints "shallow: a = 5, b.c = 5"
valueProp().a == 3 -- still referencing the original table
valueProp().b.c == 5 -- still referencing the original `b` table
shallowProp().a == 5 -- updated to the new value
shallowProp().b.c == 5 -- referencing the original `b` table, which was updated
deepProp().a == 4 -- unmodified after the last update
deepProp().b.c == 4 -- unmodified after the last update
So, a little bit tricky. The general rule of thumb is:
- If working with immutable objects, use the default
value
value copy, which preserves the original. - If working with an array of immutible objects, use the
shallow
table copy. - In most other cases, use a
deep
table copy.
#
API Overview
Constants - Useful values which cannot be changed
NIL
Functions - API calls offered directly by the extension
bind extend is
Constructors - API calls which return an object, typically one that offers API methods
AND FALSE FROM IMMUTABLE new NOT OR THIS TRUE
Fields - Variables which can only be accessed from an object returned by a constructor
mainWindow
Methods - API calls which can only be made on an object returned by a constructor
ABOVE AND ATLEAST ATMOST BELOW bind cached clear clone deepTable EQ get hasWatchers id IMMUTABLE IS ISNOT label mirror monitor mutable mutate NEQ NOT OR owner preWatch set shallowTable toggle toObservable unwatch update value watch wrap