As a Clojure learning exercise, I am porting Bulbs (), a graph-database library I wrote, from Python to Clojure.
One of the things I'm still somewhat fuzzy on is how to structure the library in a Clojure idiomatic way.
To support multiple databases, Bulbs uses dependency injection. The different database backends are abstracted away in a custom Client class that implements an interface, and the client is configured at runtime.
The Graph object and its various proxy objects hold an instance of the low-level Client object:
# bulbs/neo4jserver/graph.py
class Graph(object):
default_uri = NEO4J_URI
def __init__(self, config=None):
self.config = config or Config(self.default_uri)
self.client = Neo4jClient(self.config)
self.vertices = VertexProxy(Vertex, self.client)
self.edges = EdgeProxy(Edge, self.client)
You use Bulbs by creating a Graph object for the respective graph-database server:
>>> from bulbs.neo4jserver import Graph
>>> g = Graph()
And then you can create vertices and edges in the database via the proxy objects:
>>> james = g.vertices.create(name="James")
>>> julie = g.vertices.create(name="Julie")
>>> g.edges.create(james, "knows", julie)
This design makes it easy to use Bulbs from the REPL because all you have to do is import and instantiate the Graph object (or possibly pass in a custom Config object if needed).
But I'm not sure how to approach this design in Clojure since the Graph object and its proxies need to hold the Client object, which is configured at runtime.
What's the Clojure-way of doing this?
UPDATE: This is what I ended up doing...
;; bulbs/neo4jserver/client.clj
(def ^:dynamic *config* default-config)
(defn set-config!
[config]
(alter-var-root #'*config* (fn [_] (merge default-config config))))
(defn neo4j-client
[& [config]]
(set-config! config))
(neo4j-client {:root_uri ""})
(println *config*)
UPDATE 2:
Andrew Cooke pointed out that using a global var would preclude you from being able to use multiple, independent graph "instances" in your program, whereas you can in the Python version.
And so I came up with this:
(defn graph
[& [config]]
(let [config (get-config config)]
(fn [func & args]
(apply func config args))))
(defn create-vertex
[config data]
(let [path (build-path vertex-path)
params (remove-null-values data)]
(rest/post config path params)))
(defn gremlin
[config script & [params]]
(rest/post config gremlin-path {:script script :params params}))
And then you can call the different functions like this:
(def g (graph {:root_uri ""}))
(g create-vertex {:name "James"})
(g gremlin "g.v(id)" {:id 178})
Now I haven't delved into macros yet, and I'm not too sure of the merits of this approach compared to others so feedback welcome.
Protocols are well suited to this in Clojure, you define a protocol (a lot like an interface) that defines all the functions required to interface with a database, then at runtime you call the contructor of the graph protocol that builds in instance of the protocol that connects to your DB of choice.
the basic flow is very much the same, except using Clojure protocols.