Aleksandar Prokopec / @alexprokopec
A planning model in which an AI agent decides on the next action by traversing a special data structure called a behavior tree.
Every behavior tree is composed of two node types:
task nodes and control nodes.
They are the leaf nodes of a behavior tree.
They execute a side-effect, and return running, success or failure status.
They are the leaf nodes of a behavior tree.
They execute a side-effect, and return running, success or failure status.
They are the leaf nodes of a behavior tree.
They execute a side-effect, and return running, success or failure status.
Control nodes are inner nodes that bind subtrees together.
Given a set of subtrees and their return values, a control node decides whether to run some subtree, or return a success or failure status.
Two basic control nodes: sequence and selector.
The main reason behind their popularity.
Different subtrees can be developed, tuned and tested independently, and then merged into larger functional units.
The basic model can be extended with nodes such as
repeaters, randomizers or inverters.
<?xml version="1.0" encoding="UTF-8"?>
<agent>
<selector>
<sequence name="capture-the-flag>
<task name="move"></task>
<task name="take-the-flag"></task>
</sequence>
<module name="defend-the-base">
</module>
</selector>
</agent>
Behavior tree planner is nothing more than an AST interpreter.
AI researchers have been rediscovering the wheel.
def agent() = {
captureTheFlag() || defendTheBase()
}
def captureTheFlag(): Boolean = {
move() && takeTheFlag()
}
def agent() = {
captureTheFlag() || defendTheBase()
}
def captureTheFlag(): Boolean = {
move() && takeTheFlag()
}
Most languages cannot suspend computation and resume it later.
A programming construct that allows suspending the computation (i.e. yielding), and resuming it later from the point where it was suspended.
def captureTheFlag(): Boolean = {
yield(move()) && takeTheFlag()
}
val double = (x: Int) => x + x
double(7)
val double = (x: Int) => x + x
double(7)
val double = (x: Int) => 7 + 7
double(7)
val double = (x: Int) => 14
double(7)
val double = ...
14
A function invocation is an entity that exists during program execution.
However, the callsite cannot observe the existence of that entity.
val double =
(x: Int) => x + x
val double = coroutine {
(x: Int) => x + x
}
This coroutine does not yield.
The yieldval
construct suspends computation
and yields a value to the caller.
val double = coroutine { (x: Int) =>
yieldval(x)
yieldval(x)
}
Coroutine invocation is resumed by the caller.
Therefore, it is an observable entity in the program.
val double = coroutine { (x: Int) =>
yieldval(x)
yieldval(x)
}
val i = call(double(7))
A coroutine invocation can be observed by the callsite.
Therefore, it must be a first-class object.
val double = coroutine { (x: Int) =>
yieldval(x)
yieldval(x)
}
val i = call(double(7))
var sum = 0
while (i.resume) { sum += i.value }
val double = coroutine { (x: Int) =>
↑yieldval(x)
yieldval(x)
}
val i = call(double(7))↑
var sum = 0
while (i.resume) { sum += i.value }
val double = coroutine { (x: Int) =>
↑yieldval(x)
yieldval(x)
}
val i = call(double(7))
var sum = 0
while (i.resume↑) { sum += i.value }
val double = coroutine { (x: Int) =>
yieldval(x)↑
yieldval(x)
}
val i = call(double(7))
var sum = 0
while (i.resume↑) { sum += i.value }
val double = coroutine { (x: Int) =>
yieldval(x)↑
yieldval(x)
}
val i = call(double(7))
var sum = 0
while (i.resume) { sum += i.value↑ }
val double = coroutine { (x: Int) =>
yieldval(x)↑
yieldval(x)
}
val i = call(double(7))
var sum = 0
while (i.resume↑) { sum += i.value }
val double = coroutine { (x: Int) =>
yieldval(x)
yieldval(x)↑
}
val i = call(double(7))
var sum = 0
while (i.resume↑) { sum += i.value }
val double = coroutine { (x: Int) =>
yieldval(x)
yieldval(x)↑
}
val i = call(double(7))
var sum = 0
while (i.resume) { sum += i.value↑ }
val double = coroutine { (x: Int) =>
yieldval(x)
yieldval(x)↑
}
val i = call(double(7))
var sum = 0
while (i.resume↑) { sum += i.value }
val double = coroutine { (x: Int) =>
yieldval(x)
yieldval(x)
}↑
val i = call(double(7))
var sum = 0
while (i.resume↑) { sum += i.value }
val double = coroutine { (x: Int) =>
yieldval(x)
yieldval(x)
}↑
val i = call(double(7))
var sum = 0
while (i.resume) { sum += i.value }
↑
call
- creates a coroutine instanceresume
- resumes a coroutine instancevalue
- obtains the last yielded valueresult
- obtains the result of the invocation
val double =
coroutine { (x: Int) =>
yieldval(x)
yieldval(x)
}
val double: Int ~> (Int, Unit) =
coroutine { (x: Int) =>
yieldval(x)
yieldval(x)
}
val i: Int <~> Unit =
call(double(7))
As coroutine definitions grow larger,
so does the need to decompose them into independent components.
A closed-addressing hash table is an array containing buckets.
A bucket is a list of elements.
val array: Array[List[T]]
Assume that we know how to traverse a list of elements.
val bucket =
coroutine { (b: List[T]) =>
var cur = b
while (cur != Nil) {
yieldval(cur.head)
cur = cur.tail
}
}
Then, we should be able to use that to traverse an array of lists.
val table =
coroutine { (t: Array[List[T]]) =>
var i = 0
while (i < t.length) {
bucket(t(i))
i += 1
}
}
The direct call reuses the stack of the same coroutine instance.
How do Scala coroutines generalize other models?
def iterator(tree: Tree) =
call(foreach(tree))
val it = tree.iterator
while (it.resume)
println(it.value)
Iterators follow directly from foreach
definitions.
val foreach =
(t: Tree[T], f: T => Unit) =>
if (t != null)
foreach(t.left, f)
f(t.elem)
foreach(t.right, f)
}
Iterators follow directly from foreach
definitions.
val foreach =
coroutine { (t: Tree[T]) =>
if (t != null)
foreach(t.left)
yieldval(t.elem)
foreach(t.right)
}
}
def loginRequest(): Future[String]
def httpRequest(c: String): Future[Page]
async {
val credential =
await { loginRequest() }
val ui =
await { httpRequest(credential) }
ui.html
}
How do we define async
and await
using coroutines?
def await[R]: Future[R] ~> (Future[R], R) =
coroutine { (f: Future[R]) =>
yieldval(f)
f.value.get.get
}
def async[R](b: () ~> (Future[Any], R)) = {
val i = call(b())
val p = Promise[R]()
@tailrec def loop(): Unit =
if (i.resume)
i.value.onSuccess(loop)
else p.success(i.result)
Future { loop() }
p.future
}
An actor must declare all receive operations
in terms of a top-level receive
.
class Printer extends Actor {
def receive = {
case x: Int => println(x)
}
}
A reactor uses first-class events sources,
and receives by calling onEvent
.
class Printer extends Reactor[Int] {
main.events.onEvent { x =>
println(x)
}
}
A reactor uses first-class events sources,
and receives by calling onEvent
.
class Adder extends Reactor[Int] {
operands.once.onEvent { x =>
operands.once.onEvent { y =>
println(x + y)
}
}
}
This soon results in the pyramid of doom.
Instead, we want to write code without nesting.
class Adder extends Reactor[Int] {
val x = operands.get()
val y = operands.get()
println(x + y)
}
Challenge: implement methods react
and get
,
that define a reactor, and extract an event from its event source.
type Obs = (() => Unit) => Unit
def get: () ~> (Obs, T) =
coroutine { () =>
var ret: T = _
val obs = (cont: () => Unit) =>
onEvent(x => { ret = x; cont() })
yieldval(obs)
ret
}
type Obs = (() => Unit) => Unit
def react[T](c: () ~> (Obs, Unit)) =
Reactor[T] {
val i = call(c())
def loop() =
if (i.resume) i.value(loop)
loop()
}
ScalaCheck tests typically used generators to explore the input space.
val tuples: Gen[(Int, Int)] =
for {
a <- choose(0, Int.MaxValue)
b <- choose(0, a)
} yield (a, b)
property("comm") = forAll(tuples) {
a + b == b + a
}
More intuitive: backtracking without inversion of control.
property("comm") = {
val a = choose(0 until Int.MaxValue)
val b = choose(0 until a)
a + b == b + a
}
type Program = Seq[() => Unit] <~> Unit
val choose:
Seq[Int] ~> (Seq[() => Unit], Int) =
coroutine { (vals: Seq[Int]) =>
var res: Int = _
yieldval(vals.map(x => () => res = x))
res
}
type Program = Seq[() => Unit] <~> Unit
val backtrack: Program ~> (Unit, Unit) =
coroutine { (p: Program) =>
if (p.resume) {
for (prepare <- p.value) {
prepare()
backtrack(p.snapshot )
}
} else yieldval(())
}
type Test = () ~> (Seq[() => Unit], Unit)
val forever =
coroutine { (test: Test) =>
while (true) {
val p = call(test())
backtrack(p)
}
}
def property(t: Test) = {
val i = call(forever(t))
for (i <- 0 until MAX_TESTS) i.resume
}
property {
val a = choose(0 until Int.MaxValue)
val b = choose(0 until Int.MaxValue)
a + b == b + a
}
type Shift = (() => Unit) => Unit
def reset(b: () ~> (Shift, Unit)) = {
def continue(i: Shift <~> Unit) =
if (i.resume)
i.value(() => continue(i.snapshot))
continue(call(b()))
}
def shift: Shift ~> (Shift, Unit) =
coroutine { (b: Shift) =>
yieldval(b)
}
type CoroutineAPI =
CallbackStyleAPI => DirectStyleAPI
implicit val aBitOfWork =
implicitly[TwentyLinesOfCode]