Early steps in Elixir and RDF

Using RDF.ex to work with RDF vocabularies in Elixir

Photo by Steve Johnson on Unsplash

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 package from Marcel Otto which has pretty comprehensive support for RDF – and he’s recently also extended this support to querying with 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 out for a spin and see what it could do. I won’t give any detailed introduction to here but will focus here on one particular aspect – the support for RDF vocabularies. The distribution ships with five vocabularies (, , , , and ) already included. But it should be pretty simple to add in some new vocabularies, for example and . Let’s see how we might do this.

1. Create a ‘TestVocab’ project

First off, let’s create a new project (in camel case) using the usual Mix build tool invocation (in snake case):

% mix new test_vocab

We’ll then declare a dependency on in the file:

defp deps do
[
{:rdf, "~> 0.5"}
]

And we use Mix to add in the dependency:

% mix deps.get

See here for the project 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 which is defined in the usual location. Specifically, we’ll remove the standard boilerplate and add a declaration, and also a definition block for . We also include a 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 definition block we have two keywords: which is a string specifying the base IRI for the vocabulary, and which takes a word list of the vocabulary terms.

Let’s try this out in IEx, the Elixir shell, using the option to run our 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 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 maintains IRIs as Elixir structs as we can see by using the 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 sigil is used to provide a simple string representation for the IRI struct. We can access the IRI string by using the 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 will determine which terms to include for us. The schema file is read from the standard location . 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 and which echo the keywords and 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 in and add in these vocabulary definitions to our 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 command), we’ll see some warnings. For the purposes of this tutorial let’s just ignore this validation behaviour for now by setting the option to for these namespaces.

defvocab BIBO,
base_iri: "http://purl.org/ontology/bibo/",
file: "bibo.ttl",
case_violations: :ignore
defvocab 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 function. They are, however, allowed by in any place that an IRI is expected.

iex(6)> BIBO.Book
TestVocab.BIBO.Book
iex(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 .

@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 for RDF terms.

4a. Long form with explicit RDF triples

Well first let’s make things easier by adding a hidden file for IEx configuration on startup which imports functions from our project , imports some basic building block functions (, , , ) from the module, and also adds an alias for the builtin XSD namespace so we can use the unqualified namespace form , as well as our vocabulary namespaces so we can use the unqualified namespace forms , , etc. (By the way, functions tend to be identified using a form, where 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 as regular Elixir tuples, i.e. . Now we’ve already defined our subject , we’re using the new vocabulary terms for our predicates , and we use either the functions , or to generate our objects . There is an exception with the and objects where we instead use a 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 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 and invoke with the argument and we’ll add that to the 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 and .

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 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 but invoked with the argument and we’ll also that add to the 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 module (which we import):

  • for IRIs, e.g.
  • for literals, e.g. for a plain string, and for a language tagged string (with no sign)
  • for blank nodes, e.g.

Note that there is no sigil form for datatyped literals and instead we make use of convenience RDF functions (here for ).

The property functions we used earlier took no arguments and just returned the property IRI as an 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 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 there’s an example of a 4-argument function call for . 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 and . And to generalize we can add the further function clause which just takes a single argument and raises an error with a help message. Note, however, that if either of the keywords or are used the previously defined function clauses will be invoked instead. To keep things simple we’ll also define a function form 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 )"
end
def 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 . But the simplest solution for serializing in Turtle format are the module functions.

So, we can write the RDF description to 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 package can be used for working with the RDF data model in Elixir.

Specifically we’ve used to define a set of RDF vocabularies for two schemas ( and ). 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.

See here for the project code.

This is the first in a series of posts.

You can also follow me on Twitter as @tonyhammond.

Distributed data, distributed compute – the graph! | #writing, #workseeking