Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plain, Star, Roles, Patterns and Generics #23

Closed
wants to merge 6 commits into from
Closed

Conversation

blake-regalia
Copy link
Contributor

@blake-regalia blake-regalia commented Jul 10, 2021

This PR addresses several shortcomings of the current typings, introduces a set of narrower 'Plain' types for developers dealing with pre-RDF-star formats such as Turtle 1.1, and includes several fixes for generics, defaults, arguments, and return types. Closes #5

1. Introduction of 'Plain' and 'Star' namespaces

Not all libraries are ready to adopt the RDF-star specification draft and some will be interested in exclusively dealing with RDF 1.1 data. To accommodate the narrower scope of those 'Plain' Term types, this introduces the following namespaces:

1a. PlainRole and StarRole

Unions of Term types for the various roles they play in 'plain' RDF 1.1 Data, and RDF-star data, respectively. Each namespace exports the following named types: Subject, Predicate, Object, Graph, Datatype, and Quad

Note that PlainRole is an export-only type and not used by @rdfs/types internally. It is meant to provide developers with a type for working with serialization formats and datasets that do not (yet) support RDF-star, such as Turtle 1.1 .

  • 1a.1

    types/data-model.d.ts

    Lines 137 to 147 in 0824ab6

    /**
    * Unions of Term types for the various roles they play in 'plain' RDF 1.1 Data
    */
    export namespace PlainRole {
    export type Subject = NamedNode | BlankNode;
    export type Predicate = NamedNode;
    export type Object = NamedNode | BlankNode | Literal<Datatype>;
    export type Graph = DefaultGraph | NamedNode | BlankNode;
    export type Datatype = NamedNode;
    export type Quad = PlainQuad;
    }
  • 1a.2

    types/data-model.d.ts

    Lines 162 to 172 in 0824ab6

    /**
    * Unions of Term types for the various roles they play in RDF-star data
    */
    export namespace StarRole {
    export type Subject = PlainRole.Subject | StarQuad;
    export type Predicate = PlainRole.Predicate;
    export type Object = NamedNode | BlankNode | Literal<Datatype> | StarQuad;
    export type Graph = PlainRole.Graph;
    export type Datatype = PlainRole.Datatype;
    export type Quad = StarQuad;
    }

1b. PlainPattern and StarPattern

For each type in PlainRole and StarRole, these pattern interfaces add a union with Variable | null for the various RDFJS methods that accept term patterns as input.

  • 1b.1

    types/data-model.d.ts

    Lines 149 to 159 in 0824ab6

    /**
    * Unions of Term types for the various
    */
    export namespace PlainPatern {
    export type Subject = PlainRole.Subject | TermPattern;
    export type Predicate = PlainRole.Predicate | TermPattern;
    export type Object = PlainRole.Object | TermPattern;
    export type Graph = PlainRole.Graph | TermPattern;
    export type Datatype = PlainRole.Datatype | TermPattern;
    export type Quad = PlainQuad | TermPattern;
    }
  • 1b.2

    types/data-model.d.ts

    Lines 174 to 184 in 0824ab6

    /**
    * Unions of Term types for the various
    */
    export namespace StarPatern {
    export type Subject = StarRole.Subject | TermPattern;
    export type Predicate = StarRole.Predicate | TermPattern;
    export type Object = StarRole.Object | TermPattern;
    export type Graph = StarRole.Graph | TermPattern;
    export type Datatype = StarRole.Datatype | TermPattern;
    export type Quad = StarQuad | TermPattern;
    }

2. Addition of PlainQuad and StarQuad

Without removing BaseQuad, these two new interfaces extend BaseQuad by specifying a stricter term type for each quad component than the former Quad_{Subject|Predicate|Object|Graph} interfaces. Most notably, PlainQuad and StarQuad components do not have unions with Variable (those cases are covered separately by PlainPattern and StarPattern).

