In the last week I have been working on a Neo4j API in Scala, taking navigation 
in the graph as primary.

Just like the Enhanced API written in Java, the Scala API generalizes each 
element (Node, Relationship, RelationshipType, property name and property 
value) of the Neo4j database as being a Vertex. 

Before digging into the details of the Scala API, let's start with some example 
code.

    val name = Db(String("name"))
    val friend = Db(VertexOut("FRIEND"))
    val john = Db(NewVertex).put(name, "John")
    val pete = Db(NewVertex).put(name, "Pete").put(friend, john)

This piece of code defines the PropertyType "name" and the EdgeTypes "FRIEND", 
creates two vertices for the persons "John" and "Pete", and states that John is 
a friend of Pete.

In standard Neo4j API this could have been written as:

  Node john = db.createNode();
  Node pete = db.createNode();
  john.setProperty("name", "John");
  pete.setProperty("name", "Pete");
  pete.createRelationshipTo(john, DynamicRelationshipType.withName("FRIEND"));

Apart from an obvious style difference, there is one immediate difference 
noticeable between the two API's.

In the Neo4j API it is possible to write:

  john.setProperty("name", "John");
  pete.setProperty("name", 99);

While the following Scala program won't typecheck:

  pete.put(name, 99) //ERROR

The "name" property is defined as a String and the API enforces that type. This 
also applies when fetching a property value. In the Neo4j API we write:

  john.getProperty("name")  // returns java.lang.Object

In the Scala API we write:

  john(name)   // returns java.lang.String

It is also possible to ask the names of pete's friend as follows:

  pete(friend andThen name)

This is equal to the Neo4j call:

  
(String)pete.getSingleRelationship(DynamicRelationshipType.withName("FRIEND"), 
Direction.OUTGOING).endNode.getProperty("name")

If pete has more than one friend, we have to define a different key to fetch 
them ( the VertexOut key refers to one single relationship, either the one to 
be created, or a singular exisiting relationship ):

  val friends = Db(Vertices("FRIEND"))

We now have a key to all "FRIEND" relationships so we can ask:

  pete(friends andThen name)

This returns an Iterable[String] with the names of all Pete's friends.

We can even do:

  pete(Rec(friends) andThen name)

This returns an Iterable[String] with the names of all Pete's friends of 
friends (to the n-th degree). The Rec object recursively applies "friend" to 
all vertices it traverses, remembering already taken paths, traversed 
Relationships or traversed Nodes (settings are optional with sensible defaults).

We can also write:

  pete(Rec(friend, 2) andThen name)

This returns an Iterable[String] with the names of all Pete's friends of 
friends (to the 2nd degree)

It is even possible to write:

  pete(Rec(friend andThen friend) andThen name)

This returns an Iterable[String] with the names of all Pete's friends of 
friends (to the n-th degree where n is even)

Instead of having get methods for properties and relationships and traversal 
methods on nodes, the Scala API uses one calling pattern for all database 
related objects:

object(traverser)

So a call like Db(String("name")) is not just a call on the database to return 
a PropertyType with name "name" and datatype String, it is a traversal from the 
database to that PropertyType. What is being returned with that call is a 
traverser itself. 

Traversers can be composed with "andThen", so the output of one traverser is 
used as input for the next traverser. 

All traversers are typed, so the "andThen" connective can only be applied when 
the type of the output of the left-hand-side traverser is equal to the type as 
the input of the right-hand-side traverser. This is checked at compile time.

Traversals not only work on Vertex objects and it's subtypes (Property, 
PropertyType, Edge, EdgeType...), it also works on Iterable[Vertex]. 

Instead of fetching just pete's friends, as in:

pete(friends)

we can also fetch the friends of pete and john:

val frnds = List(pete, john)
frnds(friends)

or if we don't need the "frnds" object later on, we simply state:

List(pete, john)(friends)

and if we want the names of those friends:

List(pete, john)(friends andThen name)

it is even possible to set properties or create relationships on 
Iterable[Vertex]

val age = Db(Int("age"))
val nationality = Db(String("nationality"))
List(pete, john).put(age, 40).put(nationality, "Irish")

This sets the "age" property to 40 on both "pete" and "john".

It is also possible to write this as a traversal:

List(pete, john)(Put(age, 40) andThen Put(nationality, "Irish"))

All traversers are function objects, so they can both be called as a function 
and can be treated as an object. This makes it possible to create traverers 
programmatically, allowing for the storage of traversers in the database, and 
many more nifty tricks.

Using the Put object, we could for example create a list of such 
actions/traversals and perform a validation on the list before actually calling 
the put-functions.

There is much more to say about the API, for example the support of n-ary 
edges, but I will leave it for now. I think this approach to traversals and the 
fusion of Vertex and Iterable[Vertex] is enough for now.

Niels



                                          
_______________________________________________
Neo4j mailing list
[email protected]
https://lists.neo4j.org/mailman/listinfo/user

Reply via email to