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