Note: Each of the Quad_{Subject|Predicate|Object|Graph} interfaces are still made available in the exports and carry the same exact types as before. A deprecation annotation has been added to notify the user to consider using one of the more fitting types.

  • 2.1

    types/data-model.d.ts

    Lines 300 to 324 in 0824ab6

    /**
    * An RDF quad, containing the subject, predicate, object and graph terms.
    */
    export interface PlainQuad extends StarQuad {
    /**
    * The subject.
    * @see PlainRole.Subject
    */
    subject: PlainRole.Subject;
    /**
    * The predicate.
    * @see PlainRole.Predicate
    */
    predicate: PlainRole.Predicate;
    /**
    * The object.
    * @see PlainRole.Object
    */
    object: PlainRole.Object;
    /**
    * The named graph.
    * @see PlainRole.Graph
    */
    graph: PlainRole.Graph;
    }
  • 2.2

    types/data-model.d.ts

    Lines 274 to 298 in 0824ab6

    /**
    * An RDF quad, containing the subject, predicate, object and graph terms.
    */
    export interface StarQuad extends BaseQuad {
    /**
    * The subject.
    * @see StarRole.Subject
    */
    subject: StarRole.Subject;
    /**
    * The predicate.
    * @see StarRole.Predicate
    */
    predicate: StarRole.Predicate;
    /**
    * The object.
    * @see StarRole.Object
    */
    object: StarRole.Object;
    /**
    * The named graph.
    * @see StarRole.Graph
    */
    graph: StarRole.Graph;
    }

3. OutQuad now defaults to StarQuad instead of BaseQuad

