-
Notifications
You must be signed in to change notification settings - Fork 5
Adjustable Algorithms
In TBD, adjustable algorithms are written using our library.
#Mod
Every value in the algorithm which may change as the result of changes to the input must be placed into a modifiable. To create a new modifiable, you must call the 'mod' library function:
def mod[T](initializer: => Changeable[T]): Mod[T]
'mod' takes a single parameter, a block that initializes the value of the mod, and then returns the modifiable. For those unfamiliar with Scala syntax, the '=>' before the type of the argument means that it will be used as a call-by-name parameter, rather than call-by-value.
This means that the parameter will not be evaluated until it is used by 'mod', which allows us to specify the parameter as a block while ensuring that 'mod' can do some necessary set-up work before the block is run.
The parameter to 'mod' has type 'Changeable', and the only way to create a Changeable is to call write:
write[T](value: T): Changeable[T]
Changeables do not have any interpretable value on their own. They simply ensure that the initializer passed to mod ends in a write. When write is called, the mod that is written into is the one created by the call to mod that is closest in enclosing scope to the write, which will also be the call the write's Changeable is returned to.
Thus, a typical creation of a mod would look like:
val mod1 = mod {
// some code
write(value1)
}
after which, 'mod1' would contain 'value1'.
#Read
To access the contents of a modifiable, you must call 'read' on it:
def read[T, U](mod: Mod[T])(reader: T => Changeable[U]): Changeable[U]
Read takes a modifiable and a reader, a function takes the value of the modifiable as a parameter and returns a Changeable. Since the value of the modifiable is only accessible inside the reader function, when the modifiable is updated TBD only has to reexecute the reader to update the computation.
Read returns a changeable so that the reader can write into a modifiable that is declared outside of the read. This allows the reader to return a value to the rest of the algorithm while propagating changes to any code that relies on the return value if it changes as the result of the reader being reexecuted.
Scala supports several different formats for specifying function values, and any may be used with read as the situation calls for. However, in general case statements are preferred, as this is an elegant way to specify the reader as a block while binding variable names. A typical call to read:
val mod2 = mod {
read(mod1) {
case null =>
// some code
write(someReturn)
case value =>
// some other code
write(someOtherReturn)
}
}
#Par
To allow adjustable algorithms to be parallel, the library has a 'par' function. Par takes two functions as parameters and evaluates them in parallel.
Each of the functions takes a 'Context' as a parameter - a Context allows the library functions to know which Worker they are executing on. Each of the library functions takes a Context as an implicit parameter (in Scala, an implicit parameter doesn't have to be specified in the parameter list as long as there is an implicit object of the correct type in scope).
par[T, U](left: Context => T, right: Context => T)
#Adjustable
To actually define an adjustable algorithm that TBD can run, you must extend the 'Adjustable' trait:
trait Adjustable[T] {
def run(implicit c: Context): T
}
For example, the following program takes a modifiable and outputs another modifiable that will always have a value twice that of the input value.
class Example(modA: Mod[Int]) extends Adjustable[Mod[Int]] {
def run(implicit c: Context) = {
mod {
read(modA) {
case value => write(value * 2)
}
}
}
}