Skip to content
Rel
REL CONCEPTS
Modules

Modules

This concept guide introduces modules in Rel.

Introduction

Modules in Rel provide a way to organize and structure your data modeling, offering some distinct advantages:

  • Related definitions can be grouped together.
  • Models and database logic can be parameterized and reused.
  • Names can be local to each module, which prevents clashes and keeps the top-level namespace clean.
  • Relations can be renamed before they are used.

This concept guide introduces the basic syntax for modules in Rel, and gives some more advanced examples after that.

Simple Example

The basic module syntax groups relations under a common namespace. For example:

// install

module store
    def office = {"Boston"; "New York"; "Los Angeles"; "Chicago"}
    def product = {(1, "Laptops"); (2, "Desktops"); (3, "Phones")}
end

Modules can also be thought of as nested relations. In the example above, office and product are nested inside store, and available as store[:office] and store[:product]:

// query

store[:office]
Loading store-office...
// query

store[:product][1]
Loading store-product...

Instead of writing store[:office], you can write store:office. Instead of store[:product][1], you can write store:product[1]. The shorter versions are preferred:

// query

def R = equal(store[:office], store:office)
def output = R
Loading store-output-equal...

Importing Module Names: with

You can choose to import some of the definitions of a module into other namespaces and optionally rename them, using statements of the form with <module> use <relation> [as <newname>]. For example:

// query

with store use office
def output = count[office]
Loading count-office...

This example shows how you can rename relations when you import:

// query

with store use product as p
def output = p[1]
Loading rename-relations-on-import...

Using Modules Inside Other Modules

You can use existing module definitions when defining new modules. For example:

// query

module summary
    with store use office
    def office_count = count[office]
    def product = store:product
end

def output = summary
Loading define-new-modules...

Note that while store:office was used to define office_count, it did not become part of summary. By contrast, store:product is part of summary, because of the definition def product = store:product.

Variable Scope

When variable names clash, the innermost scope takes precedence. For example:

// query

def a = 10

module m
    def a = 20
    def b = a + 5
end

def output = m
Loading innermost-scope...

Imports can be shadowed by a new definition that follows them, which will take precedence. In this case, a warning is printed:

// query

module m1
     def a = 1
end

with m1 use a

def a = 5

def output = a
Loading warning-is-printed...

The warning states: “‘a’ is imported but not used because there is a declaration of the same name in the same scope.”

If the def a = ... comes before the with, the import takes precedence in the code that follows it.

// query

module m1
     def a = 1
end
def a = 5

with m1 use a

def output = a
Loading import-takes-precedence...

Try to avoid name clashes in the same scope. It is preferred to put with statements earlier in the code, but later definitions can shadow them. There will be an error message when this happens.

Nested Modules

Modules can be nested. In other words, modules can be defined within modules. This is especially useful when data are fully normalized into a Graph Normal Form:

// query

module person
    def ssn = 123-45-6789
    module name
        def first = "John"
        def middle = "Q"
        def last = "Public"
    end
    module birth
        def city = "Pittsburg"
        def state = "PA"
        def country = "USA"
        def date = parse_date["2000-01-01", "Y-m-d"]
    end
end

def output = person:birth:city
Loading nested-modules...

Module Union

Like all relations, modules can be defined in separate definitions, which are then unioned:

// query

module m2
    def a = 1
end

module m2
    def b = 1
end

def output = m2
Loading module-union...

With this feature, you can, for example:

  • Define large or complex features of a given module separately, even in different installed sources.
  • Extend an existing module for the purposes of a particular query, without persisting the change.

You can also take the union of two separate modules. For example:

// query

module A
    def a = 1
end

module B
    def a = 1
    def b = 1
end

def AB = A ; B

def output = AB
Loading union-of-two-modules...

This lets you extend a given module with functionality defined in another.

Integrity Constraints

Currently, basic integrity constraints are supported in non-parameterized modules (future releases will extend this functionality to parameterized modules). A simple example is shown below:

// query

module mymodule
    def R = {1; 2}
    ic {count[R] = 2}
end

def output = mymodule
Loading integrity-constraints...

Parameterized Modules

Modules can be made more reusable by parameterizing them by one or more relations.

Parameterized modules must use the @inline annotation. For example, you can build a simple module that collects a few statistics for a relation R:

// install

@inline
module my_stats[R]
    def my_minmax = (min[R], max[R])
    def my_mean = mean[R]
    def my_median = median[R]
