Defining your own type for citable objects

Published

May 18, 2024

Overview

ISBN numbers uniquely identify published editions of a book. In this tutorial, we will define a type representing a book identified by ISBN-10 number. We’ll create a new URN type for ISBN-10 values, then define a structure for a book following the CITE architecture’s model of a citable object, so that we can identify it by our new ISBN-10 URN type and label it, apply URN logic to compare objects of our new type, and serialize citable books to plain-text format.

We’ll accomplish this in five steps:

  1. define a new type of URN for ISBN-10 values
  2. define a new type of citable object, the CitableBook
  3. implement citation functions for it (the CitableTrait)
  4. implement comparison using URN logic (the UrnComparisonTrait)
  5. implement round-trip serialization (the CexTrait)

(1) Creating a new URN type

For ISBN-10 identifiers:

using CitableBase

struct Isbn10Urn <: Urn
    isbn::AbstractString
end

import Base: show
function show(io::IO, u::Isbn10Urn)
    print(io, u.isbn)
end

distanthorizons = Isbn10Urn("urn:isbn10:022661283X")
urn:isbn10:022661283X
struct IsbnComparable <: UrnComparisonTrait end
import CitableBase: urncomparisontrait
function urncomparisontrait(::Type{Isbn10Urn})
    IsbnComparable()
end

import CitableBase: urnequals
function urnequals(u1::Isbn10Urn, u2::Isbn10Urn)
    u1 == u2
end

import CitableBase: urncontains
function urncontains(u1::Isbn10Urn, u2::Isbn10Urn)
    initial1 = components(u1.isbn)[3][1]
    initial2 = components(u2.isbn)[3][1]
    initial1 == initial2
end
urncontains (generic function with 2 methods)
function english(urn::Isbn10Urn)
    langarea = components(urn.isbn)[3][1]
    langarea == '0' || langarea == '1'
end

import CitableBase: urnsimilar


function urnsimilar(u1::Isbn10Urn, u2::Isbn10Urn)
    initial1 = components(u1.isbn)[3][1]
    initial2 = components(u2.isbn)[3][1]

    (english(u1) && english(u2)) ||  initial1 == initial2
end
urnsimilar (generic function with 2 methods)

(2) A type for citable books

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 326 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 200 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")
enumerations = Isbn10Urn("urn:isbn10:022656875X")
enumerationsbook = CitableBook(enumerations, "Enumerations: Data and Literary Study", "Andrew Piper")
Andrew Piper, *Enumerations: Data and Literary Study* (urn:isbn10:022656875X)
distantbook == enumerationsbook
false

(3) 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))
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)
Isbn10Urn
urn(distantbook)
urn:isbn10:022661283X
label(distantbook)
"Ted Underwood, *Distant Horizons: Digital Evidence and Literary Change* (urn:isbn10:022661283X)"

(4) 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))
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
wrong = Isbn10Urn("urn:isbn10:1108922036")
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

(5) 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))
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)

!!! tip “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](https://cite-architecture.github.io/CitableLibrary.jl/stable/).

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

We first defined a new URN type, the Isbn10Urn, and new type called CitableBook. Here’s what Julia’s dump function shows for an example of a CitableBook:

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

We implemented three traits for the CitableBook type, 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 “guides” section of this includes a recipe showing how to gl a step further by wrapping a Vector of CitableBooks in a type supporting the CITE architecture’s definition of a collection with citable content.