Skip to main content

Quick Start

Welcome to Zef! This Quick Start is designed to give you a taste of the major ideas and concepts of Zef. If you haven't already, please go through the [[ZefDoc - Glossary]] to get an overview of some common terms used in Zef.

Start at the top and work your way down!

Creating a new database (DB)

# imports all the Zef tools you need  
from zef import *
from zef.ops import *

# creates a new empty database
db = DB()

We plan on introducing user specific configs in the near future which will clean import *.

For now, the imports unlock user friendly syntax and powerful Zef operators (ZefOps) which will be highlighted below.

Adding data to the graph

The Zef graph data model is completely flat. There's no properties or data nested inside nodes.

The fundamental parts of any graph are instances of

  • Entities
  • Relations
  • Attribute Entities (AEs), nodes to which values can be assigned over time

In Zef, "|" pipe is overloaded so users can chain together values, Zef operators (ZefOps), and functions in sequential, lazy, and executable pipelines where data flow is left to right.

p1 = ET.Person | g | run                    # adds a new entity ET.Person to the graph  

(p1, RT.FirstName, "Yolandi") | g | run # adds "fields" (source, relation, target) to the graph

Above, ET.Person defines the entity type "Person", just as RT.FirstName
defines the relation type "FirstName". These are not predefined types - you can
use some different names like RT.MyFavoriteRelation.

Exploring the graph

yo is a ZefOp that can be used in different contexts for useful information outputted in ASCII. You can think of yo as the Python help designed for Zef. When used on a graph it outputs a summary, on an instance an overview, and on a ZefOp a docstring.

All Zef pipeline expressions are lazy by default. collect is a ZefOp used at the end of a Zef pipeline that makes it eager and returns a value. Without collect, the expression is just data.

g | yo | collect            # outputs a detailed summary of the graph  

g | now | yo | collect # outputs a summary of the latest time slice of the graph

now is a ZefOp that takes users to the latest time slice.

p1 | yo | collect           # outputs an overview of a prior time slice of the specified node  

p1 | now | yo | collect # outputs an overview of the latest time slice of the specified node

You will notice that p1 | now shows RT.FirstName as you're viewing the latest time slice which includes all actions that have occurred up to this point.

Traversing the graph

Graph traversals can also be done using ZefOps.

name_rel = p1 | now | out_rel[RT.FirstName]     # steps onto the edge  
name_rel | yo | collect # outputs overview of the edge

name_aet = p1 | now | Out[RT.FirstName] # steps onto the target
name_aet | yo | collect # outputs overview of the target

The below ZefOps are used for traversals when there is exactly one connected RAE.

out_rel traverses to an outgoing edge. in_rel traverses to an incoming edge.

Out traverses to the target of an edge. In traverses to the source of an edge.

These four ZefOps also exist in plural form: out_rels, in_rels, Outs, and Ins. These plural forms return a list of zero or more connected RAEs.

Retrieving values

value is a ZefOp that retrieves the value on an AET node with respect to its time slice.

current_name: str = name_aet | value | collect  
print(current_name)

Triggering computation

run is a ZefOp that triggers computation of a Zef pipeline (which is lazy by default) and runs the pipeline output through a function.
If the pipeline output is an Effect, there is an implicit execute curried in that executes the Effect.
Unlike collect, run does not return the output of the pipeline but runs it.

current_name: str = name_aet | value | collect  
print(current_name)

name_aet | value | run[print]

Persisting graphs

Persisting graphs requires a free ZefHub account. See the [[ZefDoc - Installing Zef]] on getting your ZefHub account.

# This persists, syncs, and distributes all future changes via ZefHub in real-time  
g | sync[True] | run

Once a Zef graph is synced, all subsequent changes will be automatically persisted, synchronized, and distributed via ZefHub in real-time.

Sharing graphs

We can also share graphs that are synced with ZefHub easily with others.

# ---------------- Python Session A (You) -----------------  
g | uid | to_clipboard | run # every graph has an auto-generated uid
# or
g | tag['share-stories'] | run # a user-specified tag can be assigned to a graph

"your-friends-email" | grant[KW.view][g] | run # grants view access to our friend

to_clipboard is a Zefop that returns an Effect that copies data to our local clipboard.

Our friend can access our graph with the shared uid or tag (you can mimic this by opening a separate Python shell).

# ---------------- Python Session B (Friend) -----------------  
graph_uid = '...' # copied from Slack/WhatsApp/email
g = Graph(graph_uid) # retrieve the graph using uid
# or
graph_tag = '<your-user-name>/share-stories'
g = Graph(graph_tag) # retrieve the graph using a tag

g | now | all[ET] | collect # let's see all entities in the latest time slice

Our friend wants to add data to our graph as well.

p2 = ET.Person | g | run                   # Oh no, this returns: Error('user has no append privileges')!  

We previously only granted view access. We can grant append access to our friend, along with read-access for everyone.

"your-friends-email" | grant[KW.append][g] | run        # grants append access to our friend  
"group:everyone" | grant[KW.view][g] | run # grants view access to all ZefHub users
"group:everyone" | grant[KW.discover][g] | run # grants ability for all ZefHub users to search and discover

Collaborating and live updates

Let's subscribe to any changes that happen.

# ---------------- Python Session A (You) -----------------  
g | on[Instantiated[ET]] | subscribe[print] # we want to be informed any time an ET is added

Our friend can make multiple changes to the graph in one transaction.

# ---------------- Python Session B (Friend) -----------------  
p1 = (g
| now
| all[ET.Person]
| single # there is only one ET.Person on the graph so far
| collect
)


actions = [
(ET.Person['p2'], RT.Name, "Ninja"), # 'p2' is an internal id / name
(p1, RT.FriendOf, Z['p2']), # Z is used to refer to entities by internal name
]
actions | transact[g] | run # convert the actions to a transact Effect and execute

Since we hooked up a subscription, a message will now show up in Session A.

Making changes and exploring past values

A new value can be assigned.

p1_name = p1 | Out[RT.FirstName] | collect  
p1_name | assign_value["Anri"] | g | run # Atomic entities can have values reassigned

The complete versioned history can be viewed.

p1_name | now | yo | collect                          # explore all history until latest time slice  

Time traveling

Zef uses an immutable data structure, which means full versioning comes out-of-the-box.

We can access any previous time slice with ease.

# ---------------- Python Session B (Friend) -----------------  
p1_past_absolute = p1 | time_travel[Time('2021 December 4 15:31:00 (+0100)')] | collect # go to an absolute time slice
p1_past_relative = p1 | time_travel[-2] | collect # go to a relative time slice