Walkers
The concept of a walker is one of the new programming language abstractions introduced by Jaseci and the
Jack Language. In a nutshell, a walker is a unit of execution that retains state (its context scope, i.e.,
has variables) as it travels over a graphs. Walkers walk from node to node in the graph and executing its body.
The walker’s body is specified with an opening and closing braces ( {
}
) and is executed to
completion on each node it lands on. In this sense a walker iterates while spooling through a
sequence of nodes that it ‘takes’ using the take keyword. We call each of these iterations
node-bound iterations.
Variables in a walker's body are divided into two categories: context variables, which retain their values as the walker moves through the graph, and local variables, which are reinitialized for each node-bound iteration.
Note
Walkers are initialized with default context variables on creation. Has variables only clear on destruction and can be overwritten with calls to
walker_prime
. The intuition here is walkers simply keep state until they are destroyed.
Walkers offer a different approach to programmatic execution, distinct from the common function-based model used in other languages. Instead of a function's scope being temporarily pushed onto a growing stack as functions call other functions, scopes in Jaseci can be laid out spatially on the graph and walkers can traverse the graph, carrying their scope with them. This new model introduces data-spatial problem solving, where walkers can access any scope at any time in a modular manner, unlike in the function-based model where scopes become inaccessible after a function is called until it returns.
When solving problems with walkers, a developer can think of that walker as a little self-contained robot or agent that can retain context as it spatially moves about a graph, interacting with the context in nodes and edges of that graph.
Init Walker with Examples
When we run a jac code, by default it's executing the init
walker. Basically the walker init
works as the main method in other programming language. save following code as main.jac
and run the code in jsctl
shell with jac run main.jac
walker init{
std.out("This is from init walker \n");
}
Expected Output:
This is from init walker
As you can see, this code has executed the init
walker. Now let's create another walker;
walker second_walker{
std.out("This is from second walker \n");
}
walker init{
std.out("This is from init walker");
root{
spawn here walker::second_walker;
}
}
Expected Output:
This is from init walker
This is from second walker
The statements from second walker
and init
are printed in the jac shell, and we may run just second_walker
directly by using the command jac run main.jac -walk second_walker
. Here, the -walk
parameter instructs the jsctl
to execute a certain walker.
Walkers Navigating Graphs Examples
As mentioned earlier the walkers can traverse(walk) through the nodes of the graph in breadth first search (BFS) or depth first search(DFS) approaches.
Note
BFS is a traversal approach in which begins from root node and walk through all nodes on the same level before moving on to the next level. DFS is also a traversal approach in which the traverse begins at the root node and proceeds through the nodes as far as possible until we reach the node with no unvisited nearby nodes.
We are creating the following graph to demonstrate traversing of walkers in the coming sections;
Jaseci introduces the handy command called "take" to instruct walker to navigate through nodes. See how that works in following example;
node plain: has number;
## defining the graph
graph example {
has anchor head;
spawn {
n=[];
for i=0 to i<7 by i+=1 {
n.l::append(spawn node::plain(number=i+1));
}
n[0] ++> n[1] ++> n[2];
n[1] ++> n[3];
n[0] ++> n[4] ++> n[5];
n[4] ++> n[6];
head=n[0];
}
}
#init walker traversing
walker init {
root {
start = spawn here ++> graph::example;
take-->;
}
plain {
std.out(here.number);
take-->;
}
}
Expected Output:
1
2
5
3
4
6
7
take
command lets the walker traverse through graph nodes. You may notice by default, a walker traverse with take
command using the breadth first search approach. But the take
command is flexible hence you can indicate whether the take command should use a depth first or a breadth first traversal to navigate. Look at the following example; More information about take
command and keywords to operate walkers can be found here
In addition to the introduction of the take
command to support new types of control flow for node-bound iterations. The keywords and semantics of disengage
, skip
, and ignore
are also introduced. These instruct walkers to stop walking the graph, skip over a node for execution, and ignore certain paths of the graph. More information about these can be found in here
Walker Spawning Examples
Jaseci walkers act like little robots traversing graphs, with a unique ability to spawn other walkers that can also walk the graph and return a value to the parent walker. This powerful feature is achieved by specifying the variable to receive the returned value using the has anchor some_variable
syntax.
Here's a simple example of how to use walker spawning in Jaseci:
walker parent {
has result;
result = spawn here walker::child;
std.out("Child walker returned: ", result);
}
walker child {
has anchor return_value;
return_value = "Hello, I am the child walker!";
}
In this example, the parent walker spawns the child walker and sets the return_value anchor to a string. The parent walker then assigns its result variable to the value returned by the child walker, and finally outputs the returned value using std.out.
With this feature, you can easily create dynamic traversal patterns that adapt to changing data and requirements, making Jaseci a powerful tool for developing complex applications.
Walker Callback
Walker callback is used for running a walker to a specific node using public key
instead of authorization token.
Use Case
Generating public URL that can be used as callback API for 3rd party Webhook API. You may also use this as a public endpoint just to run a specific walker to a specific node.
Structure
POST /js_public/walker_callback/{node uuid}
/{spawned walker uuid}
?key={public key}
Steps to Generate
1. Jac Code
walker sample_walker: anyone {
has fieldOne;
with entry {
report 1;
}
}
2. Register Sentinel
curl --request POST \
--url http://localhost:8000/js/sentinel_register \
--header 'Authorization: token {yourToken}' \
--header 'Content-Type: application/json' \
--data '{ "name": "sentinel1", "code": "walker sample_walker: anyone {\r\n\thas fieldOne;\r\n\twith entry {\r\n\t\treport 1;\r\n\t}\r\n}" }'
// RESPONSE
[
{
"version": "3.5.7",
"name": "zsb",
"kind": "generic",
"jid": "urn:uuid:b4786c7a-cf24-49a4-8c2c-755c75a35043",
"j_timestamp": "2022-05-11T05:57:07.849673",
"j_type": "sentinel"
}
]
3. Spawn Public Walker (sample_walker)
curl --request POST \
--url http://localhost:8000/js/walker_spawn_create \
--header 'Authorization: token {yourToken}' \
--header 'Content-Type: application/json' \
--data '{ "name": "sample_walker", "snt":"active:sentinel" }'
// RESPONSE
{
"context": {},
"anchor": null,
"name": "sample_walker",
"kind": "walker",
// this is the spawned walker uuid to be used
"jid": "urn:uuid:2cf6d0dc-e7eb-4fc8-8564-1bbdb48baad3",
"j_timestamp": "2022-06-07T09:45:22.101017",
"j_type": "walker"
}
4. Getting Public Key
curl --request POST \
--url http://localhost:8000/js/walker_get \
--header 'Authorization: token {yourToken}' \
--header 'Content-Type: application/json' \
--data '{ "mode": "keys", "wlk": "spawned:walker:sample_walker", "detailed": false }'
// RESPONSE
{
// this is the public key used for walker callback
"anyone": "97ca941e6bf1f43c3a4e531e40b2ad5a"
}
5. Construct the URL
Assuming there's a node with uuid of aa1bb26e-238b-40a0-8e39-333ec363ace7
this endpoint can now be accessible by anyone
POST /js_public/walker_callback/
aa1bb26e-238b-40a0-8e39-333ec363ace7
/2cf6d0dc-e7eb-4fc8-8564-1bbdb48baad3
?key=97ca941e6bf1f43c3a4e531e40b2ad5a