Paradigm Overview

Paradigm is a model management framework supporting uniform treatment of heterogeneous resources as graph data with conformance and transformation relationships. It provides mathematically-grounded primitives supporting protocol interoperability, model-to-code generation, and rapid integration.

Your Universe of discourse is bootstrapped from a built-in metamodel which conforms to itself. Different Paradigms are introduced as models for objects under scrutiny, such as filesystem contents or data models of a particular format. Graph data is decoupled from its physical form by the graph protocol. So you can have (for example) an XML file specifying the conformance of filesystem objects, or vice versa.

New levels of abstraction are created by introducing transforms at the metamodel level. For example, a schema starts as data of its metamodel, but becomes a model itself in some (hopefully obvious) way. Then we can work with data that conforms to the schema. Some illustrative demos are available at paradigmpro.live.

Structure

Paradigms

Graph (Data)

Paradigm Operations

For these examples we'll use the provided Metamodel paradigm:

metamodel_paradigm = Paradigm.Builtin.Metamodel.definition()

Abstraction

Paradigm.Abstraction allows movement between paradigm definitions and their graph representations.

Any valid Paradigm struct should round-trip:

embedded_metamodel = Paradigm.Abstraction.embed(metamodel_paradigm)
Paradigm.Abstraction.extract(embedded_metamodel) == metamodel_paradigm

Conformance

Paradigm.Conformance.check_graph/2 validates that graph data conforms to its paradigm definition. The conformance checker ensures data integrity by validating:

  • Class validity - All nodes reference defined classes
  • Property completeness - Required properties are present, unknown properties flagged
  • Cardinality constraints - List/single value requirements met
  • Reference integrity - All references point to existing nodes of correct classes
  • Enumeration values - Values match defined enum options

The embedded metamodel validates against itself:

Paradigm.Conformance.check_graph(embedded_metamodel, metamodel_paradigm)

Or if 2 graph objects are passed, the module will attempt to use the Abstraction module to produce a paradigm from the 2nd one:

Paradigm.Conformance.check_graph(embedded_metamodel, embedded_metamodel)

Transforms

The Paradigm.Transform protocol defines how transforms are handled.

They are invoked with transform(transformer, source, target, opts).

  • transformer implements the transform protocol
  • source is a graph
  • target is a graph (not necessarily different or empty, just where new nodes will be added)
  • opts allows configuration.

A simple helper function handles the configuration-free transform case holding the results in memory:

  def transform(transformer, source) do
    target = Paradigm.Graph.MapGraph.new()
    Paradigm.Transform.transform(transformer, source, target, [])
  end

Function transforms

The transform protocol is implemented for Function in the obvious way so that anonymous functions may be used. Here's a simple injection function:

fn source, target ->
  {:ok,
    Paradigm.Graph.stream_all_nodes(source)
    |> Enum.reduce(target, fn node, acc_target ->
      Paradigm.Graph.insert_node(acc_target, node)
    end)
  }
end

Class-based transforms

Paradigm.Transform.ClassBasedTransform encapsulates a common pattern:

  1. Select all nodes of a given type
  2. For each one, produce 1 or more resulting nodes
  3. Reduce across the target graph, inserting them all We can get rid of a lot of repeated code with a builder pattern:
    import Paradigm.Transform.ClassBasedTransform
    new()
    |> with_default(fn node -> node end) # Copy all by default
    |> rename_class("class1", "class2")  # A simple rename helper
    |> for_class("strange_type",
    fn node ->
     %{node | data: %{}}              # Copy over with blanked data
    end)
    |> for_class("multi_type",           # Return a list of nodes
    fn node ->
     [
       %Node{id: node.id <> "_1", ...},
       %Node{id: node.id <> "_2", ...}
     ]
    end)
    |> for_class("insufficient_context_type",
    fn node, graph ->                 # Function can take 2 args
     #Pull in additional information to build the node
    end
    )
    Here you can see the flexibility, as the class-based transform function has access to the node and the full graph, and returns an arbitrary list of nodes.

Pipeline Transforms

Paradigm.Transform.PipelineTransform allows transforms to be composed arbitrarily.

PipelineTransform.new([transform1, transform2, transform3])

Note that intermediate steps automatically target a MapGraph.new(). This means memory should be considered, and "cumulative" effects need to be explicitly carried forward by each step.

Universe Paradigm

The Paradigm.Builtin.Universe paradigm is a system-level model treating Paradigm.Graph and Paradigm.Transform objects as primitive types. The Paradigm.Universe module provides helper functions for working with Universe graphs, including content-addressed (inner) graphs.

The result is all the embedding, conformance checking and transforms above are achieved more ergonomically internal to a Universe-conformant graph:

Paradigm.Universe.bootstrap()
|> Paradigm.Universe.register_transform_by_name(Paradigm.Transform.Identity, "Metamodel", "Metamodel")
|> Paradigm.Universe.apply_propagate()
|> Paradigm.Conformance.conforms?(Paradigm.Builtin.Universe.definition())

Installation

If available in Hex, add paradigm to your list of dependencies in mix.exs:

def deps do
  [
    {:paradigm, "~> 0.3.0"}
  ]
end

Or install directly from GitHub:

def deps do
  [
    {:paradigm, github: "ParadigmaticSystems/paradigm"}
  ]
end

Then run:

mix deps.get

Quick Start

Here's a basic example using the builtin metamodel:

# Get the metamodel paradigm
paradigm = Paradigm.Builtin.Metamodel.definition()

# Embed it into a graph for manipulation
graph = Paradigm.Abstraction.embed(paradigm)

# Validate that the embedded graph conforms to the metamodel
Paradigm.Conformance.check_graph(graph, paradigm)
# => %Paradigm.Conformance.Result{issues: []}

# Extract back to a Paradigm struct
extracted_paradigm = Paradigm.Abstraction.extract(graph)
# extracted_paradigm == paradigm