Early steps in Elixir and RDF
Using RDF.ex to work with RDF vocabularies in Elixir
I’ve recently begun to look at Elixir again. This time though in a little more detail. I had been aware of this elegant new language some years back when it was still shiny and new but then other things intervened and it all got pushed back. And Erlang too I had earlier played with and was also very struck with. But now seemed like a pretty good time to take up this exploring again and to get a better handle on the language and see what it could actually do for me.
I won’t say too much about Elixir itself other than to list its well-known characteristics as a programming language: functional, concurrent, fault tolerant. Well, at least that’s the Erlang inheritance anyways. (And Erlang’s been around for at least as long as the Web, and by some measures predates Java even.) But Elixir is more than just a passenger riding on top of the Erlang VM. First off, it’s a real joy to use. It has a clean, modern syntax, a meta-programming capability, rich documentation with doctesting built right in, consistent style guidelines, and a mature toolchain (including code formatter).
“Diving into the Elixir ecosystem is like unboxing something from Apple.”
– Ian M. Asaff
So in all, Elixir is a relatively new language (version 1.0 was released just four years ago in September 2014) and libraries are still being built for it. Of course, I had to look around to see if there was any hint at all of a semantic library and was really excited to find the RDF.ex
package from Marcel Otto which has pretty comprehensive support for RDF – and he’s recently also extended this support to querying with SPARQL.ex
too. (And a special thanks to Marcel for reviewing this post, answering all my simple-minded questions and making valuable suggestions for improvements.)
Now, I’m still learning Elixir and trying to get my head around the functional paradigm as well as understanding OTP and distributed computing. But I thought anyway I could take RDF.ex
out for a spin and see what it could do. I won’t give any detailed introduction to RDF.ex
here but will focus here on one particular aspect – the support for RDF vocabularies. The distribution ships with five vocabularies (RDF
, RDFS
, OWL
, SKOS
, and XSD
) already included. But it should be pretty simple to add in some new vocabularies, for example DC
and BIBO
. Let’s see how we might do this.
1. Create a ‘TestVocab’ project
First off, let’s create a new project TestVocab
(in camel case) using the usual Mix build tool invocation (in snake case):
% mix new test_vocab
We’ll then declare a dependency on RDF.ex
in the mix.exs
file:
defp deps do
[
{:rdf, "~> 0.5"}
]
And we use Mix to add in the dependency:
% mix deps.get
See here for the project TestVocab
code.
2. Add a new vocabulary for ‘DC’ elements
Now let’s look first at the core DC elements vocabulary as this is very simple: 15 properties only. In fact, with this small a number we can easily list these out by hand. We’ll make some changes to our main module TestVocab
which is defined in the usual lib/test_vocab.ex
location. Specifically, we’ll remove the standard boilerplate and add a use
declaration, and also a defvocab
definition block for DC
. We also include a @moduledoc
attribute for documentation purposes.
defmodule TestVocab do
@moduledoc """
Test module used in "Early steps in Elixir and RDF" post
""" use RDF.Vocabulary.Namespace defvocab DC,
base_iri: "http://purl.org/dc/elements/1.1/",
terms: ~w[
contributor coverage creator date description format
identifier language publisher relation rights source
subject title type
] end
Note that in the defvocab
definition block we have two keywords: base_iri
which is a string specifying the base IRI for the vocabulary, and terms
which takes a word list of the vocabulary terms.
Let’s try this out in IEx, the Elixir shell, using the -S
option to run our mix.exs
script:
% iex -S mix
Erlang/OTP 21 [erts-10.0.6] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]Compiling 1 file (.ex)
Compiling vocabulary namespace for http://purl.org/dc/elements/1.1/
Interactive Elixir (1.7.3) - press Ctrl+C to exit (type h() ENTER for help)iex(1)> alias TestVocab.DC
[TestVocab.DC]iex(2)> DC.type
~I<http://purl.org/dc/elements/1.1/type>iex(3)> DC.format
~I<http://purl.org/dc/elements/1.1/format>
We use the alias
directive which allows us to provide a namespace shortcut and dispense with the parent module name.
So, there we have it, a very simple means of generating RDF IRIs in the DC namespace. Note that RDF.ex
maintains IRIs as Elixir structs as we can see by using the i
helper in IEx:
iex(4)> i DC.type
Term
~I<http://purl.org/dc/elements/1.1/type>
Data type
RDF.IRI
Description
This is a struct. Structs are maps with a __struct__ key.
Reference modules
RDF.IRI, Map
Implemented protocols
Inspect, IEx.Info, String.Chars, RDF.Term
The ~I
sigil is used to provide a simple string representation for the IRI struct. We can access the IRI string by using the value
field of the struct.
iex(5)> DC.type.value
"http://purl.org/dc/elements/1.1/type"
Now, there is a simpler way to do this. Instead of explicitly listing out all the terms we can just point at a vocabulary schema file and RDF.ex
will determine which terms to include for us. The schema file dc.ttl
is read from the standard location priv/vocabs/
. We just need to add this path to the project and add in the schema file itself. (Note that other RDF serializations could also have been used.)
defmodule TestVocab do
@moduledoc """
Test module used in "Early steps in Elixir and RDF" post
""" use RDF.Vocabulary.Namespace defvocab DC,
base_iri: "http://purl.org/dc/elements/1.1/",
file: "dc.ttl"end
We can also check out the namespace using the private fields __base_iri__
and __terms__
which echo the keywords base_iri
and terms
used in creating the vocabulary term.
iex(6)> DC.__base_iri__
"http://purl.org/dc/elements/1.1/"iex(7)> DC.__terms__
[:contributor, :coverage, :creator, :date, :description, :format, :identifier,
:language, :publisher, :relation, :rights, :source, :subject, :title, :type]
3. Add new vocabularies for ‘BIBO’ ontology
Now let’s try something more ambitious – the BIBO ontology. This term set has both classes and properties and also spans multiple namespaces. For this term set we will read the RDF file bibo.ttl
in priv/vocabs/
and add in these vocabulary definitions to our lib/test_vocab.ex
file:
defvocab BIBO,
base_iri: "http://purl.org/ontology/bibo/",
file: "bibo.ttl"defvocab DCTERMS,
base_iri: "http://purl.org/dc/terms/",
file: "bibo.ttl"defvocab EVENT,
base_iri: "http://purl.org/NET/c4dm/event.owl#",
file: "bibo.ttl"defvocab FOAF,
base_iri: "http://xmlns.com/foaf/0.1/",
file: "bibo.ttl"defvocab PRISM,
base_iri: "http://prismstandard.org/namespaces/1.2/basic/",
file: "bibo.ttl"defvocab SCHEMA,
base_iri: "http://schemas.talis.com/2005/address/schema#",
file: "bibo.ttl"defvocab STATUS,
base_iri: "http://purl.org/ontology/bibo/status/",
file: "bibo.ttl"
Now when we recompile this by opening IEx again (or by using the recompile
command), we’ll see some warnings. For the purposes of this tutorial let’s just ignore this validation behaviour for now by setting the case_violations
option to :ignore
for these namespaces.
defvocab BIBO,
base_iri: "http://purl.org/ontology/bibo/",
file: "bibo.ttl",
case_violations: :ignoredefvocab DCTERMS,
base_iri: "http://purl.org/dc/terms/",
file: "bibo.ttl",
case_violations: :ignore...defvocab STATUS,
base_iri: "http://purl.org/ontology/bibo/status/",
file: "bibo.ttl",
case_violations: :ignore
If we now reopen IEx we can try out the new vocabularies. We’ll first alias the namespaces so we can use project unqualified names.
% iex -S mix
Erlang/OTP 21 [erts-10.0.6] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]Compiling 1 file (.ex)
Compiling vocabulary namespace for http://purl.org/dc/elements/1.1/
Compiling vocabulary namespace for http://purl.org/ontology/bibo/
Compiling vocabulary namespace for http://purl.org/dc/terms/
Compiling vocabulary namespace for http://purl.org/NET/c4dm/event.owl#
Compiling vocabulary namespace for http://xmlns.com/foaf/0.1/
Compiling vocabulary namespace for http://prismstandard.org/namespaces/1.2/basic/
Compiling vocabulary namespace for http://schemas.talis.com/2005/address/schema#
Compiling vocabulary namespace for http://purl.org/ontology/bibo/status/
Interactive Elixir (1.7.3) - press Ctrl+C to exit (type h() ENTER for help)iex(1)> alias TestVocab.{DC, BIBO, DCTERMS, EVENT, FOAF, PRISM, SCHEMA, STATUS}
[TestVocab.DC, TestVocab.BIBO, TestVocab.DCTERMS, TestVocab.EVENT, TestVocab.FOAF, TestVocab.PRISM, TestVocab.SCHEMA, TestVocab.STATUS]iex(2)> FOAF.family_name
~I<http://xmlns.com/foaf/0.1/family_name>iex(3)> DCTERMS.isVersionOf
~I<http://purl.org/dc/terms/isVersionOf>iex(4)> BIBO.editor
~I<http://purl.org/ontology/bibo/editor>iex(5)> PRISM.doi
~I<http://prismstandard.org/namespaces/1.2/basic/doi>
Looks good so far for these properties.
Now classes behave a little differently. They do not resolve directly to IRIs as properties do but can be made to resolve using the RDF.iri
function. They are, however, allowed by RDF.ex
in any place that an IRI is expected.
iex(6)> BIBO.Book
TestVocab.BIBO.Bookiex(7)> RDF.iri(BIBO.Book)
~I<http://purl.org/ontology/bibo/Book>iex(8)> i BIBO.Book
Term
TestVocab.BIBO.Book
Data type
Atom
Raw representation
:"Elixir.TestVocab.BIBO.Book"
Reference modules
Atom
Implemented protocols
Inspect, IEx.Info, String.Chars, List.Chars, RDF.Term
4. Use the vocabularies to build RDF statements
So, let’s put this to use now and build some RDF statements. And what better subject to use than the recent book ‘Adopting Elixir’ by Ben Marx, José Valim, and Bruce Tate.
Following the simple example given for a book resource in the BIBO ontology we aim to provide a very basic RDF description for this bibliographic item as recorded in the file 978–1–68050–252–7.ttl
.
@prefix bibo: <http://purl.org/ontology/bibo/> .
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .<urn:isbn:978-1-68050-252-7> a bibo:Book ;
dc:creator <https://twitter.com/bgmarx> ;
dc:creator <https://twitter.com/josevalim> ;
dc:creator <https://twitter.com/redrapids> ;
dc:date "2018-03-14"^^xsd:date ;
dc:format "Paper" ;
dc:publisher <https://pragprog.com/> ;
dc:title "Adopting Elixir"@en .
Now how can we generate this RDF description in Elixir?
We’re going to show two ways: 1) a rather basic version building on explicit RDF triples, and 2) a more natural Elixir style using piped function calls and RDF.Sigils
for RDF terms.
4a. Long form with explicit RDF triples
Well first let’s make things easier by adding a .iex.exs
hidden file for IEx configuration on startup which imports functions from our project TestVocab
, imports some basic building block functions (iri/1
, literal/1
, literal/2
, triple/3
) from the RDF
module, and also adds an alias for the builtin XSD namespace so we can use the unqualified namespace form XSD.*
, as well as our vocabulary namespaces so we can use the unqualified namespace forms BIBO.*
, DC.*
, etc. (By the way, functions tend to be identified using a name/arity
form, where arity
is the number of arguments a function takes.)
import TestVocab
import RDF, only: [iri: 1, literal: 1, literal: 2, triple: 3]alias RDF.NS.{XSD}
alias TestVocab.{DC, BIBO, DCTERMS, EVENT, FOAF, PRISM, SCHEMA, STATUS}
Let’s define a subject for our RDF triples.
iex(9)> s = iri("urn:isbn:978-1-68050-252-7")
~I<urn:isbn:978-1-68050-252-7>
And now let’s create those RDF triples. These are implemented in RDF.ex
as regular Elixir tuples, i.e. {s, p, o}
. Now we’ve already defined our subject s
, we’re using the new vocabulary terms for our predicates p
, and we use either the functions iri/1
, or literal/1
to generate our objects o
. There is an exception with the DC.date
and DC.title
objects where we instead use a literal/2
function to generate a literal with datatype and language tag, respectively.
iex(10)> t0 = {s, RDF.type, iri(BIBO.Book)}
{~I<urn:isbn:978-1-68050-252-7>, ~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>, ~I<http://purl.org/ontology/bibo/Book>}iex(11)> t1 = {s, DC.creator, iri("https://twitter.com/bgmarx")}{~I<urn:isbn:978-1-68050-252-7>, ~I<http://purl.org/dc/elements/1.1/creator>, ~I<https://twitter.com/bgmarx>}iex(12)> t2 = {s, DC.creator, iri("https://twitter.com/josevalim"}
{~I<urn:isbn:978-1-68050-252-7>, ~I<http://purl.org/dc/elements/1.1/creator>, ~I<https://twitter.com/josevalim>}iex(13)> t3 = {s, DC.creator, iri("https://twitter.com/redrapids")}
{~I<urn:isbn:978-1-68050-252-7>, ~I<http://purl.org/dc/elements/1.1/creator>, ~I<https://twitter.com/redrapids>}iex(14)> t4 = {s, DC.date, literal("2018-03-14", datatype: XSD.date)}
{~I<urn:isbn:978-1-68050-252-7>, ~I<http://purl.org/dc/elements/1.1/date>,
%RDF.Literal{value: ~D[2018-03-14], datatype: ~I<http://www.w3.org/2001/XMLSchema#date>}}iex(15)> t5 = {s, DC.format, literal("Paper")}
{~I<urn:isbn:978-1-68050-252-7>, ~I<http://purl.org/dc/elements/1.1/format>, ~L"Paper"}iex(16)> t6 = {s, DC.publisher, iri("https://pragprog.com/")}{~I<urn:isbn:978-1-68050-252-7>, ~I<http://purl.org/dc/elements/1.1/publisher>, ~I<https://pragprog.com/>}iex(17)> t7 = {s, DC.title, literal("Adopting Elixir", language: "en")}
{~I<urn:isbn:978-1-68050-252-7>, ~I<http://purl.org/dc/elements/1.1/title>, ~L"Adopting Elixir"en}
And finally we assemble those triples into an RDF description. Here we just scoop them up and pass them as a list to the RDF.Description
constructor.
iex(18)> RDF.Description.new [t0, t1, t2, t3, t4, t5, t6, t7]
#RDF.Description{subject: ~I<urn:isbn:978-1-68050-252-7>
~I<http://purl.org/dc/elements/1.1/creator>
~I<https://twitter.com/bgmarx>
~I<https://twitter.com/josevalim>
~I<https://twitter.com/redrapids>
~I<http://purl.org/dc/elements/1.1/date>
%RDF.Literal{value: ~D[2018-03-14], datatype:
~I<http://www.w3.org/2001/XMLSchema#date>}
~I<http://purl.org/dc/elements/1.1/format>
~L"Paper"
~I<http://purl.org/dc/elements/1.1/publisher>
~I<https://pragprog.com/>
~I<http://purl.org/dc/elements/1.1/title>
~L"Adopting Elixir"en
~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>
~I<http://purl.org/ontology/bibo/Book>}
We can wrap this whole construction into a function which we’ll call book/1
and invoke with the argument :with_triples
and we’ll add that to the TestVocab
module.
def book(:with_triples) do s = RDF.iri("urn:isbn:978-1-68050-252-7") t0 = {s, RDF.type, RDF.iri(BIBO.Book)}
t1 = {s, DC.creator, RDF.iri("https://twitter.com/bgmarx")}
t2 = {s, DC.creator, RDF.iri("https://twitter.com/josevalim")}
t3 = {s, DC.creator, RDF.iri("https://twitter.com/redrapids")}
t4 = {s, DC.date, RDF.literal("2018-03-14"), datatype: XSD.date)}
t5 = {s, DC.format, RDF.literal("Paper")}
t6 = {s, DC.publisher, RDF.iri("https://pragprog.com/")}
t7 = {s, DC.title, RDF.literal("Adopting Elixir", language: "en")} RDF.Description.new [t0, t1, t2, t3, t4, t5, t6, t7]end
Note that for explicitness we’ll use the fully qualified names RDF.iri
and RDF.literal
.
4b. Short form with piped function calls
OK, so let’s now show how we might do this in a more natural Elixir style. Here we make use of two RDF.ex
features: sigils for RDF terms, and variant property function calls which implement a description builder style. And to glue it all together, the Elixir pipe operator |>
.
We can show this construction style also using the function book/1
but invoked with the argument :with_pipes
and we’ll also that add to the TestVocab
module.
def book(:with_pipes) do import RDF.Sigils ~I<urn:isbn:978-1-68050-252-7>
|> RDF.type(BIBO.Book)
|> DC.creator(~I<https://twitter.com/bgmarx>,
~I<https://twitter.com/josevalim>,
~I<https://twitter.com/redrapids>)
|> DC.date(RDF.date("2018-03-14"))
|> DC.format(~L"Paper")
|> DC.publisher(~I<https://pragprog.com/>)
|> DC.title(~L"Adopting Elixir"en)end
So, briefly we can construct new RDF terms using the sigils defined in the RDF.Sigils
module (which we import):
~I
for IRIs, e.g.~I<urn:isbn:978–1–68050–252–7>
~L
for literals, e.g.~L"Paper"
for a plain string, and~L"Adopting Elixir"en
for a language tagged string (with no@
sign)~B
for blank nodes, e.g.~B<foo>
Note that there is no sigil form for datatyped literals and instead we make use of convenience RDF functions (here RDF.date/1
for "2018–03–14"
).
The property functions we used earlier took no arguments and just returned the property IRI as an RDF.IRI
struct. But there are also property functions with the same name but taking multiple arguments (between 2 and 6 arguments) for RDF subject and RDF object(s). The 2-argument form expects an RDF subject and a single RDF object and returns an RDF.Description
struct.
iex(19)> DC.format(~I<urn:isbn:978–1–68050–252–7>, ~L"Paper")
#RDF.Description{subject: ~I<urn:isbn:978–1–68050–252–7>
~I<http://purl.org/dc/elements/1.1/format>
~L"Paper"}
In our function book(:with_pipes)
there’s an example of a 4-argument function call for DC.creator
. The first argument is the RDF subject and subsequent arguments are for RDF objects.
And that leaves that essential piece of plumbing, the Elixir pipe operator |>
. This takes an Elixir expression and passes it along as the first argument to the next function call. Elixir functions are usually arranged to expect the first argument to be piped in thus allowing for chains of function calls to be built up.
Now we can call the two constructions directly using the separate function clauses book(:with_triples)
and book(:with_pipes)
. And to generalize we can add the further function clause book(arg)
which just takes a single argument arg
and raises an error with a help message. Note, however, that if either of the keywords :with_triples
or :with_pipes
are used the previously defined function clauses will be invoked instead. To keep things simple we’ll also define a function form book/0
taking no arguments but silently selecting for one of the construction functions.
def book(arg) do
raise "! Error: Usage is book( :with_triples | :with_pipes )"
enddef book(), do: book(:with_pipes)
Note that this fragment shows the two forms for defining a function, a block form and a keyword form.
4c. Serializing the RDF description
There are various options for reading and writing the RDF description as a string or as a file. See the documentation for RDF.Serialization
. But the simplest solution for serializing in Turtle format are the RDF.Turtle
module functions.
So, we can write the RDF description to stdout
as a Turtle string as follows:
iex(20)> RDF.Turtle.write_string!(book) |> IO.puts
<urn:isbn:978-1-68050-252-7
a <http://purl.org/ontology/bibo/Book> ;
<http://purl.org/dc/elements/1.1/creator <https://twitter.com/bgmarx>, <https://twitter.com/josevalim>, <https://twitter.com/redrapids> ;
<http://purl.org/dc/elements/1.1/date> "2018-03-14"^^<http://www.w3.org/2001/XMLSchema#date> ;
<http://purl.org/dc/elements/1.1/format> "Paper" ;
<http://purl.org/dc/elements/1.1/publisher <https://pragprog.com/> ;
<http://purl.org/dc/elements/1.1/title> "Adopting Elixir"@en .:ok
Summary
I’ve shown here in this post a simple use case demonstrating how the RDF.ex
package can be used for working with the RDF data model in Elixir.
Specifically we’ve used RDF.ex
to define a set of RDF vocabularies for two schemas (DC
and BIBO
). We’ve also used these vocabularies to build a simple RDF description for a book resource and shown how to serialize this.
But this post was more by way of providing the briefest of introductions into how Elixir can be used for RDF processing. The real interest, however, in using Elixir for semantic web applications, beyond any functional programming best practice, is ultimately twofold: 1) fault tolerant processing, especially where networks and federated queries are involved, and 2) better management of distributed compute solutions. I hope to be able to show more on this in some follow-on posts.
Next post in this series
See here for the project TestVocab
code.
This is the first in a series of posts.
You can also follow me on Twitter as @tonyhammond.