Citable entities

Summary

The task: We will define a type representing a book identified by ISBN-10 number. Our type will follow the CITE architecture's model of a citable object, so that we can identify it by URN and label, apply URN logic to compare objects of our new type, and serialize citable books to plain-text format.

The implementation:

  • define a new type of citable object, the CitableBook
  • implement citation functions for it (the CitableTrait)
  • implement comparison using URN logic (the UrnComparisonTrait)
  • implement round-trip serialization (the CexTrait)

Defining the CitableBook

We'll take advantage of Julia's type hierarchy to create an abstract CitablePublication type, and make CitableBook a subtype of it. We won't create any further subtypes in this guide, but if we wanted to implement a type for some other form of citable publication, we could then share code applicable to any type of publication (that is, any subtype of CitablePublication).

We'll identify the book using the Isbn10Urn type we previously defined. Again, we'll keep the example simple, and just include strings for authors and a title. You could elaborate this type however you choose.

abstract type CitablePublication <: Citable end
struct CitableBook <: CitablePublication
    urn::Isbn10Urn
    title::AbstractString
    authors::AbstractString
end

As we did with the Isbn10Urn, we'll override the Base package's show function for our new type.

function show(io::IO, book::CitableBook)
    print(io, book.authors, ", *", book.title, "* (", book.urn, ")")
end
show (generic function with 285 methods)

We'll also override the == function so we can easily compare books for equality.

import Base.==
function ==(book1::CitableBook, book2::CitableBook)
    book1.urn == book2.urn && book1.title == book2.title && book1.authors == book2.authors
end
== (generic function with 193 methods)

We can test these by creating a couple of examples of our new type.

distantbook = CitableBook(distanthorizons, "Distant Horizons: Digital Evidence and Literary Change", "Ted Underwood")
enumerationsbook = CitableBook(enumerations, "Enumerations: Data and Literary Study", "Andrew Piper")
Andrew Piper, *Enumerations: Data and Literary Study* (urn:isbn10:022656875X)
distantbook == enumerationsbook
false

Implementing the CitableTrait

The first trait we will implement is the CitableTrait, which specifies that citable objects have an identifying URN and a human-readable label. We'll follow the same general pattern we saw when we implemented the UrnComparisonTrait for the Isbn10Urn type, namely:

  1. define a singleton type to use for the trait value
  2. override the function identifying the trait value for our new type. This time the function is named citabletrait, and we'll define it to return the concrete value CitableByIsnb10() for the type CitableBook.
struct CitableByIsbn10 <: CitableTrait end

import CitableBase: citabletrait
function citabletrait(::Type{CitableBook})
    CitableByIsbn10()
end
citabletrait (generic function with 2 methods)
citabletrait(typeof(distantbook))
Main.CitableByIsbn10()

CitableBase includes the citable function to test whether individual objects belong to a type implementing the function. (This is parallel to the urncomparable function we saw before.)

citable(distantbook)
true

Implementing the required functions urntype, urn, label

Implementing urntype, urn and label is now trivial. The urntype function will report the type of URN we cite this object with. The urn function just returns the urn field of the book. In Julia, Base.show underlies the string function, so since we have already implemented show for our book type, we can just return string(book) for the label function.

import CitableBase: urntype
function urntype(book::CitableBook)
    Isbn10Urn
end

import CitableBase: urn
function urn(book::CitableBook)
    book.urn
end

import CitableBase: label
function label(book::CitableBook)
    string(book)
end
label (generic function with 3 methods)
urntype(distantbook)
Main.Isbn10Urn
urn(distantbook)
urn:isbn10:022661283X
label(distantbook)
"Ted Underwood, *Distant Horizons: Digital Evidence and Literary Change* (urn:isbn10:022661283X)"

Implementing the UrnComparisonTrait