The specification says that each component of the 'Quad' interface be a Term, hence why BaseQuad exists in the typings. In this PR, OutQuad still extends BaseQuad, meaning that developers may still use quad implementations that have unorthodox term types such as Literal in the subject position. However, this PR makes it so that if the generics arguments are omitted, then OutQuad will default to a quad with the traditional term types (including the RDF-star subject and object types) for each component. This change provides developers with stronger type information by default without removing features nor deviating from the spec.

  • 3.1
    export interface DataFactory<OutQuad extends BaseQuad = StarQuad, InQuad extends BaseQuad = StarQuad> {
  • 3.2
    export interface DatasetCore<OutQuad extends BaseQuad = StarQuad, InQuad extends BaseQuad = StarQuad> {
  • 3.3
    export interface DatasetCoreFactory<OutQuad extends BaseQuad = StarQuad, InQuad extends BaseQuad = StarQuad, D extends DatasetCore<OutQuad, InQuad> = DatasetCore<OutQuad, InQuad>> {
  • 3.4
    export interface Dataset<OutQuad extends BaseQuad = StarQuad, InQuad extends BaseQuad = StarQuad> extends DatasetCore<OutQuad, InQuad> {
  • 3.5
    export interface DatasetFactory<OutQuad extends BaseQuad = StarQuad, InQuad extends BaseQuad = StarQuad, D extends Dataset<OutQuad, InQuad> = Dataset<OutQuad, InQuad>>

4. InQuad now defaults to StarQuad instead of OutQuad

InQuad should not default to OutQuad if the 2nd generic argument is omitted. For example:

export class MyDataset extends RDFJS.DatasetCore<MySpecialQuad> {
    // ...
}

Would imply now that the quad parameter is typed to MySpecialQuad for add(quad), delete(quad), has(quad), etc. -- but the library should accept all RDFJS terms as arguments in order to implement the spec, not just MySpecialQuad.

By defaulting to InQuad to StarQuad, if this argument is omitted, then the types for quad arguments are assumed to be traditional, RDF-star compatible quads.

See 3.1, 3.2, 3.3, 3.4, and 3.5 above for code references.

5. subject, predicate, object and graph arguments are now typed to InQuad['{subject|predicate|object|graph}']

Before this change, each argument in match methods (e.g., DatasetCore#match(), Dataset#deleteMatches(), and so on...) were typed to Term. Now these methods are typed by component types within the given generic InQuad argument, and each one is union'ed with | Variable | null.

  • 5.1
    match(subject?: InQuad['subject'] | TermPattern, predicate?: InQuad['predicate'] | TermPattern, object?: InQuad['object'] | TermPattern, graph?: InQuad['graph'] | TermPattern): DatasetCore<OutQuad, InQuad>;
  • 5.2
    deleteMatches(subject?: InQuad['subject'] | TermPattern, predicate?: InQuad['predicate'] | TermPattern, object?: InQuad['object'] | TermPattern, graph?: InQuad['graph'] | TermPattern): this;
  • 5.3
    match(subject?: InQuad['subject'] | TermPattern, predicate?: InQuad['predicate'] | TermPattern, object?: InQuad['object'] | TermPattern, graph?: InQuad['graph'] | TermPattern): Dataset<OutQuad, InQuad>;
  • 5.4
    match(subject?: InQuad['subject'] | TermPattern, predicate?: InQuad['predicate'] | TermPattern, object?: InQuad['object'] | TermPattern, graph?: InQuad['graph'] | TermPattern): Stream<OutQuad>;
  • 5.5
    removeMatches(subject?: InQuad['subject'] | TermPattern, predicate?: InQuad['predicate'] | TermPattern, object?: InQuad['object'] | TermPattern, graph?: InQuad['graph'] | TermPattern)

6. Distinguish between OutQuad vs. InQuad generic arguments for Source and Store

Prior to this PR, the argument types and return type of Source#match() (and, by inheritance, Store#match) were the same; however the implementors should be allowed to accept RDFJS term types as arguments and return a stream of their own implementation of Quad. This change allows implementors to specify those two types (OutQuad and InQuad) much the same way it is done in dataset.d.ts.

Additionally, Store#removeMatches() and Store#deleteGraph() now specify InQuad rather than the ambiguous Q.

  • 6.1
    export interface Source<OutQuad extends BaseQuad = StarQuad, InQuad extends BaseQuad = StarQuad> {
  • 6.2
    export interface Store<OutQuad extends BaseQuad = StarQuad, InQuad extends BaseQuad = StarQuad> extends Source<OutQuad>, Sink<Stream<InQuad>, EventEmitter> {
  • 6.3
    remove(stream: Stream<InQuad>): EventEmitter;
  • 6.4
    deleteGraph(graph: InQuad['graph'] | string): EventEmitter;

7. Remove toArray()

Per rdfjs/dataset-spec#64 .

@changeset-bot
Copy link

changeset-bot bot commented Jul 10, 2021

⚠️ No Changeset found

Latest commit: b54f469

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@tpluscode
Copy link
Contributor

Would you revert the rdfjs-tests.d.ts. rename? It did not get picked up by GitHub's diff

Copy link
Contributor

@tpluscode tpluscode left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me start by saying that I find this PR too big and touching too many areas and without expressing the exact pain points which necessitate bringing in those changes. It also caused a slightly messy diff

  1. The "Role" concept
  2. The arguments of dataset, stream and store (could even be separated?)
  3. toArray

Re 3. It's simple enough; I propose it was a separate PR. A breaking change, though?
Re 2. I don't understand fully. I added some comments which address my concerns

I like the idea of "roles" although I would propose a slightly different execution. Please see my branch extract-roles. I pushed (and renamed) out the StarRole and PlainRole to separate modules and used them as generics to provide types for quad's components

Thus, Quad becomes the default "RDF* Quad" and no deprecation is needed.

import {RdfStar} from './data-model/rdf-star'
import {Rdf11} from './data-model/rdf1.1';

interface Quad<R extends Role = RdfStar> extends BaseQuad {
    subject: R['Subject'];
    predicate: R['Predicate'];
    object: R['Object'];
    graph: R['Graph'];
}

And yes, I don't understand what the *Pattern types are for (other than union in match and similar functions)

import { Stream } from './stream';

export interface DatasetCore<OutQuad extends BaseQuad = Quad, InQuad extends BaseQuad = OutQuad> {

export interface DatasetCore<OutQuad extends BaseQuad = StarQuad, InQuad extends BaseQuad = StarQuad> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not agree with this change. I remember having discussed this with @bergos. While I agree with you goal to have the "in" type be more accepting so that any Term can be passed as argument, this does not reflect reality.

Implementations may or may not require their specific types being used. It is safer to default to a more strict pattern where same type is enforced. This way the author of library or type declaration has the choice to relax the default. It is as simple as

-export class MyDataset extends RDFJS.DatasetCore<MySpecialQuad> {
+export class MyDataset extends RDFJS.DatasetCore<MySpecialQuad, StarQuad> {
    // ...
}

The alternative is that consumers will use MyDataset unbeknownst that it will break in runtime when anything other that MySpecialQuad is provided.

@@ -192,13 +185,13 @@ export interface Dataset<OutQuad extends BaseQuad = Quad, InQuad extends BaseQua
*/
union(quads: Dataset<InQuad>): Dataset<OutQuad, InQuad>;

match(subject?: Term | null, predicate?: Term | null, object?: Term | null, graph?: Term | null): Dataset<OutQuad, InQuad>;
match(subject?: InQuad['subject'] | TermPattern, predicate?: InQuad['predicate'] | TermPattern, object?: InQuad['object'] | TermPattern, graph?: InQuad['graph'] | TermPattern): Dataset<OutQuad, InQuad>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change, excuse the pun, does not match the spec.

Regardless, narrowing the parameters to a subset of Term will be a breaking change whenever the inputs were not strictly typed to the more specific type

let term: Term

// this works now
// will fail when merged
dataset.match(term)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will also make TermPattern redundant?

Copy link
Contributor Author

@blake-regalia blake-regalia Jul 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change allows the implementor to narrow the subtype of Quad they will accept, for example in a dataset that does not support RDF-Star. I don't see what makes this incompatible with the spec. Isn't this related to what you said above?

Implementations may or may not require their specific types being used.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I see how it can appear contradictory.

While the vectors of the changes are opposite, in both cases I think they risk breaking the current behaviour. As mentioned above, a Dataset accepts any term in the match and other methods. Making it more strict will inevitable break the type checks in many projects.

I thought that maybe your changes could work if instead the defaults for type arguments were

-OutQuad extends BaseQuad = Quad
+OutQuad extends BaseQuad = BaseQuad

Thus, defaulting to Term for all parameters. However, consider type DatasetExt = DatasetCore<QuadExt>. Same thing happens, that TypeScript will refuse to transpile when Term is used as argument but rdf-ext will happily ignore "wrong" terms...

@blake-regalia
Copy link
Contributor Author

Yes, I'm happy to break these out into smaller, more manageable PRs. It's just that several of these changes are intra-dependent so it made it hard to split some of them up.

I understand the veto to 4. InQuad now defaults to StarQuad instead of OutQuad .

@tpluscode
Copy link
Contributor

Yes, I'm happy to break these out into smaller, more manageable PRs. It's just that several of these changes are intra-dependent so it made it hard to split some of them up.

I would totally separate "toArray change".

We may work out the rest. If you look at my branch I linked to, it appears that some of those changes could be avoided?

*/
export type Quad_Object = NamedNode | Literal | BlankNode | Quad | Variable;
export namespace PlainPatern {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export namespace PlainPatern {
export namespace PlainPattern {

*/
export type Quad_Subject = NamedNode | BlankNode | Quad | Variable;
export type TermPattern = Variable | null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also undefined?

@blake-regalia
Copy link
Contributor Author

Closing in favor of new and separate PRs per conversation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Export Quad Component Types
3 participants