One of the design choices behind tawny, was to build a flexible and comfortable syntax for writing OWL ontologies, using a full programming language, as opposed to writing an OWL syntax and supplementing it with programming features. This makes the tawny syntax both fully extensible and arbitrarily scriptable. In this document, we consider these advantages.
Scripting is not that complex but it does require a reasonable knowledge of either lisp or Clojure to use beyond a trivial level. We assume that knowledge here.
As described previously, tawny can be used purely as an API -- adding and removing entities from the ontology. In addition to this, it provides a set of macros which add vars to the local namespace. For scripting, it is important to understand the distinction between these two. In general, it is a little easier to script without creating vars, and if it is not necessary to have these vars, it is the best way to do so. However, this will have implications for ontology builders using the classes created in this way; in particular, they will not be able to refer to them as symbols, and tools such as auto-complete, or native documentation lookup will not work. We have, however, added relatively easy features to using vars, so that you are not forced into using macros and manipulating symbols which is a painful way to do things.
The most common requirement is to simply create multiple new concepts at once. This is relatively simply to do in Clojure is to use a list comprehension. Here is a simple example from the pizza ontology. This also adds a standard prefix for us, which saves a bit of typing.
(doseq
[n ["Carrot"
"CherryTomatoes"
"KalamataOlives"
"Lettuce"
"Peas"]]
(owlclass (str n "Topping") :super VegetableTopping))
This version does not create vars, so attempts afterwards to use
CarrotTopping
will fail. It is relatively easy to add vars also, using a
intern-entity
from tawny.read
.
(doseq
[n
["ChilliOil"
"Chives"
"Chutney"
"Coriander"
"Cumin"
]]
(tawny.read/intern-entity
(owlclass (str n "Topping") :super VegetableTopping)))
This achieves the same thing, but also adds vars. The same thing can be done
with properties, by using the objectproperty
function. So far, we have never
needed to do this, as most ontologies are class heavy, rather than property.
Clojure programmers might ask, why not just use clojure.core/intern
; to
which the answer is, it's nearly the same, but not quite because it adds some
metadata to the symbol, and runs some checks. Check the code if you want!
One of the really interesting things about Clojure is it's support for laziness. If you are not interested, please skip to the last paragraph of this section, which gives some easy rules to follow. If you are interested in why the rules are there, read on.
It is possible to express infinitely long sequences for instance; but,
unfortunately, this breaks everything when using Java objects which is how
tawny works. So, the obvious approach of using map
fails. For instance,
adding this to your file
(map
#(owlclass %)
["a" "b" "c" "d"])
will achieve nothing. This is because map
is a lazy list. Alternative
consider this (slightly elided) REPL session.
(test-ontology)
;; #<OWLOntologyImpl [Axioms: 0 Logical Axioms: 0]>
(def x (map #(owlclass %) ["a" "b" "c"]))
;; #'user/x
(get-current-ontology)
;; #<OWLOntologyImpl [Axioms: 0 Logical Axioms: 0]>
(map #(owlclass %) ["a" "b" "c"])
;; (#<OWLClassImpl a> #<OWLClassImpl b> #<OWLClassImpl c>)
(get-current-ontology)
;; #<OWLOntologyImpl [Axioms: 3 Logical Axioms: 0]>
Our first attempt to use map
produces no effects at all. Again, the value
placed in the variable x
is lazy, and until the list is evaluated nothing
happens. The value returned by the second map
is automatically evaluated,
but only because we are using the REPL. You can see the same thing in this
code; after evaluating x
at the REPL, it all works.
(test-ontology)
;; #<OWLOntologyImpl [Axioms: 0 Logical Axioms: 0]>
(def x (map #(owlclass %) ["a" "b" "c"]))
;; #'user/x
(get-current-ontology)
;; #<OWLOntologyImpl [Axioms: 0 Logical Axioms: 0]>
x
;; (#<OWLClassImpl a> #<OWLClassImpl b> #<OWLClassImpl c>)
(get-current-ontology)
;; #<OWLOntologyImpl [Axioms: 3 Logical Axioms: 0]>
The practical upshot of all this is, while doseq
has a slightly more complex
syntax than map
, it's better to use this. If you use map
or any of
Clojure's lazy constructs, you need to use doall
regularly.
It is possible to arbitrarily extend tawny.owl
, to add new features and
functionality, to suite specific workflows. For example, we have done this
with obo.clj
to add specialise support for identifiers. This form of
extension is relative complex, requiring knowledge of both clojure, and the
OWL API.
However, not all extension are complex. The most obvious example is to
essentially add new syntax. For consider this annotation property from
tawny.upper
.
(defannotationproperty Scope
:super owlcommentproperty
:comment "The scope provides context to the definition and describes that
parts of the domain which are (or are not) intended to be described by definition.")
(def scope
(partial tawny.owl/annotation Scope))
By adding function scope
, we can easy the use of the Scope
property,
reducing the necessity for the direct use of the property.
(defclass A
:annotation (annotation Scope "An long-off test class"))
(defclass B
:annotation (scope "A one-off test class"))
This kind of extension is minor, but the code is easy to write and, critically, can be included in the same file as the ontology. In this way, an tawny file can become an amalgam of ontology and functions, seamlessly integrated.
This form of extensibility is also used in tawny.pizza
where a new function
is created to describe a pizza.
(defn generate-named-pizza [& pizzalist]
(doseq
[[named & toppings] pizzalist]
(owl-class
named
:super (some-only hasTopping pizzalist))))
(generate-named-pizza
[CapricciosaPizza AnchoviesTopping MozzarellaTopping
TomatoTopping PeperonataTopping HamTopping CaperTopping
OliveTopping]
which expands to this definition in Manchester syntax.
Class: piz:CapricciosaPizza
SubClassOf:
piz:hasTopping some piz:CaperTopping,
piz:hasTopping some piz:MozzarellaTopping,
piz:hasTopping only
(piz:AnchoviesTopping
or piz:CaperTopping
or piz:HamTopping
or piz:MozzarellaTopping
or piz:OliveTopping
or piz:PeperonataTopping
or piz:TomatoTopping),
piz:hasTopping some piz:OliveTopping,
piz:hasTopping some piz:HamTopping,
piz:NamedPizza,
piz:hasTopping some piz:PeperonataTopping,
piz:hasTopping some piz:AnchoviesTopping,
piz:hasTopping some piz:TomatoTopping
In this case, this saves quite a considerable amount of typing, and also
reducing the risk of incidentally errors, as well as leaving a clearer and
declarative statement of the intention for the use of the NamedPizza
concept.
It is possible to build entities up incrementally; this is often very useful
where forward references are needed. Consider this somewhat elided example
from the tawny.pizza
(defclass Pizza)
(defoproperty hasTopping
:domain Pizza)
(defoproperty hasBase
:domain Pizza)
(owlclass Pizza
:super
(owlsome hasTopping PizzaTopping)
(owlsome hasBase PizzaBase))
In this case, we define the Pizza
class and place the OWLObject into the
Pizza
var. We use this value to define two properties hasTopping
and
hasBase
. Then we extend the definition of Pizza
to use these two
references; in this case, we use owlclass
rather than defclass
as we have
already created a class in the Pizza
var; in practice, we could also use
defclass
, which would redefine the var. Either should operate in the same
way.
Tawny also provides a refine
function which can be used to update an owl
object regardless of its type. This can be useful, for example, for adding
annotation. Consider this example from the pizzaontology
.
(doseq
[e (.getSignature pizzaontology)
:while
(and (named-object? e)
(.startsWith
(.toString (.getIRI e))
"http://www.ncl.ac.uk/pizza"))]
(try
(println "refining:" e)
(refine e :annotation (annotation creator "Phillip Lord"))
(catch Exception exp
(printf "Problem with entity: %s :%s\n" e exp)
)))
The first half just filter the relevant entities from the ontology, while the
refine
form adds an annotation; without refine
, a type check would be
necessary and the relevant function called.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close