We've already seen the UrnComparisonTrait. We'll now define it for our book type in exactly the same way we did for our URN type. (We don't even need to re-import its functions.)

struct BookComparable <: UrnComparisonTrait end

function urncomparisontrait(::Type{CitableBook})
    BookComparable()
end
urncomparisontrait (generic function with 3 methods)
urncomparisontrait(typeof(distantbook))
Main.BookComparable()
urncomparable(distantbook)
true

Defining the required functions urnequals, urncontains and urnsimilar

Implementing the URN comparison functions for a pair of CitableBooks is nothing more than applying the same URN comparison to the books' URNs.

function urnequals(bk1::CitableBook, bk2::CitableBook)
    bk1.urn == bk2.urn
end
function urncontains(bk1::CitableBook, bk2::CitableBook)
    urncontains(bk1.urn, bk2.urn)
end
function urnsimilar(bk1::CitableBook, bk2::CitableBook)
    urnsimilar(bk1.urn, bk2.urn)
end
urnsimilar (generic function with 3 methods)

Let's test these functions on CitableBooks the same way we tested them for URNs.

dupebook = distantbook
urnequals(distantbook, dupebook)
true
wrongbook = CitableBook(wrong, "Andrew Piper", "Can We Be Wrong? The Problem of Textual Evidence in a Time of Data")
urnequals(distantbook, wrongbook)
false

As before, our URNs define "similarity" as belonging to the same language area, so Distant Horizons and Can We Be Wrong? are similar.

urnsimilar(distantbook, wrongbook)
true

But "containment" was defined as code for the same ISBN areas, so Distant Horizons does not "contain" Can We Be Wrong?.

urncontains(distantbook, wrongbook)
false

Implementing the CexTrait

Finally, we will implement the CexTrait. It requires that we be able to round trip citable content to a plain-text representation in CEX format, and instantiate an equivalent object from the generated CEX. Once again we will:

  1. define a singleton type to use for the trait value
  2. override the function identifying the trait value for our new type. This time the function is named cextrait, and we'll define it to return the concrete value CitableBook() for the type CitableBook.
struct BookCex <: CexTrait end
import CitableBase: cextrait
function cextrait(::Type{CitableBook})
    BookCex()
end
cextrait (generic function with 3 methods)
cextrait(typeof(distantbook))
Main.BookCex()

CitableBase includes the cexserializable function to test individual objects.

cexserializable(distantbook)
true

Defining the required functions cex and fromcex

The cex function composes a delimited-text representation of an object on a single line, with fields separated by an optionally specified delimiting string.

import CitableBase: cex
function cex(book::CitableBook; delimiter = "|")
    join([string(book.urn), book.title, book.authors], delimiter)
end
cex (generic function with 3 methods)
cexoutput = cex(distantbook)
"urn:isbn10:022661283X|Distant Horizons: Digital Evidence and Literary Change|Ted Underwood"

The inverse of cex is fromcex. We need two essential pieces of information to convert a CEX string to an object: the CEX source data, and the type of object to instantiate. However, CitableBase dispatches this function on the trait value of the type want to instantiate. Although we can find that value with the cextrait function, it needs to appear in the function signature for dispatch to work. We will therefore implement a function with three mandatory parameters: one for the trait value, and two more for the CEX data and Julia type to create. (Two optional parameters allow you to define the delimiting string value, or create a dictionary with other configuration settings, but we won't need that for our implementation.)

import CitableBase: fromcex
function fromcex(traitvalue::BookCex, cexsrc::AbstractString, T;
    delimiter = "|", configuration = nothing, strict = true)
    fields = split(cexsrc, delimiter)
    urn = Isbn10Urn(fields[1])
    CitableBook(urn, fields[2], fields[3])
end
fromcex (generic function with 7 methods)
Example of configuring `fromcex`

The CitableLibrary package implements fromcex for its CiteLibrary class. It uses the configuration parameter to map different kinds of content to Julia classes, and create a library that many include many different kinds of citable collections. See its documentation.

Note that because CitableBase can find our type's trait value on its own, it can delegate to the function we just wrote even when you invoke it only two parameters: all a user needs to specify is the CEX data and Julia type.

restored = fromcex(cexoutput, CitableBook)
Ted Underwood, *Distant Horizons: Digital Evidence and Literary Change* (urn:isbn10:022661283X)

The acid test: did we wind up with an equivalent book?

distantbook == restored
true

Recap: citable objects

This page first defined the CitableBook. Here's what an example looks like:

dump(distantbook)
Main.CitableBook
  urn: Main.Isbn10Urn
    isbn: String "urn:isbn10:022661283X"
  title: String "Distant Horizons: Digital Evidence and Literary Change"
  authors: String "Ted Underwood"

We implemented three traits which you can test for with boolean functions.

citable(distantbook)
true
urncomparable(distantbook)
true
cexserializable(distantbook)
true

Those three traits allowed us to identify books by URN, compare books by URN, and round-trip books to and from plain-text representation.

Our initial goal was to manage a reading list of books citable by ISBN number. We could do that directly with, say, a Vector of CitableBooks, but the next page shows how we could go a step futher by wrapping a Vector of CitableBooks in a type supporting the CITE architecture's definition of a collection with citable content.