end

Like @inline definitions that take relations as arguments rather than just individual values, parameters that are instantiated with relations must begin with a capital letter, in this case, R.

// query

def output = my_stats[{1; 2; 3; 5; 8; 13; 100}]
Loading constraints-query...

The module parameter R can be instantiated with any relation that has a numeric range. For example:

// query

def pop = {("a", 10); ("b", 100); ("c", 20); ("d", 23)}
def output = my_stats[pop]
Loading instantiate-with-r...

Inlining

Note that parameterized modules should be inlined. Depending on the module, instances of the module might need @inline as well, as is the case with m in the (rather artificial) example below:

// query

@inline
module mymodule[x]
    def multiply[y] = y * x
    def divide[y] = y / x
end

@inline
def m = mymodule[10]

with mymodule[20] use divide as divides_twenty
def output:first = m:divide[240]
def output:second = divides_twenty[120]
Loading inlining...

Lowercasing x when parameterizing the module mymodule indicates that this module is parameterized by an individual variable, and not a relation (in contrast with the module my_stats above).

Example: Graph Modules

Since modules are relations themselves, they can be used to parameterize other modules.

Consider a module that constructs a complete graph for a set of nodes N; that is, a graph where each node is connected to all other nodes:

// install

@inline
module CompleteGraph[N]
    def node = N
    def edge = N,N
end

The cell below defines a bipartite graph, with M nodes each connected to N nodes, as well as a cycle graph with N nodes (where each node is connected to one “next” node, forming a loop):

// install

@inline
module BipartiteGraph[M, N]
    def node = M; N
    def edge = M, N
end

@inline
module CycleGraph[N]
    def node = N
    def edge(a in N, b in N) =
            sort[N](x, a)
        and sort[N](y, b)
        and y = x%count[N] + 1
        from x, y
end

The module GraphProperties defined below computes some basic properties of a graph module G:

// install

@inline
module GraphProperties[G]
    def outdegree[v in G:node] = count[v1 : G:edge(v, v1)] <++ 0
    def indegree[v in G:node] = count[v1 : G:edge(v1, v)] <++ 0
    def edge_count = count[G:edge] <++ 0
end

The following code instantiates the BipartiteGraph module and passes it as a parameter to the GraphProperties module to compute its properties:

// query

def graph = BipartiteGraph[{"l1"; "l2"}, {"r1"; "r2"; "r3"}]

def output = GraphProperties[graph]

Loading graph-properties...

Here are more examples showing how you can use these definitions:

// query

def cg = CompleteGraph[range[1 ,5, 1]]
def cg_props = GraphProperties[cg]

def bg = BipartiteGraph[{1; 2}, {3; 4; 5}]
def bg_props = GraphProperties[bg]

def cycleg = CycleGraph[{"a"; "b"; "c"; "d" ; "e"}]
def cycleg_props = GraphProperties[cycleg]

module output
    def complete_edge_count = cg_props:edge_count
    def bipartite_edge_count = bg_props:edge_count
    def cycle_edge_count = cycleg_props:edge_count
end
Loading more-examples...

Imported CSV Files and JSON Documents Are Modules

The result of importing CSV and JSON data can be viewed as a module.

CSV Data as Modules

Loading a CSV file into a relation csv_data gives a module of the form:

module csv_data
   def column_one[pos] = ...
   def column_two[pos] = ...
   ...
end

The relation csv_data:column_one maps positions to values for the first column in the CSV file, the same for csv_data:column_two and the second column and so on. See the CSV Import Guide for further details. For example:

// query

def config:data = """
order_id,customer,price
1,92819,3.4
3,78271,1.6
"""

def csv_data = load_csv[config]
def output = csv_data
Loading csv-as-module...

This relation is equivalent to the module:

module order_csv[pos]
    def order_id = ...
    def customer = ...
    def total_price = ...
end

This in turn is equivalent to:

def order_csv[pos, :order_id] = ...
def order_csv[pos, :customer] = ...
def order_csv[pos, :total_price] = ...

JSON Data as Modules

For JSON data, the input JSON structure is naturally reflected in the resulting Rel module structure, with nested JSON structures becoming nested modules, recursively. JSON names become Rel symbols, and values become scalars (single values) or nested modules.

For example:

// query

def config:data = """
{
  "name": "John",
  "address": {"state": "WA", "city": "Seattle"},
   "age": 21
}"""

def person = load_json[config]
def output = person
Loading json-as-modules...