Repository: AlgebraicJulia/Catlab.jl
Pull request #253 (2020-09-03 18:45:06)

V0.7.4 Version Bump on Master
Version bump the master branch. Currently if you depend on Catlab v0.7.4+ and `]dev Catlab` you get a version issue.


Patch:
diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml
index f8fd014ef..d77d3a0c3 100644
--- a/.github/workflows/TagBot.yml
+++ b/.github/workflows/TagBot.yml
@@ -9,4 +9,3 @@ jobs:
       - uses: JuliaRegistries/TagBot@v1
         with:
           token: ${{ secrets.GITHUB_TOKEN }}
-          ssh: ${{ secrets.DOCUMENTER_KEY }}
diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml
deleted file mode 100644
index a096ce085..000000000
--- a/.github/workflows/codecov.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-name: Code Coverage
-
-on: [push, pull_request]
-
-jobs:
-  build:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v2
-      - uses: julia-actions/setup-julia@latest
-        with:
-          version: '1.5'
-      - name: "Install system dependencies"
-        run: |
-          sudo apt-get update
-          sudo apt-get install graphviz pdf2svg
-          sudo apt-get install texlive-latex-base texlive-latex-extra texlive-binaries texlive-pictures texlive-luatex
-      - uses: julia-actions/julia-buildpkg@latest
-      - name: "Run exhaustive tests"
-        run: julia --project --code-coverage=user -e 'using Pkg; Pkg.activate("test"); Pkg.instantiate(); pkg"dev ."; include("test/runtests.jl"); Pkg.activate("docs"); Pkg.instantiate(); pkg"dev ."; map(x->map(y->include(joinpath(x[1],y)), x[3]), walkdir("docs/literate"));'
-      - uses: julia-actions/julia-processcoverage@v1
-      - uses: codecov/codecov-action@v1
-        with:
-          file: lcov.info
-          fail_ci_if_error: true
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 8ce401900..47b4fcbf0 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -8,7 +8,7 @@ jobs:
       - name: "Set up Julia"
         uses: julia-actions/setup-julia@latest
         with:
-          version: '1.5'
+          version: '1.4'
       - name: "Install system dependencies"
         run: |
           sudo apt-get update
diff --git a/CONDUCT.md b/CONDUCT.md
deleted file mode 100644
index 2dcb1a624..000000000
--- a/CONDUCT.md
+++ /dev/null
@@ -1,1 +0,0 @@
-Hello, and welcome! In order to maintain a healthy, respectful environment, we ask that you observe and uphold the [Julia community standards](https://julialang.org/community/standards/).
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index 8833d3946..000000000
--- a/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-The MIT License
-
-Copyright (c) 2017-2020: Evan Patterson.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 000000000..16fede22d
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,26 @@
+The Catlab.jl package is licensed under the Simplified "2-clause" BSD License:
+
+> Copyright (c) 2017: Evan Patterson.
+> All rights reserved.
+> 
+> Redistribution and use in source and binary forms, with or without
+> modification, are permitted provided that the following conditions are met:
+> 
+> * Redistributions of source code must retain the above copyright notice, this
+>   list of conditions and the following disclaimer.
+> 
+> * Redistributions in binary form must reproduce the above copyright notice,
+>   this list of conditions and the following disclaimer in the documentation
+>   and/or other materials provided with the distribution.
+> 
+> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+> AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+> IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+> DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+> FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+> DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+> SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+> OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+> OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+> 
diff --git a/Project.toml b/Project.toml
index 7eb7ce6fe..0030146f9 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,24 +1,23 @@
 name = "Catlab"
 uuid = "134e5e36-593f-5add-ad60-77f754baafbe"
-license = "MIT"
+license = "BSD2"
 authors = ["Evan Patterson <evan@epatters.org>"]
-version = "0.7.3"
+version = "0.7.4"
 
 [deps]
 AutoHashEquals = "15f4f7f2-30c1-5605-9d31-71845cf9641f"
 Compat = "34da2185-b29b-5c13-b0c7-acf172513d20"
 Compose = "a81c6b42-2e10-5240-aca2-a61377ecd94b"
 DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
-FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e"
 GeneralizedGenerated = "6b9d7cbe-bcb9-11e9-073f-15a7a543e2eb"
 JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
+LabelledArrays = "2ee39098-c373-598a-b85f-a56591580800"
 LightGraphs = "093fc24a-ae57-5d10-9952-331d41423f4d"
 LightXML = "9c8b4983-aa76-5018-a973-4c85ecc9e179"
 LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
 Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
 MLStyle = "d8e11817-5142-5d16-987a-aa16d5891078"
 MetaGraphs = "626554b9-1ddb-594c-aa3c-2596fe9399a5"
-Missings = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28"
 Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a"
 Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
 Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
@@ -27,7 +26,6 @@ Requires = "ae029012-a4dd-5104-9daa-d747884805df"
 SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
 StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
 Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
-StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a"
 
 [compat]
 AutoHashEquals = "^0"
@@ -36,21 +34,19 @@ Compose = "0.7, 0.8, 0.9"
 DataStructures = "^0"
 GeneralizedGenerated = "^0"
 JSON = "^0"
+LabelledArrays = "^1"
 LightGraphs = "^1.3"
 LightXML = "^0"
 MLStyle = "^0"
 MetaGraphs = "^0.6"
-Missings = "^0"
 Parameters = "^0"
 Reexport = "^0"
 Requires = "^1"
 StaticArrays = "^0"
-StructArrays = "^0"
 julia = "1.0"
 
 [extras]
 Convex = "f65535da-76fb-5f13-bab9-19810c17039a"
-Graphviz_jll = "3c863552-8265-54e4-a6dc-903eb78fde85"
 LinearMaps = "7a12625a-238d-50fd-b39a-03d52299707e"
 LinearOperators = "5c8ed15e-5a4c-59e4-a42b-c7e8811fb125"
 SCS = "c946c3f1-0d1f-5ce8-9dea-7daa1f7e2d13"
diff --git a/README.md b/README.md
index c8dda5d27..035fe87b7 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,7 @@
 # ![Catlab.jl](docs/src/assets/logo.png) Catlab.jl
 
 [![Build Status](https://github.com/AlgebraicJulia/Catlab.jl/workflows/Tests/badge.svg)](https://github.com/AlgebraicJulia/Catlab.jl/actions?query=workflow%3ATests)
-[![Stable Docs](https://img.shields.io/badge/docs-stable-blue.svg)](https://algebraicjulia.github.io/Catlab.jl/stable)
-[![Dev Docs](https://img.shields.io/badge/docs-dev-blue.svg)](https://algebraicjulia.github.io/Catlab.jl/dev)
-[![Code Coverage](https://codecov.io/gh/AlgebraicJulia/Catlab.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/AlgebraicJulia/Catlab.jl)
-[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.598366.svg)](https://doi.org/10.5281/zenodo.598366)
+[![Latest Docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://algebraicjulia.github.io/Catlab.jl/latest) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.598366.svg)](https://doi.org/10.5281/zenodo.598366)
 
 Catlab.jl is a framework for applied and computational category theory, written
 in the [Julia](https://julialang.org) language. Catlab provides a programming
diff --git a/docs/literate/graphics/layouts_vs_drawings.jl b/docs/literate/graphics/layouts_vs_drawings.jl
index 88aef335f..60192397c 100644
--- a/docs/literate/graphics/layouts_vs_drawings.jl
+++ b/docs/literate/graphics/layouts_vs_drawings.jl
@@ -22,7 +22,7 @@ expr = otimes(f, compose(f,g), compose(f,g,h))
 using Catlab.WiringDiagrams, Catlab.Graphics
 
 diagram = to_wiring_diagram(expr)
-WiringDiagrams.graph(diagram)
+graph(diagram)
 
 # ## Graphviz layout
 
diff --git a/docs/literate/graphics/tikz_wiring_diagrams.jl b/docs/literate/graphics/tikz_wiring_diagrams.jl
index 346f29e5f..6841daac1 100644
--- a/docs/literate/graphics/tikz_wiring_diagrams.jl
+++ b/docs/literate/graphics/tikz_wiring_diagrams.jl
@@ -110,7 +110,7 @@ X = Ob(FreeAbelianBicategoryRelations, :X)
 
 to_tikz(plus(X) ⋅ mcopy(X))
 #-
-to_tikz((mcopy(X)⊕mcopy(X)) ⋅ (id(X)⊕swap(X,X)⊕id(X)) ⋅ (plus(X)⊕plus(X)))
+to_composejl((mcopy(X)⊕mcopy(X)) ⋅ (id(X)⊕swap(X,X)⊕id(X)) ⋅ (plus(X)⊕plus(X)))
 
 # ## Custom styles
 
diff --git a/docs/make.jl b/docs/make.jl
index 8a5559d46..c44a3409f 100644
--- a/docs/make.jl
+++ b/docs/make.jl
@@ -7,8 +7,8 @@ const generated_dir = joinpath(@__DIR__, "src", "generated")
 @info "Loading Catlab.jl"
 using Catlab,
   # Must explicitly load all submodules for which docs are needed.
-  Catlab.Theories, Catlab.CategoricalAlgebra, Catlab.WiringDiagrams,
-  Catlab.Graphics, Catlab.Programs, Catlab.LinearAlgebra
+  Catlab.Theories, Catlab.WiringDiagrams, Catlab.Graphics,
+  Catlab.Programs, Catlab.LinearAlgebra
 
 @info "Building Literate.jl docs"
 
@@ -61,7 +61,6 @@ makedocs(
     "Modules" => Any[
       "apis/core.md",
       "apis/theories.md",
-      "apis/categorical_algebra.md",
       "apis/wiring_diagrams.md",
       "apis/graphics.md",
       "apis/programs.md",
diff --git a/docs/src/apis/categorical_algebra.md b/docs/src/apis/categorical_algebra.md
deleted file mode 100644
index 6fae4bf0f..000000000
--- a/docs/src/apis/categorical_algebra.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# [Categorical algebra](@id categorical_algebra)
-
-```@autodocs
-Modules = [
-  CategoricalAlgebra.FreeDiagrams,
-  CategoricalAlgebra.Limits,
-  CategoricalAlgebra.FinSets,
-  CategoricalAlgebra.FinRelations,
-  CategoricalAlgebra.Permutations,
-  CategoricalAlgebra.CSets,
-  CategoricalAlgebra.Graphs,
-]
-Private = false
-```
-
diff --git a/docs/src/apis/core.md b/docs/src/apis/core.md
index a4f4a5fa7..c66133011 100644
--- a/docs/src/apis/core.md
+++ b/docs/src/apis/core.md
@@ -1,22 +1,21 @@
-# Theories, instances, and expressions
+# Symbolic expressions
 
-At the core of Catlab is a system for defining generalized algebraic structures,
-such as categories and monoidal categories, and then creating instances of these
-structures in Julia code. The objects, morphisms, and even higher-order
-morphisms can also be represented as typed symbolic expressions, as in a
-computer algebra system. These expressions can be manipulated abstractly or
-transformed into more concrete representations, such as [wiring diagrams](@ref
-wiring_diagrams) or [Julia functions](@ref programs).
+At the core of Catlab is a system for defining and manipulating symbolic
+expressions in typed algebraic structures, including categories and monoidal
+categories. Objects, morphisms, and even higher-order morphisms are represented
+as typed symbolic expressions. The expressions can be manipulated abstractly or
+transformed, usually functorially, into more concrete representations, such as
+[wiring diagrams](@ref wiring_diagrams) or [Julia functions](@ref programs).
 
 The basic elements of this system are:
 
 1. **Generalized algebraic theories** (GATs), defined using the
-   [`@theory`](@ref) macro. Categories and other typed (multisorted) algebraic
-   structures can be defined as GATs. Alternatively, the [`@signature`](@ref)
-   macro can be used when only the signature (not the axioms) of the GAT are to
-   be specified.
+   [`@theory`](@ref) macro. Categories and other typed (multisorted)
+   algebraic structures can be defined as GATs. The [`@signature`](@ref) macro
+   can be used in cases where only the signature of the GAT is defined, and not
+   the axioms.
 
-2. **Instances**, or concrete implementations, of theories, defined using the
+2. **Instances**, or concrete implementations, of theories, asserted using the
    [`@instance`](@ref) macro.
 
 3. **Syntax systems** for theories, defined using the [`@syntax`](@ref) macro.
@@ -26,11 +25,10 @@ The basic elements of this system are:
 We'll explain each of these elements in greater detail in the following
 sections. From the programming perspective, theories can be thought of as
 *interfaces* and bear some resemblance to [type
-classes](https://en.wikipedia.org/wiki/Type_class) in languages like Haskell.
-Both instances and syntax systems can then be thought of as *implementations* of
-the interface.
+classes](https://en.wikipedia.org/wiki/Type_class). Both instances and syntax
+systems then act as *implementations* of the interface.
 
-## [Theories](@id gats)
+## Theories
 
 [Generalized algebraic
 theories](https://ncatlab.org/nlab/show/generalized+algebraic+theory) (GATs) are
@@ -53,7 +51,7 @@ import Catlab.Theories: Ob, Hom, ObExpr, HomExpr, dom, codom, compose, ⋅, id
 ```
 
 ```@example category
-@theory Category{Ob,Hom} begin
+@theory Category(Ob,Hom) begin
   @op begin
     (→) := Hom
     (⋅) := compose
@@ -114,7 +112,7 @@ inherit a theory that already has the alias defined.
   categories*
 - Pitts, 1995: Categorical logic, Sec 6: Dependent types
 
-## [Instances](@id instances)
+## Instances
 
 A theory can have one or more *instances*, or instantiations by ordinary
 Julia types and functions. This feature builds on Julia's support for generic
@@ -134,7 +132,7 @@ struct MatrixDomain
   dim::Int
 end
 
-@instance Category{MatrixDomain, Matrix} begin
+@instance Category(MatrixDomain, Matrix) begin
   dom(M::Matrix) = MatrixDomain(eltype(M), size(M,1))
   codom(M::Matrix) = MatrixDomain(eltype(M), size(M,2))
 
@@ -154,7 +152,7 @@ representing a field $k$ and an integer `dim` representing the dimensionality
 $n$, and so can be interpreted as the $n$-dimensional vector space $k^n$. The
 theory `Hom` is mapped to the standard Julia type `Matrix`.
 
-## [Syntax systems](@id syntax-systems)
+## Syntax systems
 
 Theories can also be instantiated as systems of symbolic expressions, using
 the [`@syntax`](@ref) macro. The symbolic expressions are expression trees, as
@@ -173,7 +171,7 @@ Below, we subtype from Catlab's abstract types `ObExpr` and `HomExpr` to enable
 LaTeX pretty-printing and other convenient features, but this is not required.
 
 ```@example category
-@syntax CategoryExprs{ObExpr, HomExpr} Category begin
+@syntax CategoryExprs(ObExpr, HomExpr) Category begin
 end
 
 A, B, C, D = [ Ob(CategoryExprs.Ob, X) for X in [:A, :B, :C, :D] ]
@@ -200,7 +198,7 @@ $n$. The option `strict=true` tells Catlab to check that the domain and codomain
 objects are strictly equal and throw an error if they are not.
 
 ```@example category
-@syntax SimplifyingCategoryExprs{ObExpr, HomExpr} Category begin
+@syntax SimplifyingCategoryExprs(ObExpr, HomExpr) Category begin
   compose(f::Hom, g::Hom) = associate(new(f,g; strict=true))
 end
 
@@ -240,7 +238,7 @@ import Catlab.Theories: Ob, Hom, ObExpr, HomExpr, SymmetricMonoidalCategory,
 ```
 
 ```@example cartesian-monoidal-category
-@signature CartesianCategory{Ob,Hom} <: SymmetricMonoidalCategory{Ob,Hom} begin
+@signature SymmetricMonoidalCategory(Ob,Hom) => CartesianCategory(Ob,Hom) begin
   mcopy(A::Ob)::(A → (A ⊗ A))
   delete(A::Ob)::(A → munit())
 
@@ -254,7 +252,7 @@ nothing # hide
 We could then define the copying operation in terms of the pairing.
 
 ```@example cartesian-monoidal-category
-@syntax CartesianCategoryExprsV1{ObExpr,HomExpr} CartesianCategory begin
+@syntax CartesianCategoryExprsV1(ObExpr,HomExpr) CartesianCategory begin
   mcopy(A::Ob) = pair(id(A), id(A))
 end
 
@@ -266,7 +264,7 @@ Alternatively, we could define the pairing and projections in terms of the
 copying and deleting operations.
 
 ```@example cartesian-monoidal-category
-@syntax CartesianCategoryExprsV2{ObExpr,HomExpr} CartesianCategory begin
+@syntax CartesianCategoryExprsV2(ObExpr,HomExpr) CartesianCategory begin
   pair(f::Hom, g::Hom) = compose(mcopy(dom(f)), otimes(f,g))
   proj1(A::Ob, B::Ob) = otimes(id(A), delete(B))
   proj2(A::Ob, B::Ob) = otimes(delete(A), id(B))
@@ -277,6 +275,10 @@ f, g = Hom(:f, A, B), Hom(:g, A, C)
 pair(f, g)
 ```
 
+## Presentations
+
+TODO
+
 ## API
 
 ```@autodocs
diff --git a/docs/src/apis/theories.md b/docs/src/apis/theories.md
index 150fba60e..b595e1188 100644
--- a/docs/src/apis/theories.md
+++ b/docs/src/apis/theories.md
@@ -1,28 +1,4 @@
-# Standard library of theories
-
-Through the module `Catlab.Theories`, Catlab provides a standard library of
-[generalized algebraic theories](@ref gats) for categories, monoidal categories,
-and other categorical structures. The theories correspond, in most cases, to
-standard definitions in category theory and they are used throughout Catlab and
-the AlgebraicJulia ecosystem to structure programs and provide a common
-interface for applied category theory. The module also provides default [syntax
-systems](@ref syntax-systems) for many of the theories.
-
-Categorical structures for which theories are provided include:
-
-+ categories
-+ monoidal and symmetric monoidal categories
-+ cartesian and cocartesian categories
-+ semiadditive categories/biproduct categories
-+ hypergraph categories
-+ bicategories of relations
-+ categories with two monoidal products, such as distributive monoidal
-  categories
-
-The contents of this module can be supplemented by the user, and it is even
-possible to use many parts of Catlab without using this module. The user is free
-to create new syntax systems for the theories defined here and also to define
-entirely new theories.
+# Theories
 
 ```@autodocs
 Modules = [ Theories ]
diff --git a/experiments/CompAlgebra/src/AlgebraicNets.jl b/experiments/CompAlgebra/src/AlgebraicNets.jl
index d3fb51cc8..b80b9e4c4 100644
--- a/experiments/CompAlgebra/src/AlgebraicNets.jl
+++ b/experiments/CompAlgebra/src/AlgebraicNets.jl
@@ -33,12 +33,12 @@ import Catlab.Programs.GenerateJuliaPrograms: genvar, genvars, to_function_expr,
 
 TODO: Explain
 """
-@signature AlgebraicNetTheory{Ob,Hom} <: MonoidalCategoryWithBidiagonals{Ob,Hom} begin
+@signature MonoidalCategoryWithBidiagonals(Ob,Hom) => AlgebraicNetTheory(Ob,Hom) begin
   linear(x::Any, A::Ob, B::Ob)::(A → B)
   constant(x::Any, A::Ob)::(munit() → A)
 end
 
-@syntax AlgebraicNet{ObExpr,HomExpr} AlgebraicNetTheory begin
+@syntax AlgebraicNet(ObExpr,HomExpr) AlgebraicNetTheory begin
   # FIXME: `compose` and `otimes` should delegate to wiring layer when possible.
   compose(f::Hom, g::Hom) = associate(new(f,g; strict=true))
   otimes(A::Ob, B::Ob) = associate_unit(new(A,B), munit)
diff --git a/experiments/CompAlgebra/src/MathFormulas.jl b/experiments/CompAlgebra/src/MathFormulas.jl
index 7f383882b..7c7d43edb 100644
--- a/experiments/CompAlgebra/src/MathFormulas.jl
+++ b/experiments/CompAlgebra/src/MathFormulas.jl
@@ -99,7 +99,7 @@ end
 These methods should only be used with `gensym`-ed variables since they assume
 that any two formulas have disjoint variables.
 """
-@instance AlgebraicNetTheory{NFormula, Formulas} begin
+@instance AlgebraicNetTheory(NFormula, Formulas) begin
   dom(f::Formulas) = NFormula(length(f.inputs))
   codom(f::Formulas) = NFormula(length(f.terms))
 
diff --git a/experiments/Markov/src/MarkovCategories.jl b/experiments/Markov/src/MarkovCategories.jl
index 41ef82617..2e33741a3 100644
--- a/experiments/Markov/src/MarkovCategories.jl
+++ b/experiments/Markov/src/MarkovCategories.jl
@@ -13,12 +13,12 @@ import Catlab.Theories: Ob, Hom, dom, codom, compose, ⋅, ∘, otimes, ⊗, bra
 
 """ Theory of *Markov categories*
 """
-@signature MarkovCategory{Ob,Hom} <: MonoidalCategoryWithDiagonals{Ob,Hom} begin
+@signature MonoidalCategoryWithDiagonals(Ob,Hom) => MarkovCategory(Ob,Hom) begin
   expectation(M::(A → B))::(A → B) <= (A::Ob, B::Ob)
   @op (𝔼) := expectation
 end
 
-@syntax FreeMarkovCategory{ObExpr,HomExpr} MarkovCategory begin
+@syntax FreeMarkovCategory(ObExpr,HomExpr) MarkovCategory begin
   otimes(A::Ob, B::Ob) = associate_unit(new(A,B), munit)
   otimes(f::Hom, g::Hom) = associate(new(f,g))
   compose(f::Hom, g::Hom) = associate(new(f,g; strict=true))
diff --git a/src/categorical_algebra/CSets.jl b/src/categorical_algebra/CSets.jl
index 9251b040b..241cfe5df 100644
--- a/src/categorical_algebra/CSets.jl
+++ b/src/categorical_algebra/CSets.jl
@@ -1,373 +1,294 @@
+""" Generic data structures for C-sets (presheaves).
+"""
 module CSets
-export AbstractACSet, ACSet, AbstractCSet, CSet,
+export AbstractCSet, AbstractCSetType, CSet, CSetType,
   nparts, has_part, subpart, has_subpart, incident, add_part!, add_parts!,
   copy_parts!, set_subpart!, set_subparts!, disjoint_union
 
-# Compatibility with Julia v1.0.
-import Base: fieldnames
-if VERSION < v"1.1"
-  fieldtypes(::Type{T}) where T <: NamedTuple = T.parameters[2].parameters
-  fieldtypes(::Type) = ()
-else
-  import Base: fieldtypes
-end
+using Compat
+using LabelledArrays, StaticArrays
 
-using Compat: isnothing
-using StructArrays
+using ...Present
+using ...Theories: Category, dom, codom
 
-using ...Theories
+# C-set data types
+##################
 
-# Struct arrays
-###############
+const NamedSVector{Names,T,N} = SLArray{Tuple{N},T,1,N,Names}
 
-const EmptyTuple = Union{Tuple{},NamedTuple{(),Tuple{}}}
-const StructArray0{T} = Union{StructArray{T},Vector{<:EmptyTuple}}
+""" Abstract type for C-sets (presheaves).
 
-""" Create StructArray while avoiding inconsistency with zero length arrays.
+The type parameters are:
+- `Ob`: tuple of symbols naming objects
+- `Hom`: tuple of symbols naming morphims
+- `Dom`: tuple of integers giving domain of each morphism
+- `Codom`: tuple of integers giving codomain of each morphism
+- `Data`: tuple of symbols naming extra morphisms for data/attributes
+- `DataDom`: tuple of integers giving domain of each data morphism
 
-By default, just constructs a StructArray (a struct of arrays) but when struct
-is empty, returns a ordinary Julia vector (an array of empty structs).
+For example, a graph with one vertex attribute could be represented as
+`AbstractCSet{(:V,:E),(:src,:tgt),(2,2),(1,1),(:vattr,),(1,)}`.
 
-See: https://github.com/JuliaArrays/StructArrays.jl/issues/148
+See also: [`CSet`](@ref) and [`CSetType`](@ref).
 """
-make_struct_array(x) = StructArray(x)
+abstract type AbstractCSet{Ob,Hom,Dom,Codom,Data,DataDom} end
 
-function make_struct_array(n::Int, x)
-  sa = StructArray(x)
-  @assert length(sa) == n
-  sa
-end
+""" Data type for C-sets (presheaves).
 
-make_struct_array(::EmptyTuple) = error("Length needed when struct is empty")
-make_struct_array(n::Int, ::T) where T <: EmptyTuple = fill(T(()), n)
-make_struct_array(v::Vector{<:EmptyTuple}) = v
+Instead of filling out the type parameters manually, we recommend using the
+function [`CSetType`](@ref) to generate a `CSet` type from a presentation of a
+category. Nevertheless, the first six type parameters are documented at
+[`AbstractCSet`](@ref). The remaining type parameters are an implementation
+detail and should be ignored.
 
-make_struct_array(::Type{SA}, ::UndefInitializer, n::Int) where
-  SA <: StructArray = SA(undef, n)
-make_struct_array(::Type{<:StructArray{T}}, ::UndefInitializer, n::Int) where
-  T <: EmptyTuple = fill(T(()), n)
+Following LightGraphs.jl, the incidence vectors, stored in the `incidence`
+field, are kept in sorted order. To ensure consistency, no field of the struct
+should ever be mutated directly.
+"""
+mutable struct CSet{Ob,Hom,Dom,Codom,Data,DataDom,Index,DataIndex,IsUnique} <:
+       AbstractCSet{Ob,Hom,Dom,Codom,Data,DataDom}
+  nparts::NamedSVector{Ob,Int}
+  subparts::NamedSVector{Hom,Vector{Int}}
+  indices::NamedSVector{Index,Vector{Vector{Int}}}
+  data::NamedTuple{Data}
+  data_indices::NamedTuple{DataIndex}
+end
 
-function fieldtypes(::Type{T}) where {T <: StructArray{<:NamedTuple}}
-  fieldtypes(eltype(T))
+function CSet{Ob,Hom,Dom,Codom,Data,DataDom,Index,DataIndex,IsUnique}(;
+    kw...) where {Ob,Hom,Dom,Codom,Data,DataDom,Index,DataIndex,IsUnique}
+  CSet{Ob,Hom,Dom,Codom,Data,DataDom,Index,DataIndex,IsUnique}((; kw...))
 end
-function fieldnames(::Type{T}) where {T <: StructArray{<:NamedTuple}}
-  fieldnames(eltype(T))
+function CSet{Ob,Hom,Dom,Codom,Data,DataDom,Index,DataIndex,IsUnique}(
+    datatypes::NamedTuple{Data}) where
+    {Ob,Hom,Dom,Codom,Data,DataDom,Index,DataIndex,IsUnique}
+  NOb, NHom, NIndex = length(Ob), length(Hom), length(Index)
+  CSet{Ob,Hom,Dom,Codom,Data,DataDom,Index,DataIndex,IsUnique}(
+    NamedSVector{Ob,Int,NOb}(zeros(SVector{NOb,Int})),
+    NamedSVector{Hom,Vector{Int},NHom}(Tuple(Int[] for i in 1:NHom)),
+    NamedSVector{Index,Vector{Vector{Int}},NIndex}(
+      Tuple(Vector{Int}[] for i in 1:NIndex)),
+    NamedTuple{Data}(Union{T,Missing}[] for T in datatypes),
+    NamedTuple{DataIndex}(
+      Dict{datatypes[name], is_unique ? Int : Vector{Int}}()
+      for (name, is_unique) in zip(DataIndex, IsUnique)))
 end
 
-# Data types
-############
+""" Generate an abstract C-set type from a presentation of a category.
 
-abstract type AbstractAttributedCSet{CD,AD,Ts} end
+See also: [`CSetType`](@ref).
+"""
+function AbstractCSetType(pres::Presentation{Category}; data=())
+  # Only include `Data` and `DataDom` params if there are data morphisms.
+  Ts = CSetTypeParams(pres, data=data)
+  AbstractCSet{(isempty(Ts[5]) ? Ts[1:4] : Ts[1:6])...}
+end
 
-const AbstractACSet = AbstractAttributedCSet
+""" Generate a C-set data type from a presentation of a category.
 
-struct AttributedCSet{CD <: CatDesc, AD <: AttrDesc{CD}, Ts <: Tuple, Idxed,
-             TablesType <: NamedTuple, IndicesType <: NamedTuple} <: AbstractACSet{CD,AD,Ts}
-  tables::TablesType
-  indices::IndicesType
-  function AttributedCSet{CD,AD,Ts,Idxed}() where {CD <: CatDesc, AD <: AttrDesc{CD}, Ts <: Tuple, Idxed}
-    tables = make_tables(CD,AD,Ts)
-    indices = make_indices(CD,AD,Ts,Idxed)
-    new{CD,AD,Ts,Idxed,typeof(tables),typeof(indices)}(tables,indices)
-  end
-  function AttributedCSet{CD}() where {CD <: CatDesc}
-    AttributedCSet{CD,typeof(AttrDesc(CD())),Tuple{}}()
-  end
-  function AttributedCSet{CD,AD,Ts,Idxed,TT,IT}() where
-    {CD <: CatDesc, AD <: AttrDesc{CD}, Ts <: Tuple,Idxed,TT <: NamedTuple,IT <: NamedTuple}
-    AttributedCSet{CD,AD,Ts,Idxed}()
-  end
-  function AttributedCSet{CD,AD,Ts,Idxed,TT,IT}(tables::TT,indices::IT) where
-    {CD <: CatDesc, AD <: AttrDesc{CD}, Ts <: Tuple,Idxed,TT <: NamedTuple,IT <: NamedTuple}
-    new{CD,AD,Ts,Idxed,TT,IT}(tables,indices)
-  end
+See also: [`AbstractCSetType`](@ref).
+"""
+function CSetType(pres::Presentation{Category}; kw...)
+  CSet{CSetTypeParams(pres; kw...)...}
 end
 
-const ACSet = AttributedCSet
+function CSetTypeParams(pres::Presentation{Category};
+                        data=(), index=(), unique_index=())
+  obs, homs = generators(pres, :Ob), generators(pres, :Hom)
+  data_obs, obs = separate(ob -> nameof(ob) ∈ data, obs)
+  data_homs, homs = separate(hom -> codom(hom) ∈ data_obs, homs)
+  data_index, index = separate(name -> name ∈ nameof.(data_homs),
+                               sort!(index ∪ unique_index))
+  ob_num = ob -> findfirst(obs .== ob)::Int
+  (Tuple(nameof.(obs)), Tuple(nameof.(homs)),
+   Tuple(@. ob_num(dom(homs))), Tuple(@. ob_num(codom(homs))),
+   Tuple(nameof.(data_homs)), Tuple(@. ob_num(dom(data_homs))),
+   Tuple(index), Tuple(data_index), Tuple(k ∈ unique_index for k in data_index))
+end
+separate(f, a::AbstractArray) = (i = f.(a); (a[i], a[.!i]))
 
-const AbstractCSet{CD} = AbstractACSet{CD,AttrDesc{CD,(),(),(),()},Tuple{}}
-const CSet{CD,Idxed} = ACSet{CD,AttrDesc{CD,(),(),(),()},Tuple{},Idxed}
+function Base.:(==)(x1::T, x2::T) where T <: CSet
+  # The subpart and data indices are redundant, so need not be compared.
+  x1.nparts == x2.nparts && x1.subparts == x2.subparts && x1.data == x2.data
+end
 
-function make_indices(::Type{CD},AD::Type{<:AttrDesc{CD}},Ts::Type{<:Tuple},Idxed::Tuple) where {CD}
-  ts = Ts.parameters
-  function make_idx(name)
-    if name ∈ CD.hom
-      Vector{Vector{Int}}()
-    elseif name ∈ AD.attr
-      Dict{ts[codom_num(AD,name)],Vector{Int}}()
-    else
-      error("invalid indices parameter")
-    end
-  end
-  NamedTuple{Idxed}(Tuple(make_idx(name) for name in Idxed))
+function Base.copy(cset::T) where T <: CSet
+  T(cset.nparts, map(copy, cset.subparts), map(copy, cset.indices),
+    map(copy, cset.data), map(copy, cset.data_indices))
 end
 
-function make_tables(::Type{CD}, AD::Type{<:AttrDesc{CD}}, Ts::Type{<:Tuple}) where {CD}
-  ts = Ts.parameters
-  cols = NamedTuple{CD.ob}(Tuple{Symbol,DataType}[] for ob in CD.ob)
-  for hom in CD.hom
-    push!(cols[dom(CD,hom)], (hom,Int))
+Base.empty(cset::T) where T <: CSet = T(map(eltype, cset.data))
+
+function Base.show(io::IO, mime::MIME"text/plain",
+                   a::AbstractCSet{Ob,Hom,Dom,Codom,Data,DataDom}) where
+    {Ob,Hom,Dom,Codom,Data,DataDom}
+  println(io, "CSet")
+  for ob in Ob
+    println(io, "  $ob = 1:$(nparts(a,ob))")
   end
-  for attr in AD.attr
-    push!(cols[dom(AD,attr)], (attr,ts[codom_num(AD,attr)]))
+  for (i, hom) in enumerate(Hom)
+    println(io, "  $hom : $(Ob[Dom[i]]) → $(Ob[Codom[i]])")
+    println(io, "    $(subpart(a,hom))")
   end
-  map(cols) do col
-    SA = StructArray{NamedTuple{Tuple(first.(col)),Tuple{last.(col)...}}}
-    make_struct_array(SA, undef, 0)
+  for (i, data) in enumerate(Data)
+    println(io, "  $data : $(Ob[DataDom[i]]) → $(eltype(subpart(a,data)))")
+    println(io, "    $(subpart(a,data))")
   end
 end
 
-function Base.:(==)(x1::T, x2::T) where T <: ACSet
-  x1.tables == x2.tables
-end
-
-function Base.copy(acs::T) where T <: ACSet
-  T(map(copy, acs.tables),map(copy, acs.indices))
-end
-
-Base.empty(acs::T) where T <: ACSet = T()
+# C-set interface
+#################
 
-nparts(acs::ACSet,type::Symbol) = length(acs.tables[type])
+""" Number of parts of given type in a C-set.
+"""
+nparts(cset::CSet, type::Symbol) = cset.nparts[type]
 
-""" Whether a C-set has a subpart with the given name.
+""" Whether a C-set has a part with the given name.
 """
-has_subpart(acs::ACSet, name::Symbol) = _has_subpart(acs, Val(name))
+has_part(cset::CSet, type::Symbol) = _has_part(cset, Val(type))
 
-@generated function _has_subpart(::ACSet{CD,AD}, ::Val{name}) where {CD,AD,name}
-  name ∈ CD.hom || name ∈ AD.attr
-end
+@generated _has_part(cset::CSet{obs}, ::Val{type}) where {obs,type} =
+  type ∈ obs
 
-has_part(acs::ACSet,type::Symbol,part::Int) = 1 <= part <= nparts(acs,type)
+has_part(cset::CSet, type::Symbol, part::Int) = 1 <= part <= nparts(cset, type)
+has_part(cset::CSet, type::Symbol, part::AbstractVector{Int}) =
+  let n=nparts(cset, type); [ 1 <= x <= n for x in part ] end
 
-subpart(acs::ACSet, part, name::Symbol) = subpart(acs,name)[part]
-subpart(acs::ACSet, name::Symbol) = _subpart(acs,Val(name))
+""" Get subpart of part in C-set.
 
-@generated function _subpart(acs::ACSet{CD,AD,Ts},::Val{name}) where {CD,AD,Ts,name}
-  if name ∈ CD.hom
-    :(acs.tables.$(dom(CD,name)).$name)
-  elseif name ∈ AD.attr
-    :(acs.tables.$(dom(AD,name)).$name)
+Both single and vectorized access are supported.
+"""
+subpart(cset::CSet, part, name::Symbol) = subpart(cset, name)[part]
+subpart(cset::CSet, name::Symbol) = _subpart(cset, Val(name))
+
+@generated function _subpart(cset::T, ::Val{name}) where
+    {name, obs,homs,doms,codoms,data, T <: CSet{obs,homs,doms,codoms,data}}
+  if name ∈ homs
+    :(cset.subparts.$name)
+  elseif name ∈ data
+    :(cset.data.$name)
   else
     throw(KeyError(name))
   end
 end
 
-function Base.show(io::IO, ::MIME"text/plain", acs::AbstractACSet{CD,AD,Ts}) where {CD,AD,Ts}
-  println(io,"ACSet")
-  for ob in CD.ob
-    println(io, "  $ob = 1:$(nparts(acs,ob))")
-  end
-  for (i,hom) in enumerate(CD.hom)
-    println(io, "  $hom : $(dom(CD,i)) → $(codom(CD,i))")
-    println(io, "    $(subpart(acs,hom))")
-  end
-  for (i,conc) in enumerate(AD.data)
-    println(io, "  $conc = $(Ts.parameters[i])")
-  end
-  for (i,attr) in enumerate(AD.attr)
-    println(io, "  $attr : $(dom(AD,i)) ⇒ $(codom(AD,i))")
-    println(io, "    $(subpart(acs,attr))")
-  end
-end
-
-""" Insert into sorted vector, preserving the sorting.
-"""
-function insertsorted!(a::AbstractVector, x)
-  insert!(a, searchsortedfirst(a, x), x)
-end
-
-""" Delete one occurrence of value from sorted vector.
+""" Whether a C-set has a subpart with the given name.
 """
-function deletesorted!(a::AbstractVector, x)
-  i = searchsortedfirst(a, x)
-  @assert i <= length(a) && a[i] == x "Value $x is not contained in $a"
-  deleteat!(a, i)
-end
+has_subpart(cset::CSet, name::Symbol) = _has_subpart(cset, Val(name))
 
-add_part!(acs::ACSet, type::Symbol, subparts::NamedTuple) =
-  _add_parts!(acs, Val(type), make_struct_array([subparts]))[1]
-add_part!(acs::ACSet, type::Symbol; kw...) = add_part!(acs, type, (; kw...))
-
-add_parts!(acs::ACSet, type::Symbol, subpartses::StructArray0{<:NamedTuple}) =
-  _add_parts!(acs, Val(type), subpartses)
-add_parts!(acs::ACSet, type::Symbol; kw...) =
-  add_parts!(acs, type, make_struct_array((; kw...)))
-add_parts!(acs::ACSet,type::Symbol, n::Int; kw...) =
-  add_parts!(acs, type, make_struct_array(n, (; kw...)))
-
-@generated function _add_parts!(acs::ACSet{CD,AD,Ts,Idxed,TT},::Val{ob},
-                                subpartses::T) where
-  {CD,AD,Ts,Idxed,TT,ob,T<:StructArray0{<:NamedTuple}}
-  @assert fieldnames(T) == fieldnames(fieldtype(TT,ob))
-  @assert fieldtypes(T) == fieldtypes(fieldtype(TT,ob))
-  code = quote
-    append!(acs.tables.$ob,subpartses)
-  end
-  for (i,hom) in enumerate(CD.hom)
-    if hom ∈ Idxed && CD.ob[CD.codom[i]] == ob
-      push!(code.args, :(append!(acs.indices.$(hom), repeat([[]],length(subpartses)))))
-    end
-  end
-  inner_loop = Expr(:block)
-  for hom in fieldnames(T)
-    if hom ∈ Idxed
-      if hom ∈ CD.hom
-        indices_code = quote
-          if has_part(acs, $(Expr(:quote, codom(CD,hom))), subparts.$hom)
-            insertsorted!(acs.indices.$(hom)[subparts.$hom],part_num)
-          elseif subparts.$hom != 0
-            error("No part $(subparts.$hom) exists of type $($(Expr(:quote, codom(CD,hom))))")
-          end
-        end
-        push!(inner_loop.args, indices_code)
-      elseif hom ∈ AD.attr
-        indices_code = quote
-          if subparts.$hom ∈ keys(acs.indices.$hom)
-            insertsorted!(acs.indices.$(hom)[subparts.$hom],part_num)
-          else
-            acs.indices.$(hom)[subparts.$hom] = [part_num]
-          end
-        end
-        push!(inner_loop.args, indices_code)
-      end
-    end
-  end
-  quote
-    k = length(subpartses)
-    n = nparts(acs,ob)
-    $code
-    for (i,subparts) in enumerate(subpartses)
-      part_num = i+n
-      $inner_loop
-    end
-    (n+1):(n+k)
-  end
+@generated function _has_subpart(cset::T, ::Val{name}) where
+    {name, obs,homs,doms,codoms,data, T <: CSet{obs,homs,doms,codoms,data}}
+  name ∈ homs || name ∈ data
 end
 
-incident(acs::ACSet, part, name::Symbol) = _incident(acs, part, Val(name))
-
-get_data_index(d::AbstractDict{K,Int}, k::K) where K =
-  get(d, k, nothing)
-get_data_index(d::AbstractDict{K,<:AbstractVector{Int}}, k::K) where K =
-  get(d, k, 1:0)
-
-@generated function _incident(acs::ACSet{CD,AD,Ts,Idxed}, part, ::Val{name}) where {CD,AD,Ts,Idxed,name}
-  if name ∈ Idxed
-    if name ∈ CD.hom
-      :(acs.indices.$name[part])
-    else
-      :(get_data_index.(Ref(acs.indices.$name),part))
-    end
+""" Get superparts incident to part in C-set.
+"""
+incident(cset::CSet, part, name::Symbol) = _incident(cset, part, Val(name))
+
+@generated function _incident(cset::T, part, ::Val{name}) where
+    {name, obs,homs,doms,codoms,data,data_doms,indexed,data_indexed,
+     T <: CSet{obs,homs,doms,codoms,data,data_doms,indexed,data_indexed}}
+  if name ∈ indexed
+    :(cset.indices.$name[part])
+  elseif name ∈ data_indexed
+    :(get_data_index.(Ref(cset.data_indices.$name), part))
   else
     throw(KeyError(name))
   end
 end
 
-@generated function _set_subpart!(acs::ACSet{CD,AD,Ts,Idxed}, part::Int, ::Val{name}, subpart) where
-  {CD,AD,Ts,Idxed,name}
-  code = Expr(:block)
-  if name ∈ CD.hom
-    push!(code.args, quote
-          old = acs.tables.$(dom(CD,name)).$(name)[part]
-          acs.tables.$(dom(CD,name)).$name[part] = subpart
-          end)
-  elseif name ∈ AD.attr
-    push!(code.args, quote
-          old = acs.tables.$(dom(AD,name)).$(name)[part]
-          acs.tables.$(dom(AD,name)).$name[part] = subpart
-          end)
-  else
-    throw(KeyError(name))
-  end
-  if name ∈ Idxed
-    if name ∈ CD.hom
-      indices_code = quote
-        @assert has_part(acs,$(Expr(:quote, codom(CD,name))),subpart)
-        if old != 0
-          deletesorted!(acs.indices.$(name)[old], part)
-        end
-        insertsorted!(acs.indices.$(name)[subpart], part)
-      end
-      push!(code.args, indices_code)
-    elseif name ∈ AD.attr
-      indices_code = quote
-        @assert pop!(acs.indices.$name, old) == part
-        insertsorted!(get!(acs.indices.$name, subpart) do; Int[] end, part)
-      end
-      push!(code.args, indices_code)
-    end
-  end
-  code
-end
+""" Add part of given type to C-set, optionally setting its subparts.
 
-set_subpart!(acs::ACSet, part::Int, name, subpart) = _set_subpart!(acs, part, Val(name), subpart)
+Returns the ID of the added part.
 
-function set_subpart!(acs::ACSet, parts::AbstractArray{Int}, name, subparts::AbstractArray)
-  for (part,subpart) in zip(parts,subparts)
-    set_subpart!(acs,part,name,subpart)
-  end
+See also: [`add_parts!`](@ref).
+"""
+function add_part!(cset::CSet, type::Symbol, args...; kw...)
+  part = only(_add_parts!(cset, Val(type), 1))
+  set_subparts!(cset, part, args...; kw...)
+  part
 end
 
-## FIXME: Put a more specific type on this
-function set_subpart!(acs::ACSet, parts::AbstractArray{Int}, name, subpart)
-  for part in parts
-    set_subpart!(acs,part,name,subpart)
-  end
-end
+""" Add parts of given type to C-set, optionally setting their subparts.
 
-function set_subpart!(acs::ACSet{CD,AD}, parts::Colon, name, subparts) where {CD,AD}
-  part_type = if name ∈ CD.hom
-    dom(CD,name)
-  elseif name ∈ AD.attr
-    dom(AD,name)
-  else
-    error("No such subpart: $name")
-  end
-  set_subpart!(acs,1:nparts(acs,part_type),name,subparts)
+Returns the range of IDs for the added parts.
+
+See also: [`add_part!`](@ref).
+"""
+function add_parts!(cset::CSet, type::Symbol, n::Int, args...; kw...)
+  parts = _add_parts!(cset, Val(type), n)
+  set_subparts!(cset, parts, args...; kw...)
+  parts
 end
 
-function set_subparts!(acs::ACSet, part, subparts)
-  for (name, subpart) in pairs(subparts)
-    set_subpart!(acs, part, name, subpart)
+@generated function _add_parts!(cset::T, ::Val{type}, n::Int) where
+    {type, obs,homs,doms,codoms,data,data_doms,indexed,
+     T <: CSet{obs,homs,doms,codoms,data,data_doms,indexed}}
+  ob_num = findfirst(obs .== type)::Int
+  in_homs = [ homs[i] for (i, codom) in enumerate(codoms) if codom == ob_num ]
+  out_homs = [ homs[i] for (i, dom) in enumerate(doms) if dom == ob_num ]
+  data_homs = [ data[i] for (i, dom) in enumerate(data_doms) if dom == ob_num ]
+  indexed_homs = filter(hom -> hom ∈ indexed, in_homs)
+  # Question: The three loops could (should?) be unrolled. Or is the Julia
+  # compiler smart enough to do this on its own?
+  quote
+    if n == 0; return 1:0 end
+    nparts = cset.nparts.$type + n
+    cset.nparts = SLVector(cset.nparts; $type=nparts)
+    start = nparts - n + 1
+    for name in $(Tuple(out_homs))
+      resize!(cset.subparts[name], nparts)
+      @inbounds cset.subparts[name][start:nparts] .= 0
+    end
+    for name in $(Tuple(indexed_homs))
+      index = cset.indices[name]
+      resize!(index, nparts)
+      @inbounds for part in start:nparts; index[part] = Int[] end
+    end
+    for name in $(Tuple(data_homs))
+      resize!(cset.data[name], nparts)
+      @inbounds cset.data[name][start:nparts] .= missing
+    end
+    start:nparts
   end
 end
 
 """ Copy parts from one C-set into another.
+
 All subparts among the selected parts, including any attached data, are
 preserved. Thus, if the selected parts form a sub-C-set, then the whole
 sub-C-set is preserved. On the other hand, if the selected parts do *not* form a
 sub-C-set, then some copied parts will have undefined subparts.
 """
-copy_parts!(acs::ACSet, from::ACSet; kw...) =
-  copy_parts!(acs, from, (; kw...))
-copy_parts!(acs::ACSet, from::ACSet, type::Symbol) =
-  copy_parts!(acs, from, (; type => :))
-copy_parts!(acs::ACSet, from::ACSet, type::Symbol, parts) =
-  copy_parts!(acs, from, (; type => parts))
-copy_parts!(acs::ACSet, from::ACSet, types::Tuple) =
-  copy_parts!(acs::ACSet, from::ACSet, NamedTuple{types}((:) for t in types))
-
-function copy_parts!(acs::ACSet, from::ACSet, parts::NamedTuple{types}) where types
+copy_parts!(cset::CSet, from::CSet; kw...) =
+  copy_parts!(cset, from, (; kw...))
+copy_parts!(cset::CSet, from::CSet, type::Symbol) =
+  copy_parts!(cset, from, (; type => :))
+copy_parts!(cset::CSet, from::CSet, type::Symbol, parts) =
+  copy_parts!(cset, from, (; type => parts))
+copy_parts!(cset::CSet, from::CSet, types::Tuple) =
+  copy_parts!(cset::CSet, from::CSet, NamedTuple{types}((:) for t in types))
+
+function copy_parts!(cset::CSet, from::CSet, parts::NamedTuple{types}) where types
   parts = map(types, parts) do type, part
     part == (:) ? (1:nparts(from, type)) : part
   end
-  _copy_parts!(acs, from, NamedTuple{types}(parts))
+  _copy_parts!(cset, from, NamedTuple{types}(parts))
 end
 
-@generated function _copy_parts!(acs::T, from::T, parts::NamedTuple{types}) where
-    {types,CD,AD,Ts,Idx,T <: ACSet{CD,AD,Ts,Idx}}
-  obnums = ob_num.(CD, types)
+@generated function _copy_parts!(cset::T, from::T, parts::NamedTuple{types}) where
+    {types, obs,homs,doms,codoms, T <: CSet{obs,homs,doms,codoms}}
+  obnums = [ findfirst(obs .== type)::Int for type in types ]
   in_obs, out_homs = Symbol[], Tuple{Symbol,Symbol,Symbol}[]
-  for (hom, dom, codom) in zip(CD.hom, CD.dom, CD.codom)
+  for (hom, dom, codom) in zip(homs, doms, codoms)
     if dom ∈ obnums && codom ∈ obnums
-      push!(in_obs, CD.ob[codom])
-      push!(out_homs, (hom, CD.ob[dom], CD.ob[codom]))
+      push!(in_obs, obs[codom])
+      push!(out_homs, (hom, obs[dom], obs[codom]))
     end
   end
   in_obs = Tuple(unique!(in_obs))
   quote
     newparts = NamedTuple{$types}(tuple($(map(types) do type
-      :(_copy_parts_data!(acs, from, Val($(QuoteNode(type))), parts.$type))
+      :(_copy_parts_data!(cset, from, Val($(QuoteNode(type))), parts.$type))
     end...)))
     partmaps = NamedTuple{$in_obs}(tuple($(map(in_obs) do type
       :(Dict{Int,Int}(zip(parts.$type, newparts.$type)))
@@ -377,7 +298,7 @@ end
         q = subpart(from, p, name)
         newq = get(partmaps[codom], q, nothing)
         if !isnothing(newq)
-          set_subpart!(acs, newp, name, newq)
+          set_subpart!(cset, newp, name, newq)
         end
       end
     end
@@ -385,22 +306,157 @@ end
   end
 end
 
-@generated function _copy_parts_data!(acs::T, from::T, ::Val{type}, parts) where
-  {type,CD,AD,T<:ACSet{CD,AD}}
-  homs = collect(filter(hom -> dom(CD,hom) == type, CD.hom))
-  attrs = collect(filter(attr -> dom(AD,attr) == type, AD.attr))
-  subparts = [[Expr(:kw, hom, :(zeros(Int64,n))) for hom in homs];
-              [Expr(:kw, attr, :(subpart(from,parts,$(Expr(:quote, attr))))) for attr in attrs]]
+""" Copy parts of a single type from one C-set to another.
+
+Any data attached to the parts is also copied.
+"""
+@generated function _copy_parts_data!(cset::T, from::T, ::Val{type}, parts) where
+    {type, obs,homs,doms,codoms,data,data_doms,
+     T <: CSet{obs,homs,doms,codoms,data,data_doms}}
+  obnum = findfirst(obs .== type)::Int
+  data_homs = [ data[i] for (i, dom) in enumerate(data_doms) if dom == obnum ]
   quote
-    n = length(parts)
-    $(Expr(:call, :add_parts!, Expr(:parameters, subparts...), :acs, Expr(:quote, type), :n))
+    newparts = add_parts!(cset, $(QuoteNode(type)), length(parts))
+    for name in $(Tuple(data_homs))
+      set_subpart!(cset, newparts, name, from.data[name][parts])
+    end
+    newparts
+  end
+end
+
+""" Mutate subpart of a part in a C-set.
+
+Both single and vectorized assignment are supported.
+
+See also: [`set_subparts!`](@ref).
+"""
+set_subpart!(cset::CSet, part::Int, name::Symbol, subpart) =
+  _set_subpart!(cset, part, Val(name), subpart)
+
+function set_subpart!(cset::CSet, part::AbstractVector{Int},
+                      name::Symbol, subpart)
+  broadcast(part, subpart) do part, subpart
+    _set_subpart!(cset, part, Val(name), subpart)
   end
 end
 
-function disjoint_union(acs1::T,acs2::T) where {CD,AD,T<:ACSet{CD,AD}}
-  acs = copy(acs1)
-  copy_parts!(acs,acs2,CD.ob)
-  acs
+set_subpart!(cset::CSet, ::Colon, name::Symbol, subpart) =
+  set_subpart!(cset, name, subpart)
+set_subpart!(cset::CSet, name::Symbol, new_subpart) =
+  set_subpart!(cset, 1:length(subpart(cset, name)), name, new_subpart)
+
+@generated function _set_subpart!(cset::T, part::Int, ::Val{name}, subpart) where
+    {name, obs,homs,doms,codoms,data,data_doms,indexed,data_indexed,
+     T <: CSet{obs,homs,doms,codoms,data,data_doms,indexed,data_indexed}}
+  if name ∈ homs
+    codom = obs[codoms[findfirst(name .== homs)]]
+  end
+  if name ∈ indexed
+    quote
+      @assert 0 <= subpart <= cset.nparts.$codom
+      old = cset.subparts.$name[part]
+      cset.subparts.$name[part] = subpart
+      if old > 0
+        deletesorted!(cset.indices.$name[old], part)
+      end
+      if subpart > 0
+        insertsorted!(cset.indices.$name[subpart], part)
+      end
+    end
+  elseif name ∈ homs
+    quote
+      @assert 0 <= subpart <= cset.nparts.$codom
+      cset.subparts.$name[part] = subpart
+    end
+  elseif name ∈ data_indexed
+    quote
+      old = cset.data.$name[part]
+      cset.data.$name[part] = subpart
+      if !ismissing(old)
+        unset_data_index!(cset.data_indices.$name, old, part)
+      end
+      if !ismissing(subpart)
+        set_data_index!(cset.data_indices.$name, subpart, part)
+      end
+    end
+  elseif name ∈ data
+    :(cset.data.$name[part] = subpart)
+  else
+    throw(KeyError(name))
+  end
+end
+
+""" Mutate subparts of a part in a C-set.
+
+Both single and vectorized assignment are supported.
+
+See also: [`set_subpart!`](@ref).
+"""
+set_subparts!(cset::CSet, part; kw...) = set_subparts!(cset, part, (; kw...))
+
+function set_subparts!(cset::CSet, part, subparts)
+  for (name, subpart) in pairs(subparts)
+    set_subpart!(cset, part, name, subpart)
+  end
+end
+
+""" Look up key in C-set data index.
+"""
+get_data_index(d::AbstractDict{K,Int}, k::K) where K =
+  get(d, k, nothing)
+get_data_index(d::AbstractDict{K,<:AbstractVector{Int}}, k::K) where K =
+  get(d, k, 1:0)
+
+""" Set key and value for C-set data index.
+"""
+function set_data_index!(d::AbstractDict{K,Int}, k::K, v::Int) where K
+  if haskey(d, k)
+    error("Key $k already defined in unique index")
+  end
+  d[k] = v
+end
+function set_data_index!(d::AbstractDict{K,<:AbstractVector{Int}},
+                         k::K, v::Int) where K
+  insertsorted!(get!(d, k) do; Int[] end, v)
+end
+
+""" Unset key and value from C-set data index.
+"""
+function unset_data_index!(d::AbstractDict{K,Int}, k::K, v::Int) where K
+  @assert pop!(d, k) == v
+end
+function unset_data_index!(d::AbstractDict{K,<:AbstractVector{Int}},
+                           k::K, v::Int) where K
+  vs = d[k]
+  deletesorted!(vs, v)
+  if isempty(vs)
+    delete!(d, k)
+  end
+end
+
+""" Insert into sorted vector, preserving the sorting.
+"""
+function insertsorted!(a::AbstractVector, x)
+  insert!(a, searchsortedfirst(a, x), x)
+end
+
+""" Delete one occurrence of value from sorted vector.
+"""
+function deletesorted!(a::AbstractVector, x)
+  i = searchsortedfirst(a, x)
+  @assert i <= length(a) && a[i] == x "Value $x is not contained in $a"
+  deleteat!(a, i)
+end
+
+# Limits and colimits
+#####################
+
+# TODO: Implement (co)limits of C-sets using generic (co)limits interface.
+
+function disjoint_union(cset1::T, cset2::T) where {obs, T <: CSet{obs}}
+  cset = copy(cset1)
+  copy_parts!(cset, cset2, obs)
+  cset
 end
 
 end
diff --git a/src/categorical_algebra/CategoricalAlgebra.jl b/src/categorical_algebra/CategoricalAlgebra.jl
index eae634d83..153e80973 100644
--- a/src/categorical_algebra/CategoricalAlgebra.jl
+++ b/src/categorical_algebra/CategoricalAlgebra.jl
@@ -1,19 +1,11 @@
 module CategoricalAlgebra
 
-using Reexport
-
-include("CSets.jl")
-include("Graphs.jl")
-include("FreeDiagrams.jl")
-include("Limits.jl")
+include("ShapeDiagrams.jl")
 include("Matrices.jl")
 include("FinSets.jl")
 include("FinRelations.jl")
 include("Permutations.jl")
-include("PredicatedSets.jl")
-
-@reexport using .FreeDiagrams
-@reexport using .Limits
-@reexport using .CSets
+include("CSets.jl")
+include("Graphs.jl")
 
 end
diff --git a/src/categorical_algebra/FinRelations.jl b/src/categorical_algebra/FinRelations.jl
index 4bdc8d78d..3f244b1f6 100644
--- a/src/categorical_algebra/FinRelations.jl
+++ b/src/categorical_algebra/FinRelations.jl
@@ -6,7 +6,6 @@ export BoolRig, FinRel, FinRelation, FinRelationCallable, FinRelationMatrix,
 
 import Base: +, *
 using AutoHashEquals
-import FunctionWrappers: FunctionWrapper
 
 using ...GAT
 using ...Theories: DistributiveBicategoryRelations
@@ -45,12 +44,10 @@ Base.show(io::IO, x::BoolRig) = print(io, x.value)
 
 See also: `FinSet`.
 """
-@auto_hash_equals struct FinRel{S,T} <: AbstractSetOb{S,T}
+@auto_hash_equals struct FinRel{S} <: AbstractSetOb{S}
   set::S
 end
 
-FinRel(i::Int) = FinRel{Int,Int}(i)
-FinRel(set::S) where {T,S<:AbstractSet{T}} = FinRel{S,T}(set)
 iterable(s::FinRel{Int}) = 1:s.set
 iterable(s::FinRel{<:AbstractSet}) = s.set
 
@@ -61,30 +58,28 @@ represented implicitly by an arbitrary Julia function mapping pairs of elements
 to booleans or explicitly by a matrix (dense or sparse) taking values in the rig
 of booleans ([`BoolRig`](@ref)).
 """
-abstract type FinRelation{S,T} end
+abstract type FinRelation{S} end
 
 FinRelation(R::Function, args...) = FinRelationCallable(R, args...)
 FinRelation(R::AbstractMatrix, args...) = FinRelationMatrix(R, args...)
 
 """ Relation in FinRel defined by a callable Julia object.
 """
-@auto_hash_equals struct FinRelationCallable{S,T} <: FinRelation{S,T}
-  rel::FunctionWrapper{Bool,Tuple{T,T}} # Usually `Function` but can be any Julia callable.
-  dom::FinRel{S,T}
-  codom::FinRel{S,T}
-  FinRelationCallable(R::Function, dom::Int, codom::Int) =
-    new{Int,Int}(FunctionWrapper{Int,Tuple{Int,Int}}(R), FinRel(dom), FinRel(codom))
-  FinRelationCallable(R::Function, dom::FinRel{S,T}, codom::FinRel{S,T}) where {S,T} =
-    new{S,T}(FunctionWrapper{T,Tuple{T,T}}(R), dom, codom)
+@auto_hash_equals struct FinRelationCallable{S} <: FinRelation{S}
+  rel::Any # Usually `Function` but can be any Julia callable.
+  dom::FinRel{S}
+  codom::FinRel{S}
 end
+FinRelationCallable(R::Function, dom::Int, codom::Int) =
+  FinRelationCallable(R, FinRel(dom), FinRel(codom))
 
-(R::FinRelationCallable{S,T})(x::T, y::T) where {S,T} = R.rel(x, y)
+(R::FinRelationCallable)(x, y) = R.rel(x, y)
 
 """ Relation in FinRel represented by a boolean matrix.
 
 Boolean matrices are also known as logical matrices or relation matrices.
 """
-@auto_hash_equals struct FinRelationMatrix{T<:AbstractMatrix{BoolRig}} <: FinRelation{Int,Int}
+@auto_hash_equals struct FinRelationMatrix{T<:AbstractMatrix{BoolRig}} <: FinRelation{Int}
   rel::T
 end
 
@@ -118,7 +113,7 @@ FIXME: Many methods only work for `FinRel{Int}`. The default methods should
 assume `FinRel{<:AbstractSet}` and the case `FinRel{Int}` should be handled
 specially.
 """
-@instance DistributiveBicategoryRelations{FinRel, FinRelation} begin
+@instance DistributiveBicategoryRelations(FinRel, FinRelation) begin
   dom(R::FinRelation) = R.dom
   codom(R::FinRelation) = R.codom
 
diff --git a/src/categorical_algebra/FinSets.jl b/src/categorical_algebra/FinSets.jl
index c86d3b0da..2eb3b3297 100644
--- a/src/categorical_algebra/FinSets.jl
+++ b/src/categorical_algebra/FinSets.jl
@@ -1,19 +1,18 @@
 """ Computing in the category of finite sets and functions.
 """
 module FinSets
-export FinSet, FinFunction, FinFunctionCallable, FinFunctionVector, force
+export FinSet, FinFunction, FinFunctionCallable, FinFunctionVector, force,
+  terminal, product, equalizer, pullback, limit,
+  initial, coproduct, coequalizer, pushout, colimit
 
 using AutoHashEquals
 using DataStructures: IntDisjointSets, union!, find_root
-import FunctionWrappers: FunctionWrapper
-using StaticArrays: StaticVector, SVector, @SVector
 
 using ...GAT
 using ...Theories: Category
-import ...Theories: dom, codom, id, compose, ⋅, ∘
-using ..FreeDiagrams, ..Limits
-import ..Limits: terminal, product, equalizer, pullback, limit,
-  initial, coproduct, coequalizer, pushout, colimit
+import ...Theories: dom, codom, id, compose, ⋅, ∘,
+  terminal, product, equalizer, initial, coproduct, coequalizer
+using ..ShapeDiagrams
 
 # Category of finite sets
 #########################
@@ -23,7 +22,7 @@ import ..Limits: terminal, product, equalizer, pullback, limit,
 An implementation detail used to control method dispatch. Concrete subtypes
 include `FinSet` and `FinRel`.
 """
-abstract type AbstractSetOb{S,T} end
+abstract type AbstractSetOb{S} end
 
 Base.eltype(::Type{AbstractSetOb{S}}) where S = eltype(S)
 Base.iterate(s::AbstractSetOb, args...) = iterate(iterable(s), args...)
@@ -37,12 +36,10 @@ functions, through types `FinSet{S} where S <: AbstractSet`, as well as the
 skeleton of this category, through the type `FinSet{Int}`. In the latter case,
 the object `FinSet(n)` represents the set {1,...,n}.
 """
-@auto_hash_equals struct FinSet{S,T} <: AbstractSetOb{S,T}
+@auto_hash_equals struct FinSet{S} <: AbstractSetOb{S}
   set::S
 end
 
-FinSet(n::Int) = FinSet{Int,Int}(n)
-FinSet(set::S) where {T,S<:AbstractSet{T}} = FinSet{S,T}(set)
 iterable(s::FinSet{Int}) = 1:s.set
 iterable(s::FinSet{<:AbstractSet}) = s.set
 
@@ -53,7 +50,7 @@ case it is evaluated lazily, or explictly by a vector of integers. In the latter
 case, the function (1↦1, 2↦3, 3↦2, 4↦3), for example, is represented by the
 vector [1,3,2,3].
 """
-abstract type FinFunction{S,T} end
+abstract type FinFunction{S} end
 
 FinFunction(f::Function, args...) = FinFunctionCallable(f, args...)
 FinFunction(f::AbstractVector, args...) = FinFunctionVector(f, args...)
@@ -62,15 +59,13 @@ FinFunction(f::AbstractVector, args...) = FinFunctionVector(f, args...)
 
 To be evaluated lazily unless forced.
 """
-@auto_hash_equals struct FinFunctionCallable{S,T} <: FinFunction{S,T}
-  func::FunctionWrapper{T,Tuple{T}} # Usually `Function` but can be any Julia callable.
-  dom::FinSet{S,T}
-  codom::FinSet{S,T}
+@auto_hash_equals struct FinFunctionCallable{S} <: FinFunction{S}
+  func::Any # Usually `Function` but can be any Julia callable.
+  dom::FinSet{S}
+  codom::FinSet{S}
 end
 FinFunctionCallable(f::Function, dom::Int, codom::Int) =
-  FinFunctionCallable(FunctionWrapper{Int,Tuple{Int}}(f), FinSet(dom), FinSet(codom))
-FinFunctionCallable(f::Function, dom::FinSet{S,T}, codom::FinSet{S,T}) where {S,T} =
-  FinFunctionCallable(FunctionWrapper{T,Tuple{T}}(f),dom,codom)
+  FinFunctionCallable(f, FinSet(dom), FinSet(codom))
 
 (f::FinFunctionCallable)(x) = f.func(x)
 
@@ -78,7 +73,7 @@ FinFunctionCallable(f::Function, dom::FinSet{S,T}, codom::FinSet{S,T}) where {S,
 
 The elements of the set are assumed to be {1,...,n}.
 """
-@auto_hash_equals struct FinFunctionVector{T<:AbstractVector{Int}} <: FinFunction{Int,Int}
+@auto_hash_equals struct FinFunctionVector{T<:AbstractVector{Int}} <: FinFunction{Int}
   func::T
   codom::Int
 end
@@ -108,7 +103,7 @@ Base.collect(f::FinFunction) = force(f).func
 
 """ Category of finite sets and functions.
 """
-@instance Category{FinSet, FinFunction} begin
+@instance Category(FinSet, FinFunction) begin
   dom(f::FinFunction) = f.dom
   codom(f::FinFunction) = f.codom
   
@@ -120,94 +115,104 @@ Base.collect(f::FinFunction) = force(f).func
   end
 end
 
-compose_impl(f::FinFunction{S,T}, g::FinFunction{S,T}) where {S,T} = g ∘ f
+compose_impl(f::FinFunction, g::FinFunction) = g ∘ f
 compose_impl(f::FinFunctionVector, g::FinFunctionVector) = g.func[f.func]
 
 # Limits
 ########
 
-function product(Xs::StaticVector{0,<:FinSet{Int}})
-  Limit(Xs, Multispan(FinSet(1), @SVector FinFunction{Int}[]))
-end
+terminal(::Type{FinSet{Int}}) = FinSet(1)
 
-function product(Xs::StaticVector{2,<:FinSet{Int}})
-  m, n = length.(Xs)
+function product(A::FinSet{Int}, B::FinSet{Int})
+  m, n = length(A), length(B)
   indices = CartesianIndices((m, n))
   π1 = FinFunction(i -> indices[i][1], m*n, m)
   π2 = FinFunction(i -> indices[i][2], m*n, n)
-  Limit(Xs, Span(π1, π2))
+  Span(π1, π2)
 end
 
-function product(Xs::AbstractVector{<:FinSet{Int}})
+function product(Xs::AbstractVector{FinSet{Int}})
   ns = length.(Xs)
   indices = CartesianIndices(tuple(ns...))
-  n = prod(ns)
-  πs = [FinFunction(i -> indices[i][j],n,ns[j]) for j in 1:length(ns)]
-  Limit(Xs, Multispan(FinSet(n), πs))
+  apex = prod(ns)
+  πs = [FinFunction(i -> indices[i][j],apex,ns[j]) for j in 1:length(ns)]
+  Cone(FinSet(apex), πs)
 end
 
-function equalizer(pair::ParallelPair{<:FinSet{Int}})
-  f, g = pair
-  m = length(dom(pair))
-  eq = FinFunction(filter(i -> f(i) == g(i), 1:m), m)
-  Limit(pair, Multispan(SVector(eq)))
+function equalizer(f::FinFunction{Int}, g::FinFunction{Int})
+  @assert dom(f) == dom(g) && codom(f) == codom(g)
+  m = length(dom(f))
+  FinFunction(filter(i -> f(i) == g(i), 1:m), m)
 end
 
-function equalizer(para::ParallelMorphisms{<:FinSet{Int}})
-  @assert length(para) >= 1
-  f1, frest = para[1], para[2:end]
-  m = length(dom(para))
-  eq = FinFunction(filter(i -> all(f1(i) == f(i) for f in frest), 1:m), m)
-  Limit(para, Multispan(SVector(eq)))
+function equalizer(fs::AbstractVector{<:FinFunction{Int}})
+  @assert length(fs) >= 1
+  f1 = fs[1]
+  frest = fs[2:end]
+  @assert all(dom(f) == dom(f1) && codom(f) == codom(f1) for f in frest)
+  m = length(dom(f1))
+  FinFunction(filter(i -> all(f1(i) == f(i) for f in frest), 1:m),m)
+end
+
+""" Pullback of cospan of functions between finite sets.
+
+TODO: This logic is completely generic. Make it independent of FinSet.
+"""
+function pullback(cospan::Cospan{<:FinFunction,<:FinFunction})
+  f, g = left(cospan), right(cospan)
+  prod = product(dom(f), dom(g))
+  π1, π2 = left(prod), right(prod)
+  eq = equalizer(π1⋅f, π2⋅g)
+  Span(eq⋅π1, eq⋅π2)
 end
 
-function limit(::Type{<:FinSet{Int}}, d::FreeDiagram)
-  p = product(ob(d))
-  n, leg = length(ob(p)), legs(p)
-  satisfy(e,x) = hom(d,e)(leg[src(d,e)](x)) == leg[tgt(d,e)](x)
-  f = FinFunction(filter(i -> all(satisfy(e,i) for e in edges(d)), 1:n), n)
-  Limit(d, Multispan(dom(f), [compose(f,leg[i]) for i in vertices(d)]))
+function limit(d::Diagram{FinSet{Int}, <:FinFunction{Int}})
+  p = product(d.obs)
+  n = length(apex(p))
+  satisfy((s,t,g),x) = g(leg(p,s)(x)) == leg(p,t)(x)
+  f = FinFunction(filter(i -> all(satisfy(h,i) for h in d.homs), 1:n), n)
+  Cone(dom(f),[compose(f,leg(p,i)) for i in 1:length(d.obs)])
 end
 
+
 # Colimits
 ##########
 
-function coproduct(Xs::StaticVector{0,<:FinSet{Int}})
-  Colimit(Xs, Multicospan(FinSet(0), @SVector FinFunction{Int}[]))
-end
+initial(::Type{FinSet{Int}}) = FinSet(0)
 
-function coproduct(Xs::StaticVector{2,<:FinSet{Int}})
-  m, n = length.(Xs)
+function coproduct(A::FinSet{Int}, B::FinSet{Int})
+  m, n = length(A), length(B)
   ι1 = FinFunction(1:m, m, m+n)
   ι2 = FinFunction(m+1:m+n, n, m+n)
-  Colimit(Xs, Cospan(ι1, ι2))
+  Cospan(ι1, ι2)
 end
 
-function coproduct(Xs::AbstractVector{<:FinSet{Int}})
+function coproduct(Xs::AbstractVector{FinSet{Int}})
   ns = length.(Xs)
-  n = sum(ns)
+  base = sum(ns)
   offsets = [0,cumsum(ns)...]
-  ιs = [FinFunction((1:ns[j]) .+ offsets[j],ns[j],n) for j in 1:length(ns)]
-  Colimit(Xs, Multicospan(FinSet(n), ιs))
+  πs = [FinFunction((1:ns[j]) .+ offsets[j],ns[j],base) for j in 1:length(ns)]
+  Cocone(FinSet(base), πs)
 end
 
-function coequalizer(pair::ParallelPair{<:FinSet{Int}})
-  f, g = pair
-  m, n = length(dom(pair)), length(codom(pair))
+function coequalizer(f::FinFunction{Int}, g::FinFunction{Int})
+  @assert dom(f) == dom(g) && codom(f) == codom(g)
+  m, n = length(dom(f)), length(codom(f))
   sets = IntDisjointSets(n)
   for i in 1:m
     union!(sets, f(i), g(i))
   end
   h = [ find_root(sets, i) for i in 1:n ]
   roots = unique!(sort(h))
-  coeq = FinFunction([ searchsortedfirst(roots, r) for r in h], length(roots))
-  Colimit(pair, Multicospan(SVector(coeq)))
+  FinFunction([ searchsortedfirst(roots, r) for r in h], length(roots))
 end
 
-function coequalizer(para::ParallelMorphisms{<:FinSet{Int}})
-  @assert length(para) >= 1
-  f1, frest = para[1], para[2:end]
-  m, n = length(dom(para)), length(codom(para))
+function coequalizer(fs::AbstractVector{<:FinFunction{Int}})
+  @assert length(fs) >= 1
+  f1 = fs[1]
+  frest = fs[2:end]
+  @assert all(dom(f) == dom(f1) && codom(f) == codom(f1) for f in frest)
+  m,n = length(dom(f1)), length(codom(f1))
   sets = IntDisjointSets(n)
   for i in 1:m
     for f in frest
@@ -216,25 +221,35 @@ function coequalizer(para::ParallelMorphisms{<:FinSet{Int}})
   end
   h = [ find_root(sets, i) for i in 1:n ]
   roots = unique!(sort(h))
-  coeq = FinFunction([searchsortedfirst(roots, r) for r in h], length(roots))
-  Colimit(para, Multicospan(SVector(coeq)))
+  FinFunction([searchsortedfirst(roots, r) for r in h], length(roots))
+end
+
+""" Pushout of span of functions between finite sets.
+
+TODO: This logic is completely generic. Make it independent of FinSet.
+"""
+function pushout(span::Span{<:FinFunction,<:FinFunction})
+  f, g = left(span), right(span)
+  coprod = coproduct(codom(f), codom(g))
+  ι1, ι2 = left(coprod), right(coprod)
+  coeq = coequalizer(f⋅ι1, g⋅ι2)
+  Cospan(ι1⋅coeq, ι2⋅coeq)
 end
 
-function colimit(::Type{<:FinSet{Int}}, d::FreeDiagram)
-  cp = coproduct(ob(d))
-  n, leg = length(ob(cp)), legs(cp)
+function colimit(d::Diagram{FinSet{Int}, <:FinFunction{Int}})
+  cp = coproduct(d.obs)
+  n = length(base(cp))
   sets = IntDisjointSets(n)
-  for e in edges(d)
-    s, t, h = src(d,e), tgt(d,e), hom(d,e)
-    for i in dom(h)
-      union!(sets, leg[s](i), leg[t](h(i)))
+  for (s,t,h) in d.homs
+    for i in d.obs[s]
+      union!(sets,leg(cp,s)(i),leg(cp,t)(h(i)))
     end
   end
   h = [ find_root(sets, i) for i in 1:n ]
   roots = unique!(sort(h))
   m = length(roots)
   f = FinFunction([searchsortedfirst(roots, r) for r in h], m)
-  Colimit(d, Multicospan(FinSet(m), [compose(leg[i],f) for i in vertices(d)]))
+  Cocone(FinSet(m),[compose(leg(cp,i),f) for i in 1:length(d.obs)])
 end
 
 end
diff --git a/src/categorical_algebra/FreeDiagrams.jl b/src/categorical_algebra/FreeDiagrams.jl
deleted file mode 100644
index 24af1bd0b..000000000
--- a/src/categorical_algebra/FreeDiagrams.jl
+++ /dev/null
@@ -1,227 +0,0 @@
-""" Free diagrams in a category.
-
-A [free diagram](https://ncatlab.org/nlab/show/free+diagram) in a category is a
-diagram whose shape is a free category. Examples include the empty diagram,
-discrete diagrams, parallel morphisms, spans, and cospans. Limits and colimits
-are most commonly taken over free diagrams.
-"""
-module FreeDiagrams
-export FreeDiagram, FixedFreeDiagram,
-  Span, Cospan, Multispan, Multicospan, ParallelPair, ParallelMorphisms,
-  ob, hom, dom, codom, apex, base, legs, left, right,
-  nv, ne, src, tgt, vertices, edges, has_vertex, has_edge,
-  add_vertex!, add_vertices!, add_edge!, add_edges!,
-  DecoratedCospan, AbstractFunctor, AbstractLaxator, LaxMonoidalFunctor,
-  decorator, decoration, undecorate
-
-using AutoHashEquals
-using StaticArrays: StaticVector, SVector
-
-import ...Theories: ob, hom, dom, codom, SchemaType
-using ...Present, ..CSets, ..Graphs
-using ..Graphs: TheoryGraph
-
-# Diagrams of fixed shape
-#########################
-
-""" Abstract type for free diagram of fixed shape.
-"""
-abstract type FixedFreeDiagram{Ob} end
-
-""" Multispan of morphisms in a category.
-
-A [multispan](https://ncatlab.org/nlab/show/multispan) is like a [`Span`](@ref)
-except that it may have a number of legs different than two. A colimit of this
-shape is a pushout.
-"""
-@auto_hash_equals struct Multispan{Ob,Legs<:AbstractVector} <: FixedFreeDiagram{Ob}
-  apex::Ob
-  legs::Legs
-end
-
-function Multispan(legs::AbstractVector)
-  @assert !isempty(legs) && allequal(dom.(legs))
-  Multispan(dom(first(legs)), legs)
-end
-
-""" Span of morphims in a category.
-
-A common special case of [`Multispan`](@ref). See also [`Cospan`](@ref).
-"""
-const Span{Ob} = Multispan{Ob,<:StaticVector{2}}
-
-function Span(left, right)
-  dom(left) == dom(right) ||
-    error("Domains of legs of span do not match: $left vs $right")
-  Multispan(dom(left), SVector(left, right))
-end
-
-apex(span::Multispan) = span.apex
-legs(span::Multispan) = span.legs
-left(span::Span) = span.legs[1]
-right(span::Span) = span.legs[2]
-
-Base.iterate(span::Multispan, args...) = iterate(span.legs, args...)
-Base.eltype(span::Multispan) = eltype(span.legs)
-Base.length(span::Multispan) = length(span.legs)
-
-""" Multicospan of morphisms in a category.
-
-A multicospan is like a [`Cospan`](@ref) except that it may have a number of
-legs different than two. A limit of this shape is a pullback.
-"""
-@auto_hash_equals struct Multicospan{Ob,Legs<:AbstractVector} <: FixedFreeDiagram{Ob}
-  base::Ob
-  legs::Legs
-end
-
-function Multicospan(legs::AbstractVector)
-  @assert !isempty(legs) && allequal(codom.(legs))
-  Multicospan(codom(first(legs)), legs)
-end
-
-""" Cospan of morphisms in a category.
-
-A common special case of [`Multicospan`](@ref). See also [`Span`](@ref).
-"""
-const Cospan{Ob} = Multicospan{Ob,<:StaticVector{2}}
-
-function Cospan(left, right)
-  codom(left) == codom(right) ||
-    error("Codomains of legs of cospan do not match: $left vs $right")
-  Multicospan(codom(left), SVector(left, right))
-end
-
-base(cospan::Multicospan) = cospan.base
-legs(cospan::Multicospan) = cospan.legs
-left(cospan::Cospan) = cospan.legs[1]
-right(cospan::Cospan) = cospan.legs[2]
-
-Base.iterate(cospan::Multicospan, args...) = iterate(cospan.legs, args...)
-Base.eltype(cospan::Multicospan) = eltype(cospan.legs)
-Base.length(cospan::Multicospan) = length(cospan.legs)
-
-""" Parallel morphims in a category.
-
-[Parallel morphisms](https://ncatlab.org/nlab/show/parallel+morphisms) are just
-morphisms with the same domain and codomain. A (co)limit of this shape is a
-(co)equalizer.
-
-For the common special case of two morphisms, see [`ParallelPair`](@ref).
-"""
-@auto_hash_equals struct ParallelMorphisms{Ob,Homs<:AbstractVector} <: FixedFreeDiagram{Ob}
-  dom::Ob
-  codom::Ob
-  homs::Homs
-end
-
-function ParallelMorphisms(homs::AbstractVector)
-  @assert !isempty(homs) && allequal(dom.(homs)) && allequal(codom.(homs))
-  ParallelMorphisms(dom(first(homs)), codom(first(homs)), homs)
-end
-
-""" Pair of parallel morphisms in a category.
-
-A common special case of [`ParallelMorphisms`](@ref).
-"""
-const ParallelPair{Ob} = ParallelMorphisms{Ob,<:StaticVector{2}}
-
-function ParallelPair(first, last)
-  dom(first) == dom(last) ||
-    error("Domains of parallel pair do not match: $first vs $last")
-  codom(first) == codom(last) ||
-    error("Codomains of parallel pair do not match: $first vs $last")
-  ParallelMorphisms(dom(first), codom(first), SVector(first, last))
-end
-
-dom(para::ParallelMorphisms) = para.dom
-codom(para::ParallelMorphisms) = para.codom
-hom(para::ParallelMorphisms) = para.homs
-
-Base.iterate(para::ParallelMorphisms, args...) = iterate(para.homs, args...)
-Base.eltype(para::ParallelMorphisms) = eltype(para.homs)
-Base.length(para::ParallelMorphisms) = length(para.homs)
-Base.getindex(para::ParallelMorphisms, i) = para.homs[i]
-Base.firstindex(para::ParallelMorphisms) = firstindex(para.homs)
-Base.lastindex(para::ParallelMorphisms) = lastindex(para.homs)
-
-allequal(xs::AbstractVector) = all(isequal(x, xs[1]) for x in xs[2:end])
-
-# Decorated cospans
-#------------------
-
-# FIXME: Types and structs for functors do not belong here.
-abstract type AbstractFunctor end
-abstract type AbstractLaxator end
-
-struct LaxMonoidalFunctor{Ftr <: AbstractFunctor, Lxr <: AbstractLaxator} <: AbstractFunctor
-  F::Ftr
-  L::Lxr
-end
-
-""" Decorate Cospan of morphisms for representing open networks.
-"""
-struct DecoratedCospan{Decorator <: AbstractFunctor,Decoration}
-  cospan::Cospan
-  decorator::Decorator
-  decoration::Decoration
-end
-
-decorator(m::DecoratedCospan) = m.decorator
-decoration(m::DecoratedCospan) = m.decoration
-undecorate(m::DecoratedCospan) = m.cospan
-base(m::DecoratedCospan) = base(m.cospan)
-left(m::DecoratedCospan) = left(m.cospan)
-right(m::DecoratedCospan) = right(m.cospan)
-
-# General diagrams
-##################
-
-@present TheoryFreeDiagram <: TheoryGraph begin
-  Ob::Data
-  Hom::Data
-  ob::Attr(V,Ob)
-  hom::Attr(E,Hom)
-end
-
-const FreeDiagram{ObT,HomT} = ACSet{SchemaType(TheoryFreeDiagram)...,Tuple{ObT,HomT},(:src,:tgt)}
-
-ob(d::FreeDiagram, args...) = subpart(d, args..., :ob)
-hom(d::FreeDiagram, args...) = subpart(d, args..., :hom)
-
-function FreeDiagram(obs::Vector{Ob},
-                     homs::Vector{Tuple{Int,Int,Hom}}) where {Ob,Hom}
-  @assert all(obs[s] == dom(f) && obs[t] == codom(f) for (s,t,f) in homs)
-  d = FreeDiagram{Ob,Hom}()
-  add_vertices!(d, length(obs), ob=obs)
-  add_edges!(d, getindex.(homs,1), getindex.(homs,2), hom=last.(homs))
-  return d
-end
-
-# Conversion of fixed shapes
-#---------------------------
-
-function FreeDiagram(span::Multispan{Ob}) where Ob
-  d = FreeDiagram{Ob,eltype(span)}()
-  v0 = add_vertex!(d, ob=apex(span))
-  vs = add_vertices!(d, length(span), ob=codom.(legs(span)))
-  add_edges!(d, fill(v0, length(span)), vs, hom=legs(span))
-  return d
-end
-
-function FreeDiagram(cospan::Multicospan{Ob}) where Ob
-  d = FreeDiagram{Ob,eltype(cospan)}()
-  vs = add_vertices!(d, length(cospan), ob=dom.(legs(cospan)))
-  v0 = add_vertex!(d, ob=base(cospan))
-  add_edges!(d, vs, fill(v0, length(cospan)), hom=legs(cospan))
-  return d
-end
-
-function FreeDiagram(para::ParallelMorphisms{Ob}) where Ob
-  d = FreeDiagram{Ob,eltype(para)}()
-  add_vertices!(d, 2, ob=[dom(para), codom(para)])
-  add_edges!(d, fill(1,length(para)), fill(2,length(para)), hom=hom(para))
-  return d
-end
-
-end
diff --git a/src/categorical_algebra/Graphs.jl b/src/categorical_algebra/Graphs.jl
index d06992988..0dd042d00 100644
--- a/src/categorical_algebra/Graphs.jl
+++ b/src/categorical_algebra/Graphs.jl
@@ -12,55 +12,56 @@ export AbstractGraph, Graph, AbstractSymmetricGraph, SymmetricGraph,
   set_gprop!, set_vprop!, set_eprop!, set_gprops!, set_vprops!, set_eprops!
 
 using Compat: isnothing
-import LightGraphs: SimpleGraph, SimpleDiGraph, nv, ne, src, dst,
-  edges, vertices, has_edge, has_vertex, add_edge!, add_vertex!, add_vertices!,
+import LightGraphs
+import LightGraphs: nv, ne, src, dst, edges, vertices, has_edge, has_vertex,
+  add_edge!, add_vertex!, add_vertices!,
   neighbors, inneighbors, outneighbors, all_neighbors
 
 using ...Present
-using ...Theories
+using ...Theories: FreeCategory
 using ..CSets
 
 # Graphs
 ########
 
-@present TheoryGraph(FreeSchema) begin
+@present TheoryGraph(FreeCategory) begin
   V::Ob
   E::Ob
   src::Hom(E,V)
   tgt::Hom(E,V)
 end
 
-const AbstractGraph = AbstractACSet{CatDescType(TheoryGraph)}
-const Graph = CSet{CatDescType(TheoryGraph), (:src,:tgt)}
+const AbstractGraph = AbstractCSetType(TheoryGraph)
+const Graph = CSetType(TheoryGraph, index=[:src,:tgt])
 
-nv(g::AbstractACSet) = nparts(g, :V)
-ne(g::AbstractACSet) = nparts(g, :E)
-ne(g::AbstractACSet, src::Int, tgt::Int) =
+nv(g::AbstractCSet) = nparts(g, :V)
+ne(g::AbstractCSet) = nparts(g, :E)
+ne(g::AbstractCSet, src::Int, tgt::Int) =
   count(subpart(g, e, :tgt) == tgt for e in incident(g, src, :src))
 
-src(g::AbstractACSet, args...) = subpart(g, args..., :src)
-tgt(g::AbstractACSet, args...) = subpart(g, args..., :tgt)
-dst(g::AbstractACSet, args...) = tgt(g, args...) # LightGraphs compatibility
+src(g::AbstractCSet, args...) = subpart(g, args..., :src)
+tgt(g::AbstractCSet, args...) = subpart(g, args..., :tgt)
+dst(g::AbstractCSet, args...) = tgt(g, args...) # LightGraphs compatibility
 
-vertices(g::AbstractACSet) = 1:nv(g)
-edges(g::AbstractACSet) = 1:ne(g)
-edges(g::AbstractACSet, src::Int, tgt::Int) =
+vertices(g::AbstractCSet) = 1:nv(g)
+edges(g::AbstractCSet) = 1:ne(g)
+edges(g::AbstractCSet, src::Int, tgt::Int) =
   (e for e in incident(g, src, :src) if subpart(g, e, :tgt) == tgt)
 
-has_vertex(g::AbstractACSet, v) = has_part(g, :V, v)
-has_edge(g::AbstractACSet, e) = has_part(g, :E, e)
-has_edge(g::AbstractACSet, src::Int, tgt::Int) = tgt ∈ outneighbors(g, src)
+has_vertex(g::AbstractCSet, v) = has_part(g, :V, v)
+has_edge(g::AbstractCSet, e) = has_part(g, :E, e)
+has_edge(g::AbstractCSet, src::Int, tgt::Int) = tgt ∈ outneighbors(g, src)
 
-add_vertex!(g::AbstractGraph; kw...) = add_part!(g, :V; kw...)
-add_vertices!(g::AbstractGraph, n::Int; kw...) = add_parts!(g, :V, n; kw...)
+add_vertex!(g::AbstractGraph) = add_part!(g, :V)
+add_vertices!(g::AbstractGraph, n::Int) = add_parts!(g, :V, n)
 
-add_edge!(g::AbstractGraph, src::Int, tgt::Int; kw...) =
-  add_part!(g, :E; src=src, tgt=tgt, kw...)
+add_edge!(g::AbstractGraph, src::Int, tgt::Int) =
+  add_part!(g, :E, src=src, tgt=tgt)
 
 function add_edges!(g::AbstractGraph, srcs::AbstractVector{Int},
-                    tgts::AbstractVector{Int}; kw...)
+                    tgts::AbstractVector{Int})
   @assert length(srcs) == length(tgts)
-  add_parts!(g, :E; src=srcs, tgt=tgts, kw...)
+  add_parts!(g, :E, length(srcs), src=srcs, tgt=tgts)
 end
 
 neighbors(g::AbstractGraph, v::Int) = outneighbors(g, v)
@@ -69,8 +70,8 @@ outneighbors(g::AbstractGraph, v::Int) = subpart(g, incident(g, v, :src), :tgt)
 all_neighbors(g::AbstractGraph, v::Int) =
   Iterators.flatten((inneighbors(g, v), outneighbors(g, v)))
 
-# # Symmetric graphs
-# ##################
+# Symmetric graphs
+##################
 
 @present TheorySymmetricGraph <: TheoryGraph begin
   inv::Hom(E,E)
@@ -82,32 +83,22 @@ end
 
 # Don't index `inv` because it is self-inverse and don't index `tgt`
 # because `src` contains the same information due to symmetry of graph.
-const AbstractSymmetricGraph = AbstractCSet{CatDescType(TheorySymmetricGraph)}
-const SymmetricGraph = CSet{CatDescType(TheorySymmetricGraph), (:src,)}
-
-function (::Type{T})(nv::Int) where
-    T <: Union{AbstractGraph,AbstractSymmetricGraph}
-  g = T()
-  add_vertices!(g, nv)
-  g
-end
+const AbstractSymmetricGraph = AbstractCSetType(TheorySymmetricGraph)
+const SymmetricGraph = CSetType(TheorySymmetricGraph, index=[:src])
 
-inv(g::AbstractACSet, args...) = subpart(g, args..., :inv)
+inv(g::AbstractCSet, args...) = subpart(g, args..., :inv)
 
-add_vertex!(g::AbstractSymmetricGraph; kw...) = add_part!(g, :V; kw...)
-add_vertices!(g::AbstractSymmetricGraph, n::Int; kw...) =
-  add_parts!(g, :V, n; kw...)
+add_vertex!(g::AbstractSymmetricGraph) = add_part!(g, :V)
+add_vertices!(g::AbstractSymmetricGraph, n::Int) = add_parts!(g, :V, n)
 
-add_edge!(g::AbstractSymmetricGraph, src::Int, tgt::Int; kw...) =
-  add_edges!(g, src:src, tgt:tgt; kw...)
+add_edge!(g::AbstractSymmetricGraph, src::Int, tgt::Int) =
+  add_edges!(g, src:src, tgt:tgt)
 
 function add_edges!(g::AbstractSymmetricGraph, srcs::AbstractVector{Int},
-                    tgts::AbstractVector{Int}; kw...)
+                    tgts::AbstractVector{Int})
   @assert (n = length(srcs)) == length(tgts)
-  k = nparts(g,:E)
-  doubled_kw = map(v -> vcat(v,v),values(kw))
-  add_parts!(g, :E; src=vcat(srcs,tgts), tgt=vcat(tgts,srcs),
-             inv=vcat((k+n+1):(k+2n),(k+1):(k+n)), kw...)
+  invs = nparts(g, :E) .+ [(n+1):2n; 1:n]
+  add_parts!(g, :E, 2n, src=[srcs; tgts], tgt=[tgts; srcs], inv=invs)
 end
 
 neighbors(g::AbstractSymmetricGraph, v::Int) =
@@ -126,15 +117,15 @@ Concrete types are [`PropertyGraph`](@ref) and [`SymmetricPropertyGraph`](@ref).
 abstract type AbstractPropertyGraph{T} end
 
 @present TheoryPropertyGraph <: TheoryGraph begin
-  Props::Data
-  vprops::Attr(V,Props)
-  eprops::Attr(E,Props)
+  Props::Ob
+  vprops::Hom(V,Props)
+  eprops::Hom(E,Props)
 end
 
-const _AbstractPropertyGraph{T} =
-  AbstractACSet{SchemaType(TheoryPropertyGraph)..., Tuple{Dict{Symbol,T}}}
-const _PropertyGraph{T} =
-  ACSet{SchemaType(TheoryPropertyGraph)..., Tuple{Dict{Symbol,T}}, (:src,:tgt)}
+const _AbstractPropertyGraph =
+  AbstractCSetType(TheoryPropertyGraph, data=[:Props])
+const _PropertyGraph =
+  CSetType(TheoryPropertyGraph, data=[:Props], index=[:src,:tgt])
 
 """ Graph with properties.
 
@@ -145,27 +136,28 @@ and more efficient to create a specialized C-set type using [`CSetType`](@ref).
 
 See also: [`SymmetricPropertyGraph`](@ref).
 """
-struct PropertyGraph{T,G<:_AbstractPropertyGraph{T}} <: AbstractPropertyGraph{T}
+struct PropertyGraph{T,G<:_AbstractPropertyGraph} <: AbstractPropertyGraph{T}
   graph::G
   gprops::Dict{Symbol,T}
 end
 
-PropertyGraph{T,G}(; kw...) where {T,G<:_AbstractPropertyGraph{T}} =
-  PropertyGraph(G(), Dict{Symbol,T}(kw...))
-PropertyGraph{T}(; kw...) where T = PropertyGraph{T,_PropertyGraph{T}}(; kw...)
+PropertyGraph{T,G}(; kw...) where {T,G<:_AbstractPropertyGraph} =
+  PropertyGraph(G(vprops=Dict{Symbol,T}, eprops=Dict{Symbol,T}),
+                Dict{Symbol,T}(kw...))
+PropertyGraph{T}(; kw...) where T = PropertyGraph{T,_PropertyGraph}(; kw...)
 
 @present TheorySymmetricPropertyGraph <: TheorySymmetricGraph begin
-  Props::Data
-  vprops::Attr(V,Props)
-  eprops::Attr(E,Props)
+  Props::Ob
+  vprops::Hom(V,Props)
+  eprops::Hom(E,Props)
 
   compose(inv,eprops) == eprops # Edge involution preserves edge properties.
 end
 
-const _AbstractSymmetricPropertyGraph{T} =
-  AbstractACSet{SchemaType(TheorySymmetricPropertyGraph)..., Tuple{Dict{Symbol,T}}}
-const _SymmetricPropertyGraph{T} =
-  ACSet{SchemaType(TheorySymmetricPropertyGraph)..., Tuple{Dict{Symbol,T}}, (:src,)}
+const _AbstractSymmetricPropertyGraph =
+  AbstractCSetType(TheorySymmetricPropertyGraph, data=[:Props])
+const _SymmetricPropertyGraph =
+  CSetType(TheorySymmetricPropertyGraph, data=[:Props], index=[:src])
 
 """ Symmetric graphs with properties.
 
@@ -174,17 +166,17 @@ interpreted as "undirected" property (multi)graphs.
 
 See also: [`PropertyGraph`](@ref).
 """
-struct SymmetricPropertyGraph{T,G<:_AbstractSymmetricPropertyGraph{T}} <:
+struct SymmetricPropertyGraph{T,G<:_AbstractSymmetricPropertyGraph} <:
     AbstractPropertyGraph{T}
   graph::G
   gprops::Dict{Symbol,T}
 end
 
-SymmetricPropertyGraph{T,G}(; kw...) where {T,G<:_AbstractSymmetricPropertyGraph{T}} =
-  SymmetricPropertyGraph(G(),
+SymmetricPropertyGraph{T,G}(; kw...) where {T,G<:_AbstractSymmetricPropertyGraph} =
+  SymmetricPropertyGraph(G(vprops=Dict{Symbol,T}, eprops=Dict{Symbol,T}),
                          Dict{Symbol,T}(kw...))
 SymmetricPropertyGraph{T}(; kw...) where T =
-  SymmetricPropertyGraph{T,_SymmetricPropertyGraph{T}}(; kw...)
+  SymmetricPropertyGraph{T,_SymmetricPropertyGraph}(; kw...)
 
 gprops(g::AbstractPropertyGraph) = g.gprops
 vprops(g::AbstractPropertyGraph, v) = subpart(g.graph, v, :vprops)
@@ -229,7 +221,7 @@ add_vertex!(g::AbstractPropertyGraph{T}, d::Dict{Symbol,T}) where T =
   add_part!(g.graph, :V, vprops=d)
 
 add_vertices!(g::AbstractPropertyGraph{T}, n::Int) where T =
-  add_parts!(g.graph, :V, n, vprops=[Dict{Symbol,T}() for i=1:n])
+  add_parts!(g.graph, :V, n, vprops=(Dict{Symbol,T}() for i=1:n))
 
 add_edge!(g::AbstractPropertyGraph{T}, src::Int, tgt::Int; kw...) where T =
   add_edge!(g, src, tgt, Dict{Symbol,T}(kw...))
@@ -243,7 +235,7 @@ function add_edges!(g::PropertyGraph{T}, srcs::AbstractVector{Int},
                     tgts::AbstractVector{Int}, eprops=nothing) where T
   @assert (n = length(srcs)) == length(tgts)
   if isnothing(eprops)
-    eprops = [Dict{Symbol,T}() for i=1:n]
+    eprops = (Dict{Symbol,T}() for i=1:n)
   end
   add_parts!(g.graph, :E, n, src=srcs, tgt=tgts, eprops=eprops)
 end
@@ -266,53 +258,55 @@ function add_edges!(g::SymmetricPropertyGraph{T}, srcs::AbstractVector{Int},
              inv=invs, eprops=eprops)
 end
 
-# Constructors from graphs
-##########################
+# Constructor from regular graph
+################################
 
-function PropertyGraph{T}(g::Graph, make_vprops, make_eprops; gprops...) where T
-  pg = PropertyGraph{T}(; gprops...)
+function PropertyGraph{T}(g::Graph,make_vprops,make_eprops) where {T}
+  pg = PropertyGraph{T}()
   add_vertices!(pg, nv(g))
-  add_edges!(pg, src(g), tgt(g))
-  for v in vertices(g)
-    set_vprops!(pg, v, make_vprops(v))
+  add_edges!(pg,src(g),tgt(g))
+  for i in 1:nv(g)
+    set_vprops!(pg,i,make_vprops(i))
   end
-  for e in edges(g)
-    set_eprops!(pg, e, make_eprops(e))
+  for i in 1:ne(g)
+    set_eprops!(pg,i,make_eprops(i))
   end
   pg
 end
 
-PropertyGraph{T}(g::Graph; gprops...) where T =
-  PropertyGraph{T}(g, v->Dict(), e->Dict(); gprops...)
+PropertyGraph{T}(g::Graph) where {T} = PropertyGraph{T}(g,e->Dict(),v->Dict())
 
-function SymmetricPropertyGraph{T}(g::SymmetricGraph,
-                                   make_vprops, make_eprops; gprops...) where T
-  pg = SymmetricPropertyGraph{T}(; gprops...)
+function SymmetricPropertyGraph{T}(g::SymmetricGraph,vertex_dec,edge_dec) where {T}
+  pg = SymmetricPropertyGraph{T}()
   add_vertices!(pg, nv(g))
-  for v in vertices(g)
-    set_vprops!(pg, v, make_vprops(v))
+  for v in 1:nv(g)
+    set_vprops!(pg,v,vertex_dec(v))
   end
-  for e in edges(g)
+  for e in 1:ne(g)
     if e <= inv(g,e)
-      e1, e2 = add_edge!(pg, src(g,e), tgt(g,e))
-      set_eprops!(pg, e1, make_eprops(e))
+      add_edge!(pg,src(g,e),tgt(g,e))
+      set_eprops!(pg,e,edge_dec(e))
     end
   end
   pg
 end
 
-SymmetricPropertyGraph{T}(g::SymmetricGraph; gprops...) where T =
-  SymmetricPropertyGraph{T}(g, v->Dict(), e->Dict(); gprops...)
+SymmetricPropertyGraph{T}(g::SymmetricGraph) where {T} = SymmetricPropertyGraph{T}(g,e->Dict(),v->Dict())
 
 # LightGraphs interop
 #####################
 
-function (::Type{LG})(g::Union{AbstractGraph,AbstractSymmetricGraph}) where
-    LG <: Union{SimpleGraph,SimpleDiGraph}
-  lg = LG(nv(g))
-  for e in edges(g)
-    add_edge!(lg, src(g,e), tgt(g,e))
-  end
+function LightGraphs.SimpleDiGraph(
+    g::Union{AbstractGraph,AbstractSymmetricGraph})
+  lg = LightGraphs.SimpleDiGraph(nv(g))
+  for e in edges(g); add_edge!(lg, src(g,e), tgt(g,e)) end
+  lg
+end
+
+function LightGraphs.SimpleGraph(
+    g::Union{AbstractGraph,AbstractSymmetricGraph})
+  lg = LightGraphs.SimpleGraph(nv(g))
+  for e in edges(g); add_edge!(lg, src(g,e), tgt(g,e)) end
   lg
 end
 
diff --git a/src/categorical_algebra/Limits.jl b/src/categorical_algebra/Limits.jl
deleted file mode 100644
index b31f6a2a5..000000000
--- a/src/categorical_algebra/Limits.jl
+++ /dev/null
@@ -1,162 +0,0 @@
-""" Limits and colimits in a category.
-"""
-module Limits
-export AbstractLimit, AbstractColimit, Limit, Colimit,
-  Terminal, BinaryProduct, Product, BinaryPullback, Pullback, Equalizer,
-  Initial, BinaryCoproduct, Coproduct, BinaryPushout, Pushout, Coequalizer,
-  ob, cone, cocone, apex, base, legs,
-  limit, terminal, product, proj1, proj2, equalizer, incl, pullback,
-  colimit, initial, coproduct, coproj1, coproj2, coqualizer, proj, pushout
-
-using Compat: only
-
-using AutoHashEquals
-using Missings: nonmissingtype
-using StaticArrays: StaticVector, SVector, @SVector
-
-using ...Theories
-import ...Theories: ob, terminal, product, proj1, proj2, equalizer, incl,
-  initial, coproduct, coproj1, coproj2, coequalizer, proj
-using ..FreeDiagrams
-import ..FreeDiagrams: apex, base, legs
-
-# Data types
-############
-
-""" Abstract type for limit in a category.
-
-The standard concrete subtype is [`Limit`](@ref), although for computational
-reasons certain categories may use different subtypes to include extra data.
-"""
-abstract type AbstractLimit{Ob,Diagram} end
-
-ob(lim::AbstractLimit) = apex(lim)
-cone(lim::AbstractLimit) = lim.cone
-apex(lim::AbstractLimit) = apex(cone(lim))
-legs(lim::AbstractLimit) = legs(cone(lim))
-
-Base.iterate(lim::AbstractLimit, args...) = iterate(cone(lim), args...)
-Base.eltype(lim::AbstractLimit) = eltype(cone(lim))
-Base.length(lim::AbstractLimit) = length(cone(lim))
-
-""" Limit in a category.
-"""
-@auto_hash_equals struct Limit{Ob,Diagram,Cone<:Multispan{Ob}} <:
-    AbstractLimit{Ob,Diagram}
-  diagram::Diagram
-  cone::Cone
-end
-
-const Terminal{Ob} = AbstractLimit{Ob,<:StaticVector{0}}
-const BinaryProduct{Ob} = AbstractLimit{Ob,<:StaticVector{2}}
-const Product{Ob} = AbstractLimit{Ob,<:AbstractVector}
-const BinaryPullback{Ob} = AbstractLimit{Ob,<:Cospan}
-const Pullback{Ob} = AbstractLimit{Ob,<:Multicospan}
-const Equalizer{Ob} = AbstractLimit{Ob,<:ParallelMorphisms}
-
-proj1(lim::Union{BinaryProduct,BinaryPullback}) = first(legs(lim))
-proj2(lim::Union{BinaryProduct,BinaryPullback}) = last(legs(lim))
-incl(eq::Equalizer) = only(legs(eq))
-
-""" Abstract type for colimit in a category.
-
-The standard concrete subtype is [`Colimit`](@ref), although for computational
-reasons certain categories may use different subtypes to include extra data.
-"""
-abstract type AbstractColimit{Ob,Diagram} end
-
-ob(colim::AbstractColimit) = base(colim)
-cocone(colim::AbstractColimit) = colim.cocone
-base(colim::AbstractColimit) = base(cocone(colim))
-legs(colim::AbstractColimit) = legs(cocone(colim))
-
-Base.iterate(colim::AbstractColimit, args...) = iterate(cocone(colim), args...)
-Base.eltype(colim::AbstractColimit) = eltype(cocone(colim))
-Base.length(colim::AbstractColimit) = length(cocone(colim))
-
-""" Colimit in a category.
-"""
-@auto_hash_equals struct Colimit{Ob,Diagram,Cocone<:Multicospan{Ob}} <:
-    AbstractColimit{Ob,Diagram}
-  diagram::Diagram
-  cocone::Cocone
-end
-
-const Initial{Ob} = AbstractColimit{Ob,<:StaticVector{0}}
-const BinaryCoproduct{Ob} = AbstractColimit{Ob,<:StaticVector{2}}
-const Coproduct{Ob} = AbstractColimit{Ob,<:AbstractVector}
-const BinaryPushout{Ob} = AbstractColimit{Ob,<:Span}
-const Pushout{Ob} = AbstractColimit{Ob,<:Multispan}
-const Coequalizer{Ob} = AbstractColimit{Ob,<:ParallelMorphisms}
-
-coproj1(colim::Union{BinaryCoproduct,BinaryPushout}) = first(legs(colim))
-coproj2(colim::Union{BinaryCoproduct,BinaryPushout}) = last(legs(colim))
-proj(coeq::Coequalizer) = only(legs(coeq))
-
-# Specific (co)limits
-#####################
-
-terminal(T::Type) = product(@SVector T[])
-initial(T::Type) = coproduct(@SVector T[])
-
-""" Product of a pair of objects.
-"""
-product(A, B) = product(SVector(A, B))
-
-""" Coproduct of a pair of objects.
-"""
-coproduct(A, B) = coproduct(SVector(A, B))
-
-""" Equalizer of a pair of morphisms with common domain and codomain.
-"""
-equalizer(f, g) = equalizer(ParallelPair(f, g))
-equalizer(fs::AbstractVector) = equalizer(ParallelMorphisms(fs))
-
-""" Coequalizer of a pair of morphisms with common domain and codomain.
-"""
-coequalizer(f, g) = coequalizer(ParallelPair(f, g))
-coequalizer(fs::AbstractVector) = coequalizer(ParallelMorphisms(fs))
-
-""" Pullback of a pair of morphisms with common codomain.
-"""
-pullback(f, g) = pullback(Cospan(f, g))
-pullback(fs::AbstractVector) = pullback(Multicospan(fs))
-
-""" Pushout of a pair of morphisms with common domain.
-"""
-pushout(f, g) = pushout(Span(f, g))
-pushout(fs::AbstractVector) = pushout(Multispan(fs))
-
-""" Pullback of a cospan.
-
-The default implementation computes the pullback from products and equalizers.
-"""
-function pullback(cospan::Cospan)
-  f, g = cospan
-  π1, π2 = product(dom(f), dom(g))
-  ι = incl(equalizer(π1⋅f, π2⋅g))
-  Limit(cospan, Span(ι⋅π1, ι⋅π2))
-end
-
-""" Pushout of a span.
-
-The default implementation computes the pushout from coproducts and
-coequalizers.
-"""
-function pushout(span::Span)
-  f, g = span
-  ι1, ι2 = coproduct(codom(f), codom(g))
-  π = proj(coequalizer(f⋅ι1, g⋅ι2))
-  Colimit(span, Cospan(ι1⋅π, ι2⋅π))
-end
-
-# General (co)limits
-####################
-
-# FIXME: Object type information should be encoded in C-set type.
-limit(diagram::FreeDiagram) =
-  limit(nonmissingtype(eltype(diagram.tables.V.ob)), diagram)
-colimit(diagram::FreeDiagram) =
-  colimit(nonmissingtype(eltype(diagram.tables.V.ob)), diagram)
-
-end
diff --git a/src/categorical_algebra/Matrices.jl b/src/categorical_algebra/Matrices.jl
index 9ba81f01a..a87168ed4 100644
--- a/src/categorical_algebra/Matrices.jl
+++ b/src/categorical_algebra/Matrices.jl
@@ -43,9 +43,9 @@ For a similar design (only for sparse matrices) by the Julia core developers,
 see [SemiringAlgebra.jl](https://github.com/JuliaComputing/SemiringAlgebra.jl)
 and [accompanying short paper](https://doi.org/10.1109/HPEC.2013.6670347).
 """
-@instance DistributiveSemiadditiveCategory{MatrixDom, AbstractMatrix} begin
+@instance DistributiveSemiadditiveCategory(MatrixDom, AbstractMatrix) begin
   # FIXME: Cannot define type-parameterized instances.
-  #@instance AdditiveBiproductCategory{MatrixDom{M}, M} where M <: AbstractMatrix begin
+  #@instance AdditiveBiproductCategory(MatrixDom{M}, M) where M <: AbstractMatrix begin
   @import +, dom, codom, id, mzero, munit, braid
   
   compose(A::AbstractMatrix, B::AbstractMatrix) = B*A
diff --git a/src/categorical_algebra/PredicatedSets.jl b/src/categorical_algebra/PredicatedSets.jl
deleted file mode 100644
index 5f14692b6..000000000
--- a/src/categorical_algebra/PredicatedSets.jl
+++ /dev/null
@@ -1,58 +0,0 @@
-module PredicatedSets
-export PredicatedSet,PredicatedHom
-
-import FunctionWrappers
-import FunctionWrappers: FunctionWrapper
-using ...GAT
-using ...Theories
-import ...Theories: id, compose
-
-struct PredicatedSet{T}
-  predicate::FunctionWrapper{Bool,Tuple{T}}
-  PredicatedSet(T::DataType,f::Function) = new{T}(FunctionWrapper{Bool,Tuple{T}}(f))
-end
-
-function (t::PredicatedSet{T})(x::T)::Bool where {T}
-  t.predicate(x)
-end
-
-basetype(t::PredicatedSet{T}) where {T} = T
-
-struct PredicatedHom{T,S}
-  dom::PredicatedSet{T}
-  codom::PredicatedSet{S}
-  f::FunctionWrapper{S,Tuple{T}}
-  PredicatedHom(dom::PredicatedSet{T},codom::PredicatedSet{S},f) where {T,S} =
-    new{T,S}(dom,codom,FunctionWrapper{S,Tuple{T}}(f))
-end
-
-function (f::PredicatedHom{T,S})(x::T)::S where {T,S}
-  @assert f.dom(x)
-  y = f.f(x)
-  @assert f.codom(y)
-  y
-end
-
-function compose(f::PredicatedHom{T,S},g::PredicatedHom{S,U}) where {T,S,U}
-  function h(x)
-    y = f.f(x)
-    @assert f.codom(y)
-    @assert g.dom(y)
-    g.f(y)
-  end
-  PredicatedHom(f.dom,g.codom,h)
-end
-
-# This is really the category of decidable sets and partial functions...
-@instance Category{PredicatedSet, PredicatedHom} begin
-  dom(f::PredicatedHom) = f.dom
-  codom(f::PredicatedHom) = f.codom
-
-  id(t::PredicatedSet) = PredicatedHom(t,t,x->x)
-  compose(f::PredicatedHom,g::PredicatedHom) = begin
-    @assert basetype(f.codom) == basetype(g.dom)
-    compose(f,g)
-  end
-end
-
-end
diff --git a/src/categorical_algebra/ShapeDiagrams.jl b/src/categorical_algebra/ShapeDiagrams.jl
new file mode 100644
index 000000000..bd2fc75a6
--- /dev/null
+++ b/src/categorical_algebra/ShapeDiagrams.jl
@@ -0,0 +1,121 @@
+""" Diagrams of a given shape.
+"""
+module ShapeDiagrams
+export AbstractFunctor, AbstractLaxator, LaxMonoidalFunctor,
+  Span, Cone, Cospan, DecoratedCospan, Cocone, Diagram, left, right, leg, nlegs, apex, base,
+  decorator, decoration, undecorate
+
+using ...Theories: dom, codom
+
+abstract type AbstractFunctor end
+abstract type AbstractLaxator end
+
+struct LaxMonoidalFunctor{Ftr <: AbstractFunctor, Lxr <: AbstractLaxator} <: AbstractFunctor
+  F::Ftr
+  L::Lxr
+end
+
+""" Span of morphisms in a category.
+"""
+struct Span{Left,Right}
+  left::Left
+  right::Right
+
+  function Span(left::Left, right::Right; strict::Bool=true) where {Left,Right}
+    if strict && dom(left) != dom(right)
+      error("Domains of legs in span do not match: $left vs $right")
+    end
+    new{Left,Right}(left, right)
+  end
+end
+
+apex(span::Span) = dom(span.left) # == dom(span.right)
+left(span::Span) = span.left
+right(span::Span) = span.right
+
+""" Cone of morphisms in a category.
+"""
+struct Cone{S,T}
+  apex::S # We store this separately because legs might be empty
+  legs::Vector{T}
+
+  function Cone(apex::S,legs::Vector{T}, strict::Bool=true) where {S,T}
+    if strict && !all(dom(leg) == apex for leg in legs)
+      error("Domain of legs in cone do not match")
+    end
+    new{S,T}(apex,legs)
+  end
+end
+
+apex(cone::Cone) = cone.apex
+leg(cone::Cone,n) = cone.legs[n]
+legs(cone::Cone) = cone.legs
+nlegs(cone::Cone) = length(cone.legs)
+
+""" Cospan of morphisms in a category.
+"""
+struct Cospan{Left,Right}
+  left::Left
+  right::Right
+
+  function Cospan(left::Left, right::Right; strict::Bool=true) where {Left,Right}
+    if strict && codom(left) != codom(right)
+      error("Codomains of legs in cospan do not match: $left vs $right")
+    end
+    new{Left,Right}(left, right)
+  end
+end
+
+base(cospan::Cospan) = codom(cospan.left) # == codom(cospan.right)
+left(cospan::Cospan) = cospan.left
+right(cospan::Cospan) = cospan.right
+
+
+""" Decorate Cospan of morphisms for representing open networks.
+"""
+struct DecoratedCospan{Decorator <: AbstractFunctor,Decoration}
+  cospan::Cospan
+  decorator::Decorator
+  decoration::Decoration
+end
+
+decorator(m::DecoratedCospan) = m.decorator
+decoration(m::DecoratedCospan) = m.decoration
+undecorate(m::DecoratedCospan) = m.cospan
+base(m::DecoratedCospan) = base(m.cospan)
+left(m::DecoratedCospan) = left(m.cospan)
+right(m::DecoratedCospan) = right(m.cospan)
+
+""" Cocone of morphisms in a category.
+"""
+struct Cocone{S,T}
+  base::S # We store this separately because legs might be empty
+  legs::Vector{T}
+
+  function Cocone(base::S,legs::Vector{T}, strict::Bool=true) where {S,T}
+    if strict && !all(codom(leg) == base for leg in legs)
+      error("Codomain of legs in cocone do not match")
+    end
+    new{S,T}(base,legs)
+  end
+end
+
+base(cocone::Cocone) = cocone.base
+leg(cocone::Cocone,n) = cocone.legs[n]
+nlegs(cocone::Cocone) = length(cocone.legs)
+
+struct Diagram{Ob,Hom}
+  obs::Vector{Ob}
+  homs::Vector{Tuple{Int64,Int64,Hom}}
+
+  function Diagram(obs::Vector{Ob}, homs::Vector{Tuple{Int64,Int64,Hom}}) where {Ob, Hom}
+    for (s,t,f) in homs
+      @assert obs[s] == dom(f)
+      @assert obs[t] == codom(f)
+    end
+    new{Ob,Hom}(obs,homs)
+  end
+end
+
+
+end
diff --git a/src/core/GAT.jl b/src/core/GAT.jl
index 517168b47..3e61a7426 100644
--- a/src/core/GAT.jl
+++ b/src/core/GAT.jl
@@ -7,7 +7,6 @@ using Base.Meta: ParseError
 using Compat
 using AutoHashEquals
 using DataStructures: OrderedDict
-using Logging
 using MLStyle: @match
 
 using ..Meta
@@ -189,43 +188,21 @@ end
 
 function parse_theory_head(expr::Expr)::TheoryHead
   parse = parse_theory_binding
-  parse_jl = parse_theory_binding_jlstyle
-  parse_either = parse_theory_binding_either
   @match expr begin
     (Expr(:call, :(=>), Expr(:tuple, bases), main)
       => TheoryHead(parse(main), map(parse, bases)))
-    (Expr(:(<:), main, Expr(:tuple,bases))
-      => TheoryHead(parse_jl(main), map(parse_jl, bases)))
     Expr(:call, :(=>), base, main) => TheoryHead(parse(main), [parse(base)])
-    Expr(:(<:), main, base) => TheoryHead(parse_jl(main), [parse_jl(base)])
-    _ => TheoryHead(parse_either(expr))
+    _ => TheoryHead(parse(expr))
   end
 end
 
 function parse_theory_binding(expr::Expr)::TheoryBinding
-  @warn "Using Haskell-style theory declaration with parentheses is deprecated," *
-    " use Julia-style with curly braces."
   @match expr begin
     Expr(:call, name::Symbol, params...) => TheoryBinding(name, params)
     _ => throw(ParseError("Ill-formed theory binding $expr"))
   end
 end
 
-function parse_theory_binding_jlstyle(expr::Expr)::TheoryBinding
-  @match expr begin
-    Expr(:curly, name::Symbol, params...) => TheoryBinding(name, params)
-    _ => throw(ParseError("Ill-formed theory binding $expr"))
-  end
-end
-
-function parse_theory_binding_either(expr::Expr)::TheoryBinding
-  @match expr begin
-    Expr(:call, name::Symbol, params...) => TheoryBinding(name, params)
-    Expr(:curly, name::Symbol, params...) => TheoryBinding(name, params)
-    _ => throw(ParseError("Ill-formed theory binding $expr"))
-  end
-end
-
 """ Parse the body of a GAT declaration.
 """
 function parse_theory_body(expr::Expr)
@@ -538,7 +515,7 @@ end
 """
 macro instance(head, body)
   # Parse the instance definition.
-  head = parse_theory_binding_either(head)
+  head = parse_theory_binding(head)
   functions, ext_functions = parse_instance_body(body)
 
   # We must generate and evaluate the code at *run time* because the theory
diff --git a/src/core/Syntax.jl b/src/core/Syntax.jl
index 8ff56e80f..331d2fe27 100644
--- a/src/core/Syntax.jl
+++ b/src/core/Syntax.jl
@@ -105,12 +105,7 @@ macro syntax(syntax_head, mod_name, body=nothing)
   if isnothing(body); body = Expr(:block) end
   @assert body.head == :block
   syntax_name, base_types = @match syntax_head begin
-    Expr(:call, name::Symbol, args...) => begin
-      @warn "Using Haskell-style theory declaration with parentheses is deprecated," *
-        " use Julia-style with curly braces."
-      (name, args)
-    end
-    Expr(:curly, name::Symbol, args...) => (name, args)
+    Expr(:call, name::Symbol, args...) => (name, args)
     name::Symbol => (name, [])
     _ => throw(ParseError("Ill-formed syntax signature $syntax_head"))
   end
diff --git a/src/graphics/Graphviz.jl b/src/graphics/Graphviz.jl
index cc0945434..871179863 100644
--- a/src/graphics/Graphviz.jl
+++ b/src/graphics/Graphviz.jl
@@ -12,7 +12,6 @@ export Expression, Statement, Attributes, Graph, Digraph, Subgraph,
 using Compat
 using DataStructures: OrderedDict
 using Parameters: @with_kw_noshow
-using Requires: @require
 using StaticArrays: StaticVector, SVector
 
 using ...CategoricalAlgebra.Graphs: AbstractPropertyGraph, PropertyGraph,
@@ -20,19 +19,6 @@ using ...CategoricalAlgebra.Graphs: AbstractPropertyGraph, PropertyGraph,
   src, tgt, inv, vertices, edges,
   add_vertex!, add_edge!, vprops, eprops, gprops, set_gprops!
 
-const USE_GV_JLL = Ref(false)
-
-function __init__()
-  @require Graphviz_jll="3c863552-8265-54e4-a6dc-903eb78fde85" begin
-    USE_GV_JLL[] = true
-    let cfg = joinpath(Graphviz_jll.artifact_dir, "lib", "graphviz", "config6")
-      if !isfile(cfg)
-        Graphviz_jll.dot(path -> run(`$path -c`))
-      end
-    end
-  end
-end
-
 # AST
 #####
 
@@ -120,10 +106,8 @@ Edge(path::Vararg{String}; attrs...) = Edge(map(NodeID, collect(path)), attrs)
 
 """ Run a Graphviz program.
 
-Invokes Graphviz through its command-line interface.
-
-For Julia versions prior to 1.3, it's assumed that Graphviz
-is installed on the local system.
+Assumes that Graphviz is installed on the local system and invokes Graphviz
+through its command-line interface.
 
 For bindings to the Graphviz C API, see the the package
 [GraphViz.jl](https://github.com/Keno/GraphViz.jl). At the time of this writing,
@@ -135,10 +119,6 @@ function run_graphviz(io::IO, graph::Graph; prog::Union{String,Nothing}=nothing,
     prog = graph.prog
   end
   @assert prog in ("dot","neato","fdp","sfdp","twopi","circo")
-  if USE_GV_JLL[]
-    fun = getfield(Graphviz_jll, Symbol(prog))
-    prog = fun(identity)
-  end
   open(`$prog -T$format`, io, write=true) do gv
     pprint(gv, graph)
   end
@@ -166,7 +146,7 @@ All units are in points. Note that Graphviz has 72 points per inch.
 function parse_graphviz(doc::AbstractDict)::AbstractPropertyGraph
   graph = doc["directed"] ? PropertyGraph{Any}() : SymmetricPropertyGraph{Any}()
   nsubgraphs = doc["_subgraph_cnt"] # Subgraphs are ignored.
-
+  
   # Graph-level layout: bounds and padding.
   # It seems, but is not documented, that the first two numbers in the Graphviz
   # bounding box are always zero.
@@ -175,7 +155,7 @@ function parse_graphviz(doc::AbstractDict)::AbstractPropertyGraph
     pad = 72*parse_point(get(doc, "pad", "0,0")),
     rankdir = get(doc, "rankdir", "TB"),
   )
-
+  
   # Add vertex for each Graphviz node.
   node_keys = ("id", "name", "comment", "label", "shape", "style")
   for node in doc["objects"][nsubgraphs+1:end]
@@ -188,7 +168,7 @@ function parse_graphviz(doc::AbstractDict)::AbstractPropertyGraph
     )
     add_vertex!(graph, props)
   end
-
+  
   # Add edge for each Graphviz edge.
   edge_keys = ("id", "comment", "label", "xlabel", "headlabel", "taillabel",
                "headport", "tailport")
@@ -204,7 +184,7 @@ function parse_graphviz(doc::AbstractDict)::AbstractPropertyGraph
     tgt = edge["head"] - nsubgraphs + 1
     add_edge!(graph, src, tgt, props)
   end
-
+  
   graph
 end
 
@@ -278,53 +258,12 @@ function to_graphviz(g::AbstractPropertyGraph)::Graph
   )
 end
 
-""" Convert a graph to a Graphviz graph.
-
-A simple default style is applied. For more control over the visual appearance,
-first convert the graph to a property graph, define the Graphviz attributes as
-needed, and then convert to a Graphviz graph.
-"""
-function to_graphviz(g::AbstractGraph;
-    prog::String="dot", graph_attrs::AbstractDict=Dict(),
-    node_attrs::AbstractDict=Dict(), edge_attrs::AbstractDict=Dict(),
-    node_labels::Bool=false, edge_labels::Bool=false)
-  node_labeler(v) = Dict(:label => node_labels ? string(v) : "")
-  edge_labeler(e) = if edge_labels
-    Dict(:label => string(e))
-  else
-    Dict{Symbol,String}()
-  end
-  to_graphviz(PropertyGraph{Any}(g, node_labeler, edge_labeler;
-    prog = prog,
-    graph = merge(Dict(:rankdir => "LR"), graph_attrs),
-    node = merge(Dict(
-      :shape => node_labels ? "circle" : "point",
-      :width => "0.05", :height => "0.05", :margin => "0",
-    ), node_attrs),
-    edge = merge(Dict(:arrowsize => "0.5"), edge_attrs),
-  ))
+function to_graphviz(g::AbstractGraph)
+  to_graphviz(PropertyGraph{String}(g))
 end
 
-function to_graphviz(g::AbstractSymmetricGraph;
-    prog::String="neato", graph_attrs::AbstractDict=Dict(),
-    node_attrs::AbstractDict=Dict(), edge_attrs::AbstractDict=Dict(),
-    node_labels::Bool=false, edge_labels::Bool=false)
-  node_labeler(v) = Dict(:label => node_labels ? string(v) : "")
-  edge_labeler(e) = if edge_labels
-    e′ = inv(g,e)
-    Dict(:label => "($(min(e,e′)),$(max(e,e′)))")
-  else
-    Dict{Symbol,String}()
-  end
-  to_graphviz(SymmetricPropertyGraph{Any}(g, node_labeler, edge_labeler;
-    prog = prog,
-    graph = graph_attrs,
-    node = merge(Dict(
-      :shape => node_labels ? "circle" : "point",
-      :width => "0.05", :height => "0.05", :margin => "0",
-    ), node_attrs),
-    edge = merge(Dict(:len => "0.5"), edge_attrs),
-  ))
+function to_graphviz(g::AbstractSymmetricGraph)
+  to_graphviz(SymmetricPropertyGraph{String}(g))
 end
 
 # Pretty-print
diff --git a/src/graphics/GraphvizWiringDiagrams.jl b/src/graphics/GraphvizWiringDiagrams.jl
index 78fb28d51..13ff0d938 100644
--- a/src/graphics/GraphvizWiringDiagrams.jl
+++ b/src/graphics/GraphvizWiringDiagrams.jl
@@ -377,11 +377,9 @@ becoming nodes of the second kind.
 # Arguments
 - `graph_name="G"`: name of Graphviz graph
 - `prog="neato"`: Graphviz program, usually "neato" or "fdp"
-- `box_labels=false`: if boolean, whether to label boxes with their number;
-   if a symbol, name of data attribute for box label
-- `port_labels=false`: whether to label ports with their number
-- `junction_labels=false`: if boolean, whether to label junctions with their
-  number; if a symbol, name of data attribute for junction label
+- `box_labels=nothing`: name of diagram data for box label
+- `port_labels=true`: label ports with their number
+- `junction_labels=nothing`: name of diagram data for junction label
 - `junction_size="0.075"`: size of junction nodes, in inches
 - `implicit_junctions=false`: whether to represent a junction implicity as a
   wire when it has exactly two incident ports
@@ -395,8 +393,8 @@ end
 
 function to_graphviz_property_graph(d::UndirectedWiringDiagram;
     graph_name::String="G", prog::String="neato",
-    box_labels::Union{Bool,Symbol}=false, port_labels::Bool=false,
-    junction_labels::Union{Bool,Symbol}=false,
+    box_labels::Union{Symbol,Nothing}=nothing, port_labels::Bool=true,
+    junction_labels::Union{Symbol,Nothing}=nothing,
     junction_size::String="0.075", implicit_junctions::Bool=false,
     graph_attrs::AbstractDict=Dict(), node_attrs::AbstractDict=Dict(),
     edge_attrs::AbstractDict=Dict())::SymmetricPropertyGraph
@@ -411,12 +409,8 @@ function to_graphviz_property_graph(d::UndirectedWiringDiagram;
   # Create nodes for boxes.
   box_vs = add_vertices!(graph, nboxes(d))
   set_vprop!(graph, box_vs, :id, [ "box$b" for b in boxes(d) ])
-  labels = if box_labels isa Symbol
-    node_label.(subpart(d, box_vs, box_labels))
-  else
-    box_labels ? string.(boxes(d)) : ""
-  end
-  set_vprop!(graph, box_vs, :label, labels)
+  set_vprop!(graph, box_vs, :label, isnothing(box_labels) ? "" :
+             node_label.(subpart(d, box_vs, box_labels)))
 
   # Create nodes for outer ports.
   outer_vs = add_vertices!(graph, length(ports(d, outer=true)))
@@ -429,15 +423,12 @@ function to_graphviz_property_graph(d::UndirectedWiringDiagram;
   # Create nodes and edge for junctions.
   for j in junctions(d)
     # Get all ports, including outer ports, incident to junction.
-    incident_ports = [
-      map(ports_with_junction(d, j)) do port
+    incident_ports = [ map(ports_with_junction(d, j)) do port
         b = box(d, port)
         (b, findfirst(p -> p == port, ports(d, b)))
-      end;
-      map(ports_with_junction(d, j, outer=true)) do port
+      end; map(ports_with_junction(d, j, outer=true)) do port
         (nboxes(d) + port, port)
-      end
-    ]
+      end ]
 
     # Case 1: Create edge for junction with exactly two incident ports.
     if implicit_junctions && length(incident_ports) == 2
@@ -453,11 +444,9 @@ function to_graphviz_property_graph(d::UndirectedWiringDiagram;
                        shape="circle", label="",
                        style="filled", fillcolor="black",
                        width=junction_size, height=junction_size)
-      if junction_labels isa Symbol
+      if !isnothing(junction_labels)
         set_vprop!(graph, jv, :xlabel,
                    node_label(subpart(d, j, junction_labels)))
-      elseif junction_labels
-        set_vprop!(graph, jv, :xlabel, string(j))
       end
       for (v, port) in incident_ports
         e, e_inv = add_edge!(graph, v, jv)
diff --git a/src/linear_algebra/GLA.jl b/src/linear_algebra/GLA.jl
index 00dad77ce..c6d0284ea 100644
--- a/src/linear_algebra/GLA.jl
+++ b/src/linear_algebra/GLA.jl
@@ -25,7 +25,7 @@ import ...Programs: evaluate_hom
 
 Functional fragment of graphical linear algebra.
 """
-@theory LinearFunctions{Ob,Hom} <: SemiadditiveCategory{Ob,Hom} begin
+@theory SemiadditiveCategory(Ob,Hom) => LinearFunctions(Ob,Hom) begin
   adjoint(f::(A → B))::(B → A) ⊣ (A::Ob, B::Ob)
   
   scalar(A::Ob, c::Number)::(A → A)
@@ -45,7 +45,7 @@ Functional fragment of graphical linear algebra.
   scalar(A, c) ⋅ f == f ⋅ scalar(B, c) ⊣ (A::Ob, B::Ob, c::Number, f::(A → B))
 end
 
-@syntax FreeLinearFunctions{ObExpr,HomExpr} LinearFunctions begin
+@syntax FreeLinearFunctions(ObExpr,HomExpr) LinearFunctions begin
   oplus(A::Ob, B::Ob) = associate_unit(new(A,B), mzero)
   oplus(f::Hom, g::Hom) = associate(new(f,g))
   compose(f::Hom, g::Hom) = new(f,g; strict=true) # No normalization!
@@ -55,7 +55,7 @@ end
 
 The full relational language of graphical linear algebra.
 """
-@theory LinearRelations{Ob,Hom} <: AbelianBicategoryRelations{Ob,Hom} begin
+@theory AbelianBicategoryRelations(Ob,Hom) => LinearRelations(Ob,Hom) begin
   adjoint(R::(A → B))::(B → A) ⊣ (A::Ob, B::Ob)
 
   scalar(A::Ob, c::Number)::(A → A)
@@ -67,7 +67,7 @@ The full relational language of graphical linear algebra.
   scalar(A, c) ⋅ R == R ⋅ scalar(B, c) ⊣ (A::Ob, B::Ob, c::Number, R::(A → B))
 end
 
-@syntax FreeLinearRelations{ObExpr,HomExpr} LinearRelations begin
+@syntax FreeLinearRelations(ObExpr,HomExpr) LinearRelations begin
   oplus(A::Ob, B::Ob) = associate_unit(new(A,B), mzero)
   oplus(R::Hom, S::Hom) = associate(new(R,S))
   compose(R::Hom, S::Hom) = new(R,S; strict=true) # No normalization!
diff --git a/src/linear_algebra/LinearMapsExternal.jl b/src/linear_algebra/LinearMapsExternal.jl
index a1f575256..468fafcb4 100644
--- a/src/linear_algebra/LinearMapsExternal.jl
+++ b/src/linear_algebra/LinearMapsExternal.jl
@@ -11,7 +11,7 @@ const LMs = LinearMaps
   N::Int
 end
 
-@instance LinearFunctions{LinearMapDom, LinearMap} begin
+@instance LinearFunctions(LinearMapDom, LinearMap) begin
   @import adjoint, +
 
   dom(f::LinearMap) = LinearMapDom(size(f,2))
diff --git a/src/linear_algebra/LinearOperatorsExternal.jl b/src/linear_algebra/LinearOperatorsExternal.jl
index c498a57bf..c0a365649 100644
--- a/src/linear_algebra/LinearOperatorsExternal.jl
+++ b/src/linear_algebra/LinearOperatorsExternal.jl
@@ -9,7 +9,7 @@ using .LinearOperators: opEye, opExtension, opRestriction, opZeros
   N::Int
 end
 
-@instance LinearFunctions{LinearOpDom, LinearOperator} begin
+@instance LinearFunctions(LinearOpDom, LinearOperator) begin
   @import adjoint, +
 
   dom(f::LinearOperator) = LinearOpDom(size(f,2))
diff --git a/src/linear_algebra/StructuredGLA.jl b/src/linear_algebra/StructuredGLA.jl
index 94526357c..f7ff5fde6 100644
--- a/src/linear_algebra/StructuredGLA.jl
+++ b/src/linear_algebra/StructuredGLA.jl
@@ -22,7 +22,7 @@ import ..GraphicalLinearAlgebra:
 Structured matrices have some properties that allow us to compute with them faster
 than general dense matrices. Morphisms in this category represent structured matrices.
 """
-@theory StructuredLinearFunctions{Ob,Hom} <: LinearFunctions{Ob,Hom} begin
+@theory LinearFunctions(Ob,Hom) => StructuredLinearFunctions(Ob, Hom) begin
   munit()::Ob
   @op (ℝ) := munit
 
@@ -51,7 +51,7 @@ than general dense matrices. Morphisms in this category represent structured mat
   symtridiag(a,b) == tridiagonal(a,b,b) ⊣ (A::Ob, a::(ℝ()→A⊗ℝ()), b::(ℝ()→A))
 end
 
-@syntax FreeStructuredLinearFunctions{ObExpr,HomExpr} StructuredLinearFunctions begin
+@syntax FreeStructuredLinearFunctions(ObExpr,HomExpr) StructuredLinearFunctions begin
   oplus(A::Ob, B::Ob) = associate_unit(new(A,B), mzero)
   oplus(f::Hom, g::Hom) = associate(new(f,g))
   compose(f::Hom, g::Hom) = new(f,g; strict=true) # No normalization!
diff --git a/src/programs/RelationalPrograms.jl b/src/programs/RelationalPrograms.jl
index 171044178..9a4fd895a 100644
--- a/src/programs/RelationalPrograms.jl
+++ b/src/programs/RelationalPrograms.jl
@@ -1,15 +1,14 @@
 """ Parse relation expressions in Julia syntax into undirected wiring diagrams.
 """
 module RelationalPrograms
-export RelationDiagram, UntypedRelationDiagram, TypedRelationDiagram,
-  @relation, @tensor_network, @eval_tensor_network,
+export RelationDiagram, @relation, @tensor_network, @eval_tensor_network,
   parse_relation_diagram, parse_tensor_network, compile_tensor_expr
 
 using Compat
 using MLStyle: @match
 
 using ...CategoricalAlgebra.CSets, ...Present
-using ...Theories: FreeCategory, SchemaType
+using ...Theories: FreeCategory
 using ...WiringDiagrams.UndirectedWiringDiagrams
 import ...WiringDiagrams.UndirectedWiringDiagrams: UndirectedWiringDiagram,
   TheoryUWD, TheoryTypedUWD
@@ -18,28 +17,37 @@ import ...WiringDiagrams.UndirectedWiringDiagrams: UndirectedWiringDiagram,
 #################
 
 @present TheoryRelationDiagram <: TheoryUWD begin
-  Name::Data
-  name::Attr(Box,Name)
-  variable::Attr(Junction,Name)
+  Name::Ob
+  name::Hom(Box,Name)
+  variable::Hom(Junction,Name)
 end
 
-const AbstractUntypedRelationDiagram{T} = AbstractACSet{SchemaType(TheoryRelationDiagram)...,Tuple{T}}
-const UntypedRelationDiagram{T} = ACSet{SchemaType(TheoryRelationDiagram)..., Tuple{T},
-  (:box, :junction, :outer_junction, :variable)}
+const RelationDiagram = AbstractCSetType(TheoryRelationDiagram, data=[:Name])
+const UntypedRelationDiagram = CSetType(
+  TheoryRelationDiagram, data=[:Name],
+  index=[:box, :junction, :outer_junction], unique_index=[:variable])
 
 @present TheoryTypedRelationDiagram <: TheoryTypedUWD begin
-  Name::Data
-  name::Attr(Box,Name)
-  variable::Attr(Junction,Name)
+  Name::Ob
+  name::Hom(Box,Name)
+  variable::Hom(Junction,Name)
 end
 
-const AbstractTypedRelationDiagram{Types,Names} = AbstractACSet{SchemaType(TheoryTypedRelationDiagram)...,
-                                                                Tuple{Types,Names}}
-const TypedRelationDiagram{Types,Names} = ACSet{SchemaType(TheoryTypedRelationDiagram)...,
-                                      Tuple{Types,Names}, (:box, :junction, :outer_junction, :variable)}
+const TypedRelationDiagram = CSetType(
+  TheoryTypedRelationDiagram, data=[:Name, :Type],
+  index=[:box, :junction, :outer_junction], unique_index=[:variable])
 
-RelationDiagram(ports::Int) = UndirectedWiringDiagram(UntypedRelationDiagram{Symbol}, ports)
-RelationDiagram(ports::Vector{T}) where {T} = UndirectedWiringDiagram(TypedRelationDiagram{T,Symbol}, ports)
+function UndirectedWiringDiagram(::Type{RelationDiagram}, nports::Int)
+  UndirectedWiringDiagram(UntypedRelationDiagram, nports,
+                          name=Symbol, variable=Symbol)
+end
+function UndirectedWiringDiagram(::Type{RelationDiagram},
+                                 port_types::AbstractVector{Symbol})
+  UndirectedWiringDiagram(TypedRelationDiagram, port_types,
+                          name=Symbol, variable=Symbol)
+end
+
+RelationDiagram(ports) = UndirectedWiringDiagram(RelationDiagram, ports)
 
 # Relations
 ###########
@@ -88,12 +96,6 @@ function parse_relation_diagram(head::Expr, body::Expr)
   end
 end
 
-function assert_unique(xs)
-  @assert length(xs) == 1 "variables must be unique"
-  xs[1]
-end
-
-
 function make_relation_diagram(context::AbstractVector, args::AbstractVector,
                                body::AbstractVector)
   all_vars, all_types = parse_context(context)
@@ -110,7 +112,7 @@ function make_relation_diagram(context::AbstractVector, args::AbstractVector,
   d = RelationDiagram(var_types(outer_vars))
   add_junctions!(d, var_types(all_vars), variable=all_vars)
   set_junction!(d, ports(d, outer=true),
-                assert_unique.(incident(d, outer_vars, :variable)), outer=true)
+                incident(d, outer_vars, :variable), outer=true)
 
   # Add boxes to diagram.
   for expr in body
@@ -120,7 +122,7 @@ function make_relation_diagram(context::AbstractVector, args::AbstractVector,
     end
     vars ⊆ all_vars || error("One of variables $vars is not declared")
     box = add_box!(d, var_types(vars), name=name)
-    set_junction!(d, ports(d, box), assert_unique.(incident(d, vars, :variable)))
+    set_junction!(d, ports(d, box), incident(d, vars, :variable))
   end
 
   return d
@@ -211,10 +213,10 @@ function parse_tensor_network(expr::Expr; all_vars=nothing)
   d = RelationDiagram(length(outer_vars))
   add_junctions!(d, length(all_vars), variable=all_vars)
   set_junction!(d, ports(d, outer=true),
-                assert_unique.(incident(d, outer_vars, :variable)), outer=true)
+                incident(d, outer_vars, :variable), outer=true)
   for (name, vars) in names_and_vars
     box = add_box!(d, length(vars), name=name)
-    set_junction!(d, ports(d, box), assert_unique.(incident(d, vars, :variable)))
+    set_junction!(d, ports(d, box), incident(d, vars, :variable))
   end
   return d
 end
diff --git a/src/theories/Category.jl b/src/theories/Category.jl
index f4a7c7392..666739528 100644
--- a/src/theories/Category.jl
+++ b/src/theories/Category.jl
@@ -16,7 +16,7 @@ We retain the usual meaning of the symbol ∘ (\\circ), i.e., g∘f = compose(f,
 This usage is too entrenched to overturn, inconvenient though it may be.
 We use symbol ⋅ (\\cdot) for diagrammatic composition: f⋅g = compose(f,g).
 """
-@theory Category{Ob,Hom} begin
+@theory Category(Ob,Hom) begin
   # Unicode aliases.
   @op begin
     (→) := Hom
@@ -43,7 +43,7 @@ end
 compose(fs::Vector) = foldl(compose, fs)
 compose(f, g, h, fs...) = compose([f, g, h, fs...])
 
-@syntax FreeCategory{ObExpr,HomExpr} Category begin
+@syntax FreeCategory(ObExpr,HomExpr) Category begin
   compose(f::Hom, g::Hom) = associate(new(f,g; strict=true))
 end
 
@@ -79,7 +79,7 @@ end
 
 """ Theory of (strict) *2-categories*
 """
-@signature Category2{Ob,Hom,Hom2} <: Category{Ob,Hom} begin
+@signature Category(Ob,Hom) => Category2(Ob,Hom,Hom2) begin
   """ 2-morphism in a 2-category """
   Hom2(dom::Hom(A,B), codom::Hom(A,B))::TYPE ⊣ (A::Ob, B::Ob)
   @op (⇒) := Hom2
@@ -102,7 +102,7 @@ compose2(α, β, γ, αs...) = compose2([α, β, γ, αs...])
 
 Checks domains of morphisms but not 2-morphisms.
 """
-@syntax FreeCategory2{ObExpr,HomExpr,Hom2Expr} Category2 begin
+@syntax FreeCategory2(ObExpr,HomExpr,Hom2Expr) Category2 begin
   compose(f::Hom, g::Hom) = associate(new(f,g; strict=true))
   compose(α::Hom2, β::Hom2) = associate(new(α,β))
   compose2(α::Hom2, β::Hom2) = associate(new(α,β))
diff --git a/src/theories/Limits.jl b/src/theories/Limits.jl
index a97084b9d..dd5da31cd 100644
--- a/src/theories/Limits.jl
+++ b/src/theories/Limits.jl
@@ -11,13 +11,11 @@ export CategoryWithProducts, ob, terminal, delete, product, proj1, proj2, pair,
 Finite products are presented in biased style, via the nullary case (terminal
 objects) and the binary case (binary products). The equational axioms are
 standard, especially in type theory (Lambek & Scott, 1986, Section 0.5 or
-Section I.3). Strictly speaking, this theory is not of a "category with finite
-products" (a category in which finite products exist) but of a "category with
-*chosen* finite products".
+Section I.3).
 
 For a monoidal category axiomatization, see [`CartesianCategory`](@ref).
 """
-@theory CategoryWithProducts{Ob,Hom,Terminal,Product} <: Category{Ob,Hom} begin
+@theory Category(Ob,Hom) => CategoryWithProducts(Ob,Hom,Terminal,Product) begin
   Terminal()::TYPE
   Product(foot1::Ob, foot2::Ob)::TYPE
   
@@ -50,12 +48,10 @@ end
 
 Finite limits are presented in biased style, via finite products and equalizers.
 The equational axioms for equalizers are obscure, but can found in (Lambek &
-Scott, 1986, Section 0.5), which follow "Burroni's pioneering ideas". Strictly
-speaking, this theory is not of a "finitely complete category" (a category in
-which finite limits exist) but of a "category with *chosen* finite limits".
+Scott, 1986, Section 0.5), apparently following "Burroni's pioneering ideas".
 """
-@theory CompleteCategory{Ob,Hom,Terminal,Product,Equalizer} <:
-    CategoryWithProducts{Ob,Hom,Terminal,Product} begin
+@theory CategoryWithProducts(Ob,Hom,Terminal,Product) =>
+    CompleteCategory(Ob,Hom,Terminal,Product,Equalizer) begin
   Equalizer(f::(A → B), g::(A → B))::TYPE ⊣ (A::Ob, B::Ob)
   
   # Equalizers.
@@ -91,7 +87,7 @@ of [`CategoryWithProducts`](@ref).
 
 For a monoidal category axiomatization, see [`CocartesianCategory`](@ref).
 """
-@theory CategoryWithCoproducts{Ob,Hom,Initial,Coproduct} <: Category{Ob,Hom} begin
+@theory Category(Ob,Hom) => CategoryWithCoproducts(Ob,Hom,Initial,Coproduct) begin
   Initial()::TYPE
   Coproduct(foot1::Ob, foot2::Ob)::TYPE
 
@@ -125,8 +121,8 @@ end
 Finite colimits are presented in biased style, via finite coproducts and
 coequalizers. The axioms are dual to those of [`CompleteCategory`](@ref).
 """
-@theory CocompleteCategory{Ob,Hom,Initial,Coproduct,Coequalizer} <:
-    CategoryWithCoproducts{Ob,Hom,Initial,Coproduct} begin
+@theory CategoryWithCoproducts(Ob,Hom,Initial,Coproduct) =>
+    CocompleteCategory(Ob,Hom,Initial,Coproduct,Coequalizer) begin
   Coequalizer(f::(A → B), g::(A → B))::TYPE ⊣ (A::Ob, B::Ob)
   
   # Coequalizers.
diff --git a/src/theories/Monoidal.jl b/src/theories/Monoidal.jl
index a3f163acd..ad4d29916 100644
--- a/src/theories/Monoidal.jl
+++ b/src/theories/Monoidal.jl
@@ -24,7 +24,7 @@ To avoid associators and unitors, we assume the monoidal category is *strict*.
 By the coherence theorem there is no loss of generality, but we may add a
 theory for weak monoidal categories later.
 """
-@signature MonoidalCategory{Ob,Hom} <: Category{Ob,Hom} begin
+@signature Category(Ob,Hom) => MonoidalCategory(Ob,Hom) begin
   otimes(A::Ob, B::Ob)::Ob
   otimes(f::(A → B), g::(C → D))::((A ⊗ C) → (B ⊗ D)) ⊣
     (A::Ob, B::Ob, C::Ob, D::Ob)
@@ -69,12 +69,12 @@ show_latex(io::IO, expr::ObExpr{:munit}; kw...) = print(io, "I")
 
 The theory (but not the axioms) is the same as a braided monoidal category.
 """
-@signature SymmetricMonoidalCategory{Ob,Hom} <: MonoidalCategory{Ob,Hom} begin
+@signature MonoidalCategory(Ob,Hom) => SymmetricMonoidalCategory(Ob,Hom) begin
   braid(A::Ob, B::Ob)::((A ⊗ B) → (B ⊗ A))
   @op (σ) := braid
 end
 
-@syntax FreeSymmetricMonoidalCategory{ObExpr,HomExpr} SymmetricMonoidalCategory begin
+@syntax FreeSymmetricMonoidalCategory(ObExpr,HomExpr) SymmetricMonoidalCategory begin
   otimes(A::Ob, B::Ob) = associate_unit(new(A,B), munit)
   otimes(f::Hom, g::Hom) = associate(new(f,g))
   compose(f::Hom, g::Hom) = associate(new(f,g; strict=true))
@@ -102,7 +102,7 @@ References:
   Section 6.6: "Cartesian center"
 - Selinger, 1999, "Categorical structure of asynchrony"
 """
-@signature MonoidalCategoryWithDiagonals{Ob,Hom} <: SymmetricMonoidalCategory{Ob,Hom} begin
+@signature SymmetricMonoidalCategory(Ob,Hom) => MonoidalCategoryWithDiagonals(Ob,Hom) begin
   mcopy(A::Ob)::(A → (A ⊗ A))
   @op (Δ) := mcopy
   delete(A::Ob)::(A → munit())
@@ -114,7 +114,7 @@ end
 For the traditional axiomatization of products, see
 [`CategoryWithProducts`](@ref).
 """
-@theory CartesianCategory{Ob,Hom} <: MonoidalCategoryWithDiagonals{Ob,Hom} begin
+@theory MonoidalCategoryWithDiagonals(Ob,Hom) => CartesianCategory(Ob,Hom) begin
   pair(f::(A → B), g::(A → C))::(A → (B ⊗ C)) ⊣ (A::Ob, B::Ob, C::Ob)
   proj1(A::Ob, B::Ob)::((A ⊗ B) → A)
   proj2(A::Ob, B::Ob)::((A ⊗ B) → B)
@@ -134,7 +134,7 @@ In this syntax, the pairing and projection operations are defined using
 duplication and deletion, and do not have their own syntactic elements.
 This convention could be dropped or reversed.
 """
-@syntax FreeCartesianCategory{ObExpr,HomExpr} CartesianCategory begin
+@syntax FreeCartesianCategory(ObExpr,HomExpr) CartesianCategory begin
   otimes(A::Ob, B::Ob) = associate_unit(new(A,B), munit)
   otimes(f::Hom, g::Hom) = associate(new(f,g))
   compose(f::Hom, g::Hom) = associate(new(f,g; strict=true))
@@ -160,8 +160,7 @@ The terminology is nonstandard (is there any standard terminology?) but is
 supposed to mean a monoidal category with coherent diagonals and codiagonals.
 Unlike in a biproduct category, the naturality axioms need not be satisfied.
 """
-@signature MonoidalCategoryWithBidiagonals{Ob,Hom} <:
-    MonoidalCategoryWithDiagonals{Ob,Hom} begin
+@signature MonoidalCategoryWithDiagonals(Ob,Hom) => MonoidalCategoryWithBidiagonals(Ob,Hom) begin
   mmerge(A::Ob)::((A ⊗ A) → A)
   @op (∇) := mmerge
   create(A::Ob)::(munit() → A)
@@ -173,7 +172,7 @@ end
 Mathematically the same as [`SemiadditiveCategory`](@ref) but written
 multiplicatively, instead of additively.
 """
-@theory BiproductCategory{Ob,Hom} <: MonoidalCategoryWithBidiagonals{Ob,Hom} begin
+@theory MonoidalCategoryWithBidiagonals(Ob,Hom) => BiproductCategory(Ob,Hom) begin
   pair(f::(A → B), g::(A → C))::(A → (B ⊗ C)) ⊣ (A::Ob, B::Ob, C::Ob)
   copair(f::(A → C), g::(B → C))::((A ⊗ B) → C) ⊣ (A::Ob, B::Ob, C::Ob)
   proj1(A::Ob, B::Ob)::((A ⊗ B) → A)
@@ -194,7 +193,7 @@ multiplicatively, instead of additively.
   □(A)⋅◊(A) == id(munit()) ⊣ (A::Ob)
 end
 
-@syntax FreeBiproductCategory{ObExpr,HomExpr} BiproductCategory begin
+@syntax FreeBiproductCategory(ObExpr,HomExpr) BiproductCategory begin
   otimes(A::Ob, B::Ob) = associate_unit(new(A,B), munit)
   otimes(f::Hom, g::Hom) = associate(new(f,g))
   compose(f::Hom, g::Hom) = associate(new(f,g; strict=true))
@@ -219,7 +218,7 @@ end
 
 """ Theory of (symmetric) *closed monoidal categories*
 """
-@signature ClosedMonoidalCategory{Ob,Hom} <: SymmetricMonoidalCategory{Ob,Hom} begin
+@signature SymmetricMonoidalCategory(Ob,Hom) => ClosedMonoidalCategory(Ob,Hom) begin
   # Internal hom of A and B, an object representing Hom(A,B)
   hom(A::Ob, B::Ob)::Ob
 
@@ -232,7 +231,7 @@ end
 
 """ Syntax for a free closed monoidal category.
 """
-@syntax FreeClosedMonoidalCategory{ObExpr,HomExpr} ClosedMonoidalCategory begin
+@syntax FreeClosedMonoidalCategory(ObExpr,HomExpr) ClosedMonoidalCategory begin
   otimes(A::Ob, B::Ob) = associate_unit(new(A,B), munit)
   otimes(f::Hom, g::Hom) = associate(new(f,g))
   compose(f::Hom, g::Hom) = associate(new(f,g; strict=true))
@@ -263,7 +262,7 @@ A CCC is a cartesian category with internal homs (aka, exponential objects).
 FIXME: This theory should also extend `ClosedMonoidalCategory`, but multiple
 inheritance is not yet supported.
 """
-@signature CartesianClosedCategory{Ob,Hom} <: CartesianCategory{Ob,Hom} begin
+@signature CartesianCategory(Ob,Hom) => CartesianClosedCategory(Ob,Hom) begin
   hom(A::Ob, B::Ob)::Ob
   ev(A::Ob, B::Ob)::((hom(A,B) ⊗ A) → B)
   curry(A::Ob, B::Ob, f::((A ⊗ B) → C))::(A → hom(B,C)) ⊣ (C::Ob)
@@ -273,7 +272,7 @@ end
 
 See also `FreeCartesianCategory`.
 """
-@syntax FreeCartesianClosedCategory{ObExpr,HomExpr} CartesianClosedCategory begin
+@syntax FreeCartesianClosedCategory(ObExpr,HomExpr) CartesianClosedCategory begin
   otimes(A::Ob, B::Ob) = associate_unit(new(A,B), munit)
   otimes(f::Hom, g::Hom) = associate(new(f,g))
   compose(f::Hom, g::Hom) = associate(new(f,g; strict=true))
@@ -288,7 +287,7 @@ end
 
 """ Theory of *compact closed categories*
 """
-@theory CompactClosedCategory{Ob,Hom} <: ClosedMonoidalCategory{Ob,Hom} begin
+@theory ClosedMonoidalCategory(Ob,Hom) => CompactClosedCategory(Ob,Hom) begin
   # Dual A^* of object A
   dual(A::Ob)::Ob
 
@@ -308,7 +307,7 @@ end
    ⊣ (A::Ob, B::Ob, C::Ob, f::((A ⊗ B) → C)))
 end
 
-@syntax FreeCompactClosedCategory{ObExpr,HomExpr} CompactClosedCategory begin
+@syntax FreeCompactClosedCategory(ObExpr,HomExpr) CompactClosedCategory begin
   dual(A::Ob) = distribute_unary(involute(new(A)), dual, otimes,
                                  unit=munit, contravariant=true)
   otimes(A::Ob, B::Ob) = associate_unit(new(A,B), munit)
@@ -347,11 +346,11 @@ end
 
 """ Theory of *dagger categories*
 """
-@signature DaggerCategory{Ob,Hom} <: Category{Ob,Hom} begin
+@signature Category(Ob,Hom) => DaggerCategory(Ob,Hom) begin
   dagger(f::(A → B))::(B → A) ⊣ (A::Ob, B::Ob)
 end
 
-@syntax FreeDaggerCategory{ObExpr,HomExpr} DaggerCategory begin
+@syntax FreeDaggerCategory(ObExpr,HomExpr) DaggerCategory begin
   compose(f::Hom, g::Hom) = associate(new(f,g; strict=true))
   dagger(f::Hom) = distribute_dagger(involute(new(f)))
 end
@@ -370,11 +369,11 @@ category](https://ncatlab.org/nlab/show/symmetric+monoidal+dagger-category).
 FIXME: This theory should also extend `DaggerCategory`, but multiple inheritance
 is not yet supported.
 """
-@signature DaggerSymmetricMonoidalCategory{Ob,Hom} <: SymmetricMonoidalCategory{Ob,Hom} begin
+@signature SymmetricMonoidalCategory(Ob,Hom) => DaggerSymmetricMonoidalCategory(Ob,Hom) begin
   dagger(f::(A → B))::(B → A) ⊣ (A::Ob, B::Ob)
 end
 
-@syntax FreeDaggerSymmetricMonoidalCategory{ObExpr,HomExpr} DaggerSymmetricMonoidalCategory begin
+@syntax FreeDaggerSymmetricMonoidalCategory(ObExpr,HomExpr) DaggerSymmetricMonoidalCategory begin
   otimes(A::Ob, B::Ob) = associate_unit(new(A,B), munit)
   otimes(f::Hom, g::Hom) = associate(new(f,g))
   compose(f::Hom, g::Hom) = associate(new(f,g; strict=true))
@@ -396,11 +395,11 @@ categories.
 FIXME: This theory should also extend `DaggerCategory`, but multiple inheritance
 is not yet supported.
 """
-@signature DaggerCompactCategory{Ob,Hom} <: CompactClosedCategory{Ob,Hom} begin
+@signature CompactClosedCategory(Ob,Hom) => DaggerCompactCategory(Ob,Hom) begin
   dagger(f::(A → B))::(B → A) ⊣ (A::Ob, B::Ob)
 end
 
-@syntax FreeDaggerCompactCategory{ObExpr,HomExpr} DaggerCompactCategory begin
+@syntax FreeDaggerCompactCategory(ObExpr,HomExpr) DaggerCompactCategory begin
   dual(A::Ob) = distribute_unary(involute(new(A)), dual, otimes,
                                  unit=munit, contravariant=true)
   otimes(A::Ob, B::Ob) = associate_unit(new(A,B), munit)
@@ -420,11 +419,11 @@ end
 
 """ Theory of *traced monoidal categories*
 """
-@signature TracedMonoidalCategory{Ob,Hom} <: SymmetricMonoidalCategory{Ob,Hom} begin
+@signature SymmetricMonoidalCategory(Ob,Hom) => TracedMonoidalCategory(Ob,Hom) begin
   trace(X::Ob, A::Ob, B::Ob, f::((X ⊗ A) → (X ⊗ B)))::(A → B)
 end
 
-@syntax FreeTracedMonoidalCategory{ObExpr,HomExpr} TracedMonoidalCategory begin
+@syntax FreeTracedMonoidalCategory(ObExpr,HomExpr) TracedMonoidalCategory begin
   otimes(A::Ob, B::Ob) = associate_unit(new(A,B), munit)
   otimes(f::Hom, g::Hom) = associate(new(f,g))
   compose(f::Hom, g::Hom) = associate(new(f,g; strict=true))
@@ -448,7 +447,7 @@ categories" and "spidered/dungeon categories", among other things.
 FIXME: Should also inherit `ClosedMonoidalCategory` and `DaggerCategory`, but
 multiple inheritance is not yet supported.
 """
-@theory HypergraphCategory{Ob,Hom} <: MonoidalCategoryWithBidiagonals{Ob,Hom} begin
+@theory MonoidalCategoryWithBidiagonals(Ob,Hom) => HypergraphCategory(Ob,Hom) begin
   # Self-dual compact closed category.
   dunit(A::Ob)::(munit() → (A ⊗ A))
   dcounit(A::Ob)::((A ⊗ A) → munit())
diff --git a/src/theories/MonoidalAdditive.jl b/src/theories/MonoidalAdditive.jl
index 090bdf360..fa5807992 100644
--- a/src/theories/MonoidalAdditive.jl
+++ b/src/theories/MonoidalAdditive.jl
@@ -16,7 +16,7 @@ import Base: collect, ndims, +, zero
 Mathematically the same as [`MonoidalCategory`](@ref) but with different
 notation.
 """
-@signature MonoidalCategoryAdditive{Ob,Hom} <: Category{Ob,Hom} begin
+@signature Category(Ob,Hom) => MonoidalCategoryAdditive(Ob,Hom) begin
   oplus(A::Ob, B::Ob)::Ob
   oplus(f::(A → B), g::(C → D))::((A ⊕ C) → (B ⊕ D)) <=
     (A::Ob, B::Ob, C::Ob, D::Ob)
@@ -52,12 +52,11 @@ show_latex(io::IO, expr::ObExpr{:mzero}; kw...) = print(io, "O")
 Mathematically the same as [`SymmetricMonoidalCategory`](@ref) but with
 different notation.
 """
-@signature SymmetricMonoidalCategoryAdditive{Ob,Hom} <:
-    MonoidalCategoryAdditive{Ob,Hom} begin
+@signature MonoidalCategoryAdditive(Ob,Hom) => SymmetricMonoidalCategoryAdditive(Ob,Hom) begin
   swap(A::Ob, B::Ob)::Hom(oplus(A,B),oplus(B,A))
 end
 
-@syntax FreeSymmetricMonoidalCategoryAdditive{ObExpr,HomExpr} SymmetricMonoidalCategoryAdditive begin
+@syntax FreeSymmetricMonoidalCategoryAdditive(ObExpr,HomExpr) SymmetricMonoidalCategoryAdditive begin
   oplus(A::Ob, B::Ob) = associate_unit(new(A,B), mzero)
   oplus(f::Hom, g::Hom) = associate(new(f,g))
   compose(f::Hom, g::Hom) = associate(new(f,g; strict=true))
@@ -78,8 +77,7 @@ Unlike in a cocartesian category, the naturality axioms need not be satisfied.
 
 For references, see [`MonoidalCategoryWithDiagonals`](@ref).
 """
-@theory MonoidalCategoryWithCodiagonals{Ob,Hom} <:
-    SymmetricMonoidalCategoryAdditive{Ob,Hom} begin
+@theory SymmetricMonoidalCategoryAdditive(Ob,Hom) => MonoidalCategoryWithCodiagonals(Ob,Hom) begin
   plus(A::Ob)::((A ⊕ A) → A)
   zero(A::Ob)::(mzero() → A)
   
@@ -95,7 +93,7 @@ end
 For the traditional axiomatization of coproducts, see
 [`CategoryWithCoproducts`](@ref).
 """
-@theory CocartesianCategory{Ob,Hom} <: MonoidalCategoryWithCodiagonals{Ob,Hom} begin
+@theory MonoidalCategoryWithCodiagonals(Ob,Hom) => CocartesianCategory(Ob,Hom) begin
   copair(f::(A → C), g::(B → C))::((A ⊕ B) → C) <= (A::Ob, B::Ob, C::Ob)
   coproj1(A::Ob, B::Ob)::(A → (A ⊕ B))
   coproj2(A::Ob, B::Ob)::(B → (A ⊕ B))
@@ -115,7 +113,7 @@ In this syntax, the copairing and inclusion operations are defined using merging
 and creation, and do not have their own syntactic elements. This convention
 could be dropped or reversed.
 """
-@syntax FreeCocartesianCategory{ObExpr,HomExpr} CocartesianCategory begin
+@syntax FreeCocartesianCategory(ObExpr,HomExpr) CocartesianCategory begin
   oplus(A::Ob, B::Ob) = associate_unit(new(A,B), mzero)
   oplus(f::Hom, g::Hom) = associate(new(f,g))
   compose(f::Hom, g::Hom) = associate(new(f,g; strict=true))
@@ -145,8 +143,8 @@ end
 Mathematically the same as [`MonoidalCategoryWithBidiagonals`](@ref) but written
 additively, instead of multiplicatively.
 """
-@theory MonoidalCategoryWithBidiagonalsAdditive{Ob,Hom} <:
-    MonoidalCategoryWithCodiagonals{Ob,Hom} begin
+@theory MonoidalCategoryWithCodiagonals(Ob,Hom) =>
+    MonoidalCategoryWithBidiagonalsAdditive(Ob,Hom) begin
   mcopy(A::Ob)::(A → (A ⊕ A))
   @op (Δ) := mcopy
   delete(A::Ob)::(A → mzero())
@@ -164,8 +162,8 @@ end
 Mathematically the same as [`BiproductCategory`](@ref) but written additively,
 instead of multiplicatively.
 """
-@theory SemiadditiveCategory{Ob,Hom} <:
-    MonoidalCategoryWithBidiagonalsAdditive{Ob,Hom} begin
+@theory MonoidalCategoryWithBidiagonalsAdditive(Ob,Hom) =>
+    SemiadditiveCategory(Ob,Hom) begin
   pair(f::(A → B), g::(A → C))::(A → (B ⊕ C)) ⊣ (A::Ob, B::Ob, C::Ob)
   copair(f::(A → C), g::(B → C))::((A ⊕ B) → C) ⊣ (A::Ob, B::Ob, C::Ob)
   proj1(A::Ob, B::Ob)::((A ⊕ B) → A)
@@ -197,8 +195,8 @@ end
 Mathematically the same as [`HypergraphCategory`](@ref) but with different
 notation.
 """
-@signature HypergraphCategoryAdditive{Ob,Hom} <:
-    SymmetricMonoidalCategoryAdditive{Ob,Hom} begin
+@signature SymmetricMonoidalCategoryAdditive(Ob,Hom) =>
+    HypergraphCategoryAdditive(Ob,Hom) begin
   # Supply of Frobenius monoids.
   mcopy(A::Ob)::(A → (A ⊕ A))
   @op (Δ) := mcopy
diff --git a/src/theories/MonoidalMultiple.jl b/src/theories/MonoidalMultiple.jl
index dd6d57e40..ae3e6a180 100644
--- a/src/theories/MonoidalMultiple.jl
+++ b/src/theories/MonoidalMultiple.jl
@@ -16,7 +16,7 @@ ignore coherence isomorphisms such as associators and unitors.
 FIXME: This theory should also inherit `MonoidalCategory`, but multiple
 inheritance is not supported.
 """
-@signature RigCategory{Ob,Hom} <: SymmetricMonoidalCategoryAdditive{Ob,Hom} begin
+@signature SymmetricMonoidalCategoryAdditive(Ob,Hom) => RigCategory(Ob,Hom) begin
   otimes(A::Ob, B::Ob)::Ob
   otimes(f::(A → B), g::(C → D))::((A ⊗ C) → (B ⊗ D)) ⊣
     (A::Ob, B::Ob, C::Ob, D::Ob)
@@ -28,7 +28,7 @@ end
 
 FIXME: Should also inherit `SymmetricMonoidalCategory`.
 """
-@signature SymmetricRigCategory{Ob,Hom} <: RigCategory{Ob,Hom} begin
+@signature RigCategory(Ob,Hom) => SymmetricRigCategory(Ob,Hom) begin
   braid(A::Ob, B::Ob)::((A ⊗ B) → (B ⊗ A))
   @op (σ) := braid
 end
@@ -40,7 +40,7 @@ universal invariants", Section 3.2
 
 FIXME: Should also inherit `CocartesianCategory`.
 """
-@theory DistributiveMonoidalCategory{Ob,Hom} <: SymmetricRigCategory{Ob,Hom} begin
+@theory SymmetricRigCategory(Ob,Hom) => DistributiveMonoidalCategory(Ob,Hom) begin
   plus(A::Ob)::((A ⊕ A) → A)
   zero(A::Ob)::(mzero() → A)
   
@@ -61,8 +61,8 @@ end
 
 FIXME: Should also inherit `MonoidalCategoryWithDiagonals`.
 """
-@theory DistributiveMonoidalCategoryWithDiagonals{Ob,Hom} <:
-    DistributiveMonoidalCategory{Ob,Hom} begin
+@theory DistributiveMonoidalCategory(Ob,Hom) =>
+    DistributiveMonoidalCategoryWithDiagonals(Ob,Hom) begin
   mcopy(A::Ob)::(A → (A ⊗ A))
   @op (Δ) := mcopy
   delete(A::Ob)::(A → munit())
@@ -78,7 +78,7 @@ biproduct.
 
 FIXME: Should also inherit `SemiadditiveCategory`
 """
-@theory DistributiveSemiadditiveCategory{Ob,Hom} <: DistributiveMonoidalCategory{Ob,Hom} begin
+@theory DistributiveMonoidalCategory(Ob,Hom) => DistributiveSemiadditiveCategory(Ob,Hom) begin
   mcopy(A::Ob)::(A → (A ⊕ A))
   @op (Δ) := mcopy
   delete(A::Ob)::(A → mzero())
@@ -100,7 +100,7 @@ is the cartesian product, see [`DistributiveMonoidalCategory`](@ref).
 
 FIXME: Should also inherit `CartesianCategory`.
 """
-@theory DistributiveCategory{Ob,Hom} <: DistributiveMonoidalCategoryWithDiagonals{Ob,Hom} begin
+@theory DistributiveMonoidalCategoryWithDiagonals(Ob,Hom) => DistributiveCategory(Ob,Hom) begin
   pair(f::(A → B), g::(A → C))::(A → (B ⊗ C)) ⊣ (A::Ob, B::Ob, C::Ob)
   proj1(A::Ob, B::Ob)::((A ⊗ B) → A)
   proj2(A::Ob, B::Ob)::((A ⊗ B) → B)
diff --git a/src/theories/Preorders.jl b/src/theories/Preorders.jl
index e536c1dea..dc2ba85bd 100644
--- a/src/theories/Preorders.jl
+++ b/src/theories/Preorders.jl
@@ -9,19 +9,19 @@ export ThinCategory, FreeThinCategory,
 
 Thin categories have at most one morphism between any two objects.
 """
-@theory ThinCategory{Ob,Hom} <: Category{Ob,Hom} begin
+@theory Category(Ob,Hom) => ThinCategory(Ob,Hom) begin
   f == g ⊣ (A::Ob, B::Ob, f::Hom(A,B), g::Hom(A,B))
 end
 
-@syntax FreeThinCategory{ObExpr,HomExpr} ThinCategory begin
+@syntax FreeThinCategory(ObExpr,HomExpr) ThinCategory begin
   compose(f::Hom, g::Hom) = associate(new(f,g; strict=true))
 end
 
-@theory ThinSymmetricMonoidalCategory{Ob,Hom} <: SymmetricMonoidalCategory{Ob,Hom} begin
+@theory SymmetricMonoidalCategory(Ob,Hom) => ThinSymmetricMonoidalCategory(Ob,Hom) begin
   f == g ⊣ (A::Ob, B::Ob, f::Hom(A,B), g::Hom(A,B))
 end
 
-@syntax FreeThinSymmetricMonoidalCategory{ObExpr,HomExpr} ThinSymmetricMonoidalCategory begin
+@syntax FreeThinSymmetricMonoidalCategory(ObExpr,HomExpr) ThinSymmetricMonoidalCategory begin
   compose(f::Hom, g::Hom) = associate(new(f,g; strict=true))
   otimes(A::Ob, B::Ob) = associate_unit(new(A,B), munit)
   otimes(f::Hom, g::Hom) = associate(new(f,g))
@@ -34,7 +34,7 @@ end
 
 Preorders encode the axioms of reflexivity and transitivity as term constructors.
 """
-@theory Preorder{Elt,Leq} begin
+@theory Preorder(Elt,Leq) begin
   Elt::TYPE
   Leq(lhs::Elt, rhs::Elt)::TYPE
   @op (≤) := Leq
@@ -48,7 +48,7 @@ Preorders encode the axioms of reflexivity and transitivity as term constructors
   # Read as (f⟹ A≤B ∧ g⟹ A≤B) ⟹ f ≡ g
 end
 
-@syntax FreePreorder{ObExpr,HomExpr} Preorder begin
+@syntax FreePreorder(ObExpr,HomExpr) Preorder begin
   transitive(f::Leq, g::Leq) = associate(new(f,g; strict=true))
 end
 
diff --git a/src/theories/Relations.jl b/src/theories/Relations.jl
index abf8dfd75..18c61611e 100644
--- a/src/theories/Relations.jl
+++ b/src/theories/Relations.jl
@@ -18,13 +18,13 @@ References:
 - Walters, 2009, blog post, "Categorical algebras of relations",
   http://rfcwalters.blogspot.com/2009/10/categorical-algebras-of-relations.html
 """
-@signature BicategoryRelations{Ob,Hom} <: HypergraphCategory{Ob,Hom} begin
+@signature HypergraphCategory(Ob,Hom) => BicategoryRelations(Ob,Hom) begin
   # Logical operations.
   meet(R::(A → B), S::(A → B))::(A → B) ⊣ (A::Ob, B::Ob)
   top(A::Ob, B::Ob)::(A → B)
 end
 
-@syntax FreeBicategoryRelations{ObExpr,HomExpr} BicategoryRelations begin
+@syntax FreeBicategoryRelations(ObExpr,HomExpr) BicategoryRelations begin
   otimes(A::Ob, B::Ob) = associate_unit(new(A,B), munit)
   otimes(R::Hom, S::Hom) = associate(new(R,S))
   compose(R::Hom, S::Hom) = associate(new(R,S; strict=true))
@@ -43,7 +43,8 @@ References:
 - Carboni & Walters, 1987, "Cartesian bicategories I", Sec. 5
 - Baez & Erbele, 2015, "Categories in control"
 """
-@signature AbelianBicategoryRelations{Ob,Hom} <: HypergraphCategoryAdditive{Ob,Hom} begin
+@signature HypergraphCategoryAdditive(Ob,Hom) =>
+    AbelianBicategoryRelations(Ob,Hom) begin
   # Second supply of Frobenius monoids.
   plus(A::Ob)::((A ⊕ A) → A)
   zero(A::Ob)::(mzero() → A)
@@ -57,7 +58,7 @@ References:
   bottom(A::Ob, B::Ob)::(A → B)
 end
 
-@syntax FreeAbelianBicategoryRelations{ObExpr,HomExpr} AbelianBicategoryRelations begin
+@syntax FreeAbelianBicategoryRelations(ObExpr,HomExpr) AbelianBicategoryRelations begin
   oplus(A::Ob, B::Ob) = associate_unit(new(A,B), mzero)
   oplus(R::Hom, S::Hom) = associate(new(R,S))
   compose(R::Hom, S::Hom) = associate(new(R,S; strict=true))
@@ -80,8 +81,8 @@ References:
 FIXME: Should also inherit `BicategoryOfRelations`, but multiple inheritance is
 not yet supported.
 """
-@signature DistributiveBicategoryRelations{Ob,Hom} <:
-    DistributiveMonoidalCategoryWithDiagonals{Ob,Hom} begin
+@signature DistributiveMonoidalCategoryWithDiagonals(Ob,Hom) =>
+    DistributiveBicategoryRelations(Ob,Hom) begin
   # Self-dual dagger compact category.
   dagger(R::(A → B))::(B → A) ⊣ (A::Ob, B::Ob)
   dunit(A::Ob)::(munit() → (A ⊗ A))
diff --git a/src/theories/Schema.jl b/src/theories/Schema.jl
deleted file mode 100644
index 7ae85f3ec..000000000
--- a/src/theories/Schema.jl
+++ /dev/null
@@ -1,200 +0,0 @@
-export Schema, FreeSchema, Data, Attr, SchemaExpr, DataExpr, AttrExpr,
-  CatDesc, CatDescType, AttrDesc, AttrDescType, SchemaType,
-  ob_num, hom_num, data_num, attr_num, dom_num, codom_num
-
-using MLStyle
-
-# Schema
-########
-
-""" The GAT that parameterizes Attributed C-sets
-
-A schema is comprised of a category C, a discrete category D, and a profunctor
-Attr : C^op x D → Set. In GAT form, this is given by extending the theory of
-categories with two extra types, Data for objects of D, and Attr, for elements
-of the sets given by the profunctor.
-"""
-@theory Schema{Ob,Hom,Data,Attr} <: Category{Ob,Hom} begin
-  Data::TYPE
-  Attr(dom::Ob,codom::Data)::TYPE
-
-  """ Composition is given by the action of C on the profunctor, with corresponding laws """
-  compose(f::Hom(A,B), g::Attr(B,X))::Attr(A,X) ⊣ (A::Ob, B::Ob, X::Data)
-
-  (compose(f, compose(g, a)) == compose(compose(f, g), a)
-    ⊣ (A::Ob, B::Ob, C::Ob, X::Data, f::Hom(A,B), g::Hom(B,C), a::Attr(C, X)))
-  compose(id(A), a) == a ⊣ (A::Ob, X::Ob, a::Attr(A,X))
-end
-
-abstract type SchemaExpr{T} <: GATExpr{T} end
-abstract type DataExpr{T} <: SchemaExpr{T} end
-abstract type AttrExpr{T} <: SchemaExpr{T} end
-
-@syntax FreeSchema{ObExpr,HomExpr,DataExpr,AttrExpr} Schema begin
-  # should have a normal representation for precompose of a morphism + a generator attribute
-end
-
-# Type-level representation of Schema
-#####################################
-
-""" CatDesc is a type-level description of a category
-
-In order to have Attributed C-sets that are parameterized by schemas, we have to
-use the type of this struct as the type parameter to get around Julia's restrictions
-on what sort of thing can appear as a type parameter.
-
-We split a Schema up into two types: CatDesc (describing the category) and AttrDesc
-(describing the discrete category and the profunctor). This allows us to have
-C-sets as Attributed C-sets with an empty AttrDesc.
-"""
-struct CatDesc{Ob,Hom,Dom,Codom}
-  function CatDesc{Ob,Hom,Dom,Codom}() where
-    {Ob,Hom,Dom,Codom}
-    new{Ob,Hom,Dom,Codom}()
-  end
-  function CatDesc(pres::Presentation{Schema})
-    obs, homs = generators(pres, :Ob), generators(pres,:Hom)
-    ob_syms, hom_syms = nameof.(obs), nameof.(homs)
-    ob_num = ob -> findfirst(ob_syms .== ob)::Int
-    new{Tuple(ob_syms), Tuple(hom_syms),
-        Tuple(@. ob_num(nameof(dom(homs)))), Tuple(@. ob_num(nameof(codom(homs))))}()
-  end
-end
-
-CatDescType(pres::Presentation{Schema}) = typeof(CatDesc(pres))
-
-function Base.getproperty(AD::Type{T},i::Symbol) where
-  {Ob,Hom,Dom,Codom,T<:CatDesc{Ob,Hom,Dom,Codom}}
-  @match i begin
-    :ob => Ob
-    :hom => Hom
-    :dom => Dom
-    :codom => Codom
-    _ => getfield(AD,i)
-  end
-end
-
-function ob_num(CD::Type{T}, ob::Symbol) where {Ob,Hom,Dom,Codom, T <: CatDesc{Ob,Hom,Dom,Codom}}
-  findfirst(Ob .== ob)::Int
-end
-
-function hom_num(CD::Type{T}, hom::Symbol) where {Ob,Hom,Dom,Codom, T <: CatDesc{Ob,Hom,Dom,Codom}}
-  findfirst(Hom .== hom)::Int
-end
-
-function dom_num(CD::Type{T}, hom::Int) where {Ob,Hom,Dom,Codom, T <: CatDesc{Ob,Hom,Dom,Codom}}
-  Dom[hom]
-end
-
-function dom_num(CD::Type{<:CatDesc}, hom::Symbol)
-  dom_num(CD, hom_num(CD,hom))
-end
-
-function codom_num(CD::Type{T}, hom::Int) where {Ob,Hom,Dom,Codom, T <: CatDesc{Ob,Hom,Dom,Codom}}
-  Codom[hom]
-end
-
-function codom_num(CD::Type{<:CatDesc}, hom::Symbol)
-  codom_num(CD, hom_num(CD,hom))
-end
-
-function dom(CD::Type{T}, hom::Int) where {Ob,Hom,Dom,Codom, T <: CatDesc{Ob,Hom,Dom,Codom}}
-  Ob[dom_num(CD,hom)]
-end
-
-function dom(CD::Type{T}, hom::Symbol) where {Ob,Hom,Dom,Codom, T <: CatDesc{Ob,Hom,Dom,Codom}}
-  Ob[dom_num(CD,hom)]
-end
-
-function codom(CD::Type{T}, hom::Int) where {Ob,Hom,Dom,Codom, T <: CatDesc{Ob,Hom,Dom,Codom}}
-  Ob[codom_num(CD,hom)]
-end
-
-function codom(CD::Type{T}, hom::Symbol) where {Ob,Hom,Dom,Codom, T <: CatDesc{Ob,Hom,Dom,Codom}}
-  Ob[codom_num(CD,hom)]
-end
-
-""" AttrDesc is a type-level representation of attributes added
-to a category to form a schema
-"""
-struct AttrDesc{CD,Data,Attr,ADom,ACodom}
-  function AttrDesc{CD,Data,Attr,ADom,ACodom}() where {CD,Data,Attr,ADom,ACodom}
-    new{CD,Data,Attr,ADom,ACodom}()
-  end
-  function AttrDesc(pres::Presentation{Schema})
-    CD = CatDescType(pres)
-    datas, attrs = generators(pres, :Data), generators(pres,:Attr)
-    data_syms, attr_syms = nameof.(datas), nameof.(attrs)
-    ob_num = ob -> findfirst(CD.ob .== ob)::Int
-    data_num = ob -> findfirst(data_syms .== ob)::Int
-    new{CD,Tuple(data_syms), Tuple(attr_syms),
-        Tuple(@. ob_num(nameof(dom(attrs)))), Tuple(@. data_num(nameof(codom(attrs))))}()
-  end
-  function AttrDesc(::CatDesc{Ob,Hom,Dom,Codom}) where {Ob,Hom,Dom,Codom}
-    new{CatDesc{Ob,Hom,Dom,Codom},(),(),(),()}
-  end
-end
-
-AttrDescType(pres::Presentation{Schema}) = typeof(AttrDesc(pres))
-
-function Base.getproperty(AD::Type{T}, i::Symbol) where
-  {CD,Data,Attr,ADom,ACodom,T <: AttrDesc{CD,Data,Attr,ADom,ACodom}}
-  @match i begin
-    :cd => CD
-    :data => Data
-    :attr => Attr
-    :adom => ADom
-    :acodom => ACodom
-    _ => getfield(AD,i)
-  end
-end
-
-function data_num(AD::Type{T}, data::Symbol) where
-  {CD,Data,Attr,ADom,ACodom,T <: AttrDesc{CD,Data,Attr,ADom,ACodom}}
-  findfirst(Data .== data)::Int
-end
-
-function attr_num(AD::Type{T}, attr::Symbol) where
-  {CD,Data,Attr,ADom,ACodom,T <: AttrDesc{CD,Data,Attr,ADom,ACodom}}
-  findfirst(Attr .== attr)::Int
-end
-
-function dom_num(AD::Type{T}, attr::Int) where
-  {CD,Data,Attr,ADom,ACodom,T <: AttrDesc{CD,Data,Attr,ADom,ACodom}}
-  ADom[attr]
-end
-
-function dom_num(AD::Type{<:AttrDesc}, attr::Symbol)
-  dom_num(AD,attr_num(AD,attr))
-end
-
-function codom_num(AD::Type{T}, attr::Int) where
-  {CD,Data,Attr,ADom,ACodom,T <: AttrDesc{CD,Data,Attr,ADom,ACodom}}
-  ACodom[attr]
-end
-
-function codom_num(AD::Type{<:AttrDesc}, attr::Symbol)
-  codom_num(AD,attr_num(AD,attr))
-end
-
-function dom(AD::Type{T}, attr::Int) where
-  {CD,Data,Attr,ADom,ACodom,T <: AttrDesc{CD,Data,Attr,ADom,ACodom}}
-  CD.ob[dom_num(AD,attr)]
-end
-
-function dom(AD::Type{T}, attr::Symbol) where
-  {CD,Data,Attr,ADom,ACodom,T <: AttrDesc{CD,Data,Attr,ADom,ACodom}}
-  CD.ob[dom_num(AD,attr)]
-end
-
-function codom(AD::Type{T}, attr::Int) where
-  {CD,Data,Attr,ADom,ACodom,T <: AttrDesc{CD,Data,Attr,ADom,ACodom}}
-  Data[codom_num(AD,attr)]
-end
-
-function codom(AD::Type{T}, attr::Symbol) where
-  {CD,Data,Attr,ADom,ACodom,T <: AttrDesc{CD,Data,Attr,ADom,ACodom}}
-  Data[codom_num(AD,attr)]
-end
-
-SchemaType(pres::Presentation{Schema}) = (CatDescType(pres),AttrDescType(pres))
diff --git a/src/theories/Theories.jl b/src/theories/Theories.jl
index ff350c51d..12e7b1d75 100644
--- a/src/theories/Theories.jl
+++ b/src/theories/Theories.jl
@@ -34,6 +34,5 @@ include("MonoidalAdditive.jl")
 include("MonoidalMultiple.jl")
 include("Preorders.jl")
 include("Relations.jl")
-include("Schema.jl")
 
 end
diff --git a/src/wiring_diagrams/MonoidalDirected.jl b/src/wiring_diagrams/MonoidalDirected.jl
index 1e3c71ed9..be48c1fc0 100644
--- a/src/wiring_diagrams/MonoidalDirected.jl
+++ b/src/wiring_diagrams/MonoidalDirected.jl
@@ -77,7 +77,7 @@ Extra structure, such as copying or merging, can be added to wiring diagrams in
 different ways, but wiring diagrams always form a symmetric monoidal category in
 the same way.
 """
-@instance SymmetricMonoidalCategory{Ports, WiringDiagram} begin
+@instance SymmetricMonoidalCategory(Ports, WiringDiagram) begin
   @import dom, codom
 
   function id(A::Ports)
diff --git a/src/wiring_diagrams/MonoidalUndirected.jl b/src/wiring_diagrams/MonoidalUndirected.jl
index 45273b8e3..c831107dd 100644
--- a/src/wiring_diagrams/MonoidalUndirected.jl
+++ b/src/wiring_diagrams/MonoidalUndirected.jl
@@ -66,7 +66,7 @@ otimes(f::UWD, g::UWD) where UWD <: AbstractUWD = disjoint_union(f, g)
 end
 const ObUWD = UndirectedWiringDiagramOb
 
-ObUWD(types::Vector{T}) where {T} = ObUWD{TypedUndirectedWiringDiagram{T}}(types)
+ObUWD(types::Vector) = ObUWD{TypedUndirectedWiringDiagram}(types)
 
 Base.length(A::ObUWD) = length(A.types)
 Base.cat(A::ObUWD{UWD}, B::ObUWD{UWD}) where UWD =
@@ -114,7 +114,7 @@ codom(f::HomUWD{UWD}) where UWD = ObUWD{UWD}(codom_port_types(f))
 The objects are lists of port types and morphisms are undirected wiring diagrams
 whose outer ports are partitioned into domain and codomain.
 """
-@instance HypergraphCategory{ObUWD, HomUWD} begin
+@instance HypergraphCategory(ObUWD, HomUWD) begin
   @import dom, codom
 
   function compose(f::HomUWD, g::HomUWD)
diff --git a/src/wiring_diagrams/Undirected.jl b/src/wiring_diagrams/Undirected.jl
index 56e57f868..49364a0c9 100644
--- a/src/wiring_diagrams/Undirected.jl
+++ b/src/wiring_diagrams/Undirected.jl
@@ -9,17 +9,16 @@ export UndirectedWiringDiagram, outer_box, box, junction, nboxes, njunctions,
 using Compat: isnothing
 
 using ...CategoricalAlgebra.CSets, ...Present
-using ...CategoricalAlgebra.FinSets: FinFunction
-using ...CategoricalAlgebra.Limits: pushout
-using ...Theories: FreeSchema, dom, codom, compose, ⋅, id,
-  CatDescType, AttrDescType, SchemaType
+using ...CategoricalAlgebra.ShapeDiagrams: Span
+using ...CategoricalAlgebra.FinSets: FinFunction, pushout
+using ...Theories: FreeCategory, dom, codom, compose, ⋅, id
 import ..DirectedWiringDiagrams: box, boxes, nboxes, add_box!, add_wire!,
   add_wires!, singleton_diagram, ocompose
 
 # Data types
 ############
 
-@present TheoryUWD(FreeSchema) begin
+@present TheoryUWD(FreeCategory) begin
   Box::Ob
   Port::Ob
   OuterPort::Ob
@@ -30,47 +29,43 @@ import ..DirectedWiringDiagrams: box, boxes, nboxes, add_box!, add_wire!,
   outer_junction::Hom(OuterPort,Junction)
 end
 
-const UndirectedWiringDiagram = const AbstractUWD = AbstractACSet{CatDescType(TheoryUWD)}
-const AbstractUntypedUWD = AbstractCSet{CatDescType(TheoryUWD)}
+const UndirectedWiringDiagram = const AbstractUWD = AbstractCSetType(TheoryUWD)
 const UntypedUndirectedWiringDiagram = const UntypedUWD =
-  CSet{CatDescType(TheoryUWD),(:box, :junction, :outer_junction)}
+  CSetType(TheoryUWD, index=[:box, :junction, :outer_junction])
 
 @present TheoryTypedUWD <: TheoryUWD begin
-  Type::Data
+  Type::Ob
 
-  port_type::Attr(Port,Type)
-  outer_port_type::Attr(OuterPort,Type)
-  junction_type::Attr(Junction,Type)
+  port_type::Hom(Port,Type)
+  outer_port_type::Hom(OuterPort,Type)
+  junction_type::Hom(Junction,Type)
 
   compose(junction, junction_type) == port_type
   compose(outer_junction, junction_type) == outer_port_type
 end
 
-const AbstractTypedUWD{T} = AbstractACSet{SchemaType(TheoryTypedUWD)..., Tuple{T}}
-const TypedUndirectedWiringDiagram{T} = ACSet{SchemaType(TheoryTypedUWD)..., Tuple{T}, (:box, :junction, :outer_junction)}
-const TypedUWD{T} = TypedUndirectedWiringDiagram{T}
+const TypedUndirectedWiringDiagram = const TypedUWD =
+  CSetType(TheoryTypedUWD, data=[:Type],
+           index=[:box, :junction, :outer_junction])
 
-type_type(::Type{<:TypedUWD{T}}) where {T} = T
-
-function UndirectedWiringDiagram(::Type{UWD}, nports::Int) where UWD <: AbstractUWD
-  d = UWD()
-  add_parts!(d, :OuterPort, outer_junction=zeros(Int64,nports))
+function UndirectedWiringDiagram(::Type{UWD},
+    nports::Int; data_types...) where UWD <: AbstractUWD
+  d = UWD(; data_types...)
+  add_parts!(d, :OuterPort, nports)
   return d
 end
-
 UndirectedWiringDiagram(nports::Int; kw...) =
   UndirectedWiringDiagram(UntypedUWD, nports; kw...)
 
 function UndirectedWiringDiagram(::Type{UWD},
-    port_types::AbstractVector{T}) where {T, UWD <: AbstractUWD}
-  d = UWD()
+    port_types::AbstractVector{T}; data_types...) where {UWD <: AbstractUWD, T}
+  d = UWD(; port_type=T, outer_port_type=T, junction_type=T, data_types...)
   nports = length(port_types)
-  add_parts!(d, :OuterPort, nports, outer_junction = zeros(Int64,nports), outer_port_type=port_types)
+  add_parts!(d, :OuterPort, nports, outer_port_type=port_types)
   return d
 end
-
-UndirectedWiringDiagram(port_types::AbstractVector{T}; kw...) where {T} =
-  UndirectedWiringDiagram(TypedUWD{T}, port_types; kw...)
+UndirectedWiringDiagram(port_types::AbstractVector; kw...) =
+  UndirectedWiringDiagram(TypedUWD, port_types; kw...)
 
 # Imperative interface
 ######################
@@ -112,25 +107,24 @@ add_box!(d::AbstractUWD; data...) = add_part!(d, :Box; data...)
 
 function add_box!(d::AbstractUWD, nports::Int; data...)
   box = add_box!(d; data...)
-  ports = add_parts!(d, :Port, box=[box for i in 1:nports], junction=zeros(Int64,nports))
+  ports = add_parts!(d, :Port, nports, box=box)
   box
 end
 
-function add_box!(d::AbstractUWD, port_types::AbstractVector{T}; data...) where {T}
+function add_box!(d::AbstractUWD, port_types::AbstractVector; data...)
   box = add_box!(d; data...)
   nports = length(port_types)
-  ports = add_parts!(d, :Port, box=[box for i in 1:nports],
-                     junction = zeros(Int64,nports), port_type=port_types)
+  ports = add_parts!(d, :Port, nports, box=box, port_type=port_types)
   box
 end
 
 add_junction!(d::AbstractUWD; data...) = add_part!(d, :Junction; data...)
-add_junction!(d::AbstractUWD, type::T; data...) where {T} =
+add_junction!(d::AbstractUWD, type; data...) =
   add_part!(d, :Junction; junction_type=type, data...)
 
 add_junctions!(d::AbstractUWD, njunctions::Int; data...) =
   add_parts!(d, :Junction, njunctions; data...)
-add_junctions!(d::AbstractUWD, types::AbstractVector{T}; data...) where {T} =
+add_junctions!(d::AbstractUWD, types::AbstractVector; data...) =
   add_parts!(d, :Junction, length(types); junction_type=types, data...)
 
 function set_junction!(d::AbstractUWD, port, junction; outer::Bool=false)
@@ -201,15 +195,6 @@ function singleton_diagram(::Type{T}, port_types; data...) where T<:AbstractUWD
   return d
 end
 
-function singleton_diagram(::Type{T}, port_types, box_data, junction_data) where T<:AbstractUWD
-  d = empty_diagram(T, port_types)
-  add_box!(d, port_types; box_data...)
-  js = add_junctions!(d, port_types; junction_data...)
-  set_junction!(d, js)
-  set_junction!(d, js, outer=true)
-  return d
-end
-
 """ Undirected wiring diagram defined by a cospan.
 
 The wiring diagram has a single box. The ports of this box, the outer ports, the
@@ -274,7 +259,8 @@ function ocompose(f::AbstractUWD, gs::AbstractVector{<:AbstractUWD})
   gs_outer = FinFunction(
     flat(junction(g, outer=true) .+ n for (g,n) in zip(gs, gs_offset[1:end-1])),
     gs_offset[end])
-  f_inc, g_inc = pushout(f_junction, gs_outer)
+  cospan = pushout(Span(f_junction, gs_outer))
+  f_inc, g_inc = cospan.left, cospan.right
   junctions = add_junctions!(h, length(codom(f_inc)))
   if has_subpart(h, :junction_type)
     set_subpart!(h, [collect(f_inc); collect(g_inc)], :junction_type,
@@ -301,18 +287,12 @@ function ocompose(f::AbstractUWD, i::Int, g::AbstractUWD)
 
   f_i = FinFunction(junction(f, ports(f, i)), njunctions(f))
   g_outer = FinFunction(junction(g, outer=true), njunctions(g))
-  f_inc, g_inc = pushout(f_i, g_outer)
-
+  cospan = pushout(Span(f_i, g_outer))
+  f_inc, g_inc = cospan.left, cospan.right
+  junctions = add_junctions!(h, length(codom(f_inc)))
   if has_subpart(h, :junction_type)
-    T = type_type(typeof(h))
-    pushout_types = Vector{Union{Missing, T}}(missing, length(codom(f_inc)))
-    coproduct_types = [junction_type(f); junction_type(g)]
-    for (i,idx) in enumerate([collect(f_inc); collect(g_inc)])
-      pushout_types[idx] = coproduct_types[i]
-    end
-    junctions = add_junctions!(h, Vector{T}(pushout_types))
-  else
-    junctions = add_junctions!(h, length(codom(f_inc)))
+    set_subpart!(h, [collect(f_inc); collect(g_inc)], :junction_type,
+                 [junction_type(f); junction_type(g)])
   end
 
   f_outer = FinFunction(junction(f, outer=true), njunctions(f))


Test patch:
diff --git a/test/Project.toml b/test/Project.toml
index 97679a19b..78a6e5b81 100644
--- a/test/Project.toml
+++ b/test/Project.toml
@@ -12,5 +12,4 @@ LinearMaps = "7a12625a-238d-50fd-b39a-03d52299707e"
 LinearOperators = "5c8ed15e-5a4c-59e4-a42b-c7e8811fb125"
 Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
 SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
-StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
 Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
diff --git a/test/categorical_algebra/CSets.jl b/test/categorical_algebra/CSets.jl
index 287576ae1..654c2906a 100644
--- a/test/categorical_algebra/CSets.jl
+++ b/test/categorical_algebra/CSets.jl
@@ -2,44 +2,144 @@ module TestCSets
 using Test
 
 using Catlab: @present
-using Catlab.Theories
+using Catlab.Theories: FreeCategory
 using Catlab.CategoricalAlgebra.CSets
 
-@present TheoryDecGraph(FreeSchema) begin
-  V::Ob
-  E::Ob
+# Discrete dynamical systems
+############################
 
-  src::Hom(E,V)
-  tgt::Hom(E,V)
- 
-  X::Data
-
-  vdec::Attr(V,X)
-  edec::Attr(E,X)
+@present TheoryDDS(FreeCategory) begin
+  X::Ob
+  Φ::Hom(X,X)
 end
 
-const AbstractDecGraph{T} = AbstractACSet{SchemaType(TheoryDecGraph)...,Tuple{T}}
-
-const DecGraph{T} = ACSet{SchemaType(TheoryDecGraph)...,Tuple{T},(:src,:tgt,:vdec)}
-
-g = DecGraph{Symbol}()
-
-@test add_parts!(g,:V,vdec=[:a,:b,:d]) == 1:3
-
-@test subpart(g,:vdec) == [:a,:b,:d]
-
-@test subpart(g,2,:vdec) == :b
-
-@test incident(g,:a,:vdec) == [1]
-
-@test add_parts!(g,:E,src=[1,2],tgt=[2,3],edec=[:f,:g]) == 1:2
-
-@test incident(g,3,:tgt) == [2]
-
-set_subpart!(g,2,:tgt,2)
+const AbstractDDS = AbstractCSetType(TheoryDDS)
+const DDS = CSetType(TheoryDDS, index=[:Φ])
+@test AbstractDDS == AbstractCSet{(:X,),(:Φ,),(1,),(1,)}
+@test DDS <: AbstractDDS
+@test DDS <: CSet
+
+dds = DDS()
+@test keys(dds.indices) == (:Φ,)
+@test nparts(dds, :X) == 0
+@test add_part!(dds, :X) == 1
+@test nparts(dds, :X) == 1
+@test incident(dds, 1, :Φ) == []
+
+set_subpart!(dds, 1, :Φ, 1)
+@test subpart(dds, 1, :Φ) == 1
+@test incident(dds, 1, :Φ) == [1]
+
+@test add_part!(dds, :X, Φ=1) == 2
+@test add_part!(dds, :X, Φ=1) == 3
+@test subpart(dds, :Φ) == [1,1,1]
+@test subpart(dds, [2,3], :Φ) == [1,1]
+@test incident(dds, 1, :Φ) == [1,2,3]
+
+@test has_part(dds, :X)
+@test !has_part(dds, :nonpart)
+@test has_part(dds, :X, 3)
+@test !has_part(dds, :X, 4)
+@test has_part(dds, :X, 1:5) == [true, true, true, false, false]
+
+@test has_subpart(dds, :Φ)
+@test !has_subpart(dds, :nonsubpart)
+@test_throws KeyError subpart(dds, 1, :nonsubpart)
+@test_throws KeyError set_subpart!(dds, 1, :nonsubpart, 1)
+
+# Error handling
+@test_throws AssertionError add_part!(dds, :X, Φ=5)
+@test subpart(dds, :Φ) == [1,1,1,0]
+@test incident(dds, 4, :Φ) == []
+
+# Dendrograms
+#############
+
+# Strictly speaking, this data structure is not a single dendrogram but a forest
+# of dendrograms (whose roots are the elements fixed under the `parent` map) and
+# in order to be valid dendrograms, the `height` map must satisfy
+# `compose(parent, height) ≥ height` pointwise.
+
+@present TheoryDendrogram(FreeCategory) begin
+  X::Ob
+  R::Ob
+  parent::Hom(X,X)
+  height::Hom(X,R)
+end
 
-@test incident(g,3,:tgt) == []
+const AbstractDendrogram = AbstractCSetType(TheoryDendrogram, data=[:R])
+const Dendrogram = CSetType(TheoryDendrogram, data=[:R], index=[:parent])
+@test AbstractDendrogram ==
+  AbstractCSet{(:X,),(:parent,),(1,),(1,),(:height,),(1,)}
+
+d = Dendrogram(height=Float64)
+add_parts!(d, :X, 3, height=0)
+add_parts!(d, :X, 2, height=[10,20])
+set_subpart!(d, 1:3, :parent, 4)
+set_subpart!(d, [4,5], :parent, 5)
+
+@test nparts(d, :X) == 5
+@test subpart(d, [1,2,3], :parent) == [4,4,4]
+@test subpart(d, 4, :parent) == 5
+@test subpart(d, :, :parent) == [4,4,4,5,5]
+@test incident(d, 4, :parent) == [1,2,3]
+@test has_subpart(d, :height)
+@test subpart(d, [1,2,3], :height) == [0,0,0]
+@test subpart(d, 4, :height) == 10
+@test subpart(d, :, :height) == [0,0,0,10,20]
+
+d2 = empty(d)
+copy_parts!(d2, d, :X, [4,5])
+@test nparts(d2, :X) == 2
+@test subpart(d2, [1,2], :parent) == [2,2]
+@test subpart(d2, [1,2], :height) == [10,20]
+
+du = disjoint_union(d, d2)
+@test nparts(du, :X) == 7
+@test subpart(du, :parent) == [4,4,4,5,5,7,7]
+@test subpart(du, :height) == [0,0,0,10,20,10,20]
+
+# Labeled sets
+##############
+
+# The simplest example of a C-set with a data attribute, to test data indexing.
+
+@present TheoryLabeledSet(FreeCategory) begin
+  X::Ob
+  Label::Ob
+  label::Hom(X,Label)
+end
 
-@test incident(g,2,:tgt) == [1,2]
+const IndexedLabeledSet = CSetType(
+  TheoryLabeledSet, data=[:Label], index=[:label])
+
+lset = IndexedLabeledSet(label=Symbol)
+@test keys(lset.data_indices) == (:label,)
+add_parts!(lset, :X, 2, label=[:foo, :bar])
+@test subpart(lset, :, :label) == [:foo, :bar]
+@test incident(lset, :foo, :label) == [1]
+@test isempty(incident(lset, :nonkey, :label))
+
+add_part!(lset, :X, label=:foo)
+@test incident(lset, :foo, :label) == [1,3]
+set_subpart!(lset, 1, :label, :baz)
+@test subpart(lset, 1, :label) == :baz
+@test incident(lset, [:foo,:baz], :label) == [[3],[1]]
+
+const UniqueIndexedLabeledSet = CSetType(
+  TheoryLabeledSet, data=[:Label], unique_index=[:label])
+
+lset = UniqueIndexedLabeledSet(label=Symbol)
+@test keys(lset.data_indices) == (:label,)
+add_parts!(lset, :X, 2, label=[:foo, :bar])
+@test subpart(lset, :, :label) == [:foo, :bar]
+@test incident(lset, :foo, :label) == 1
+@test incident(lset, [:foo,:bar], :label) == [1,2]
+@test incident(lset, :nonkey, :label) == nothing
+
+set_subpart!(lset, 1, :label, :baz)
+@test subpart(lset, 1, :label) == :baz
+@test incident(lset, :baz, :label) == 1
+@test_throws ErrorException set_subpart!(lset, 1, :label, :bar)
 
 end
diff --git a/test/categorical_algebra/CategoricalAlgebra.jl b/test/categorical_algebra/CategoricalAlgebra.jl
index 156cbbf9e..f5d89ea97 100644
--- a/test/categorical_algebra/CategoricalAlgebra.jl
+++ b/test/categorical_algebra/CategoricalAlgebra.jl
@@ -2,12 +2,8 @@ module TestCategoricalAlgebra
 
 using Test
 
-@testset "FreeDiagrams" begin
-  include("FreeDiagrams.jl")
-end
-
-@testset "Limits" begin
-  include("Limits.jl")
+@testset "ShapeDiagrams" begin
+  include("ShapeDiagrams.jl")
 end
 
 @testset "Matrices" begin
@@ -31,8 +27,4 @@ end
   include("Graphs.jl")
 end
 
-@testset "PredicatedSets.jl" begin
-  include("PredicatedSets.jl")
-end
-
 end
diff --git a/test/categorical_algebra/FinSets.jl b/test/categorical_algebra/FinSets.jl
index 1e708cc8d..fa5d340b1 100644
--- a/test/categorical_algebra/FinSets.jl
+++ b/test/categorical_algebra/FinSets.jl
@@ -1,8 +1,8 @@
 module TestFinSets
 using Test
 
-using Catlab.Theories, Catlab.CategoricalAlgebra
-using Catlab.CategoricalAlgebra.FinSets
+using Catlab.Theories
+using Catlab.CategoricalAlgebra.ShapeDiagrams, Catlab.CategoricalAlgebra.FinSets
 
 # Category of finite ordinals
 #############################
@@ -29,127 +29,128 @@ h = FinFunction([3,1,2], 3)
 ########
 
 # Terminal object.
-@test ob(terminal(FinSet{Int})) == FinSet(1)
+@test terminal(FinSet{Int}) == FinSet(1)
 
-# Binary product.
-lim = product(FinSet(2), FinSet(3))
-@test ob(lim) == FinSet(6)
-@test force(proj1(lim)) == FinFunction([1,2,1,2,1,2])
-@test force(proj2(lim)) == FinFunction([1,1,2,2,3,3])
+# Binary Product.
+span = product(FinSet(2), FinSet(3))
+@test apex(span) == FinSet(6)
+@test force(left(span)) == FinFunction([1,2,1,2,1,2])
+@test force(right(span)) == FinFunction([1,1,2,2,3,3])
 
-# N-ary product.
-lim = product([FinSet(2), FinSet(3)])
-@test ob(lim) == FinSet(6)
-@test force.(legs(lim)) ==
-  [FinFunction([1,2,1,2,1,2]), FinFunction([1,1,2,2,3,3])]
-@test ob(product(FinSet{Int}[])) == FinSet(1)
+# N-ary Product.
+cone = product([FinSet(2), FinSet(3)])
+@test apex(cone) == FinSet(6)
+@test force(leg(cone,1)) == FinFunction([1,2,1,2,1,2])
+@test force(leg(cone,2)) == FinFunction([1,1,2,2,3,3])
+@test apex(product(FinSet{Int}[])) == FinSet(1)
 
 # Equalizer.
 f, g = FinFunction([1,2,3]), FinFunction([3,2,1])
-@test incl(equalizer(f,g)) == FinFunction([2], 3)
-@test incl(equalizer([f,g])) == FinFunction([2], 3)
+@test equalizer(f,g) == FinFunction([2], 3)
+@test equalizer([f,g]) == FinFunction([2], 3)
 
 # Equalizer in case of identical functions.
 f = FinFunction([4,2,3,1], 5)
-@test incl(equalizer(f,f)) == force(id(FinSet(4)))
-@test incl(equalizer([f,f])) == force(id(FinSet(4)))
+@test equalizer(f,f) == force(id(FinSet(4)))
+@test equalizer([f,f]) == force(id(FinSet(4)))
 
 # Equalizer matching nothing.
 f, g = id(FinSet(5)), FinFunction([2,3,4,5,1], 5)
-@test incl(equalizer(f,g)) == FinFunction(Int[], 5)
-@test incl(equalizer([f,g])) == FinFunction(Int[], 5)
+@test equalizer(f,g) == FinFunction(Int[], 5)
+@test equalizer([f,g]) == FinFunction(Int[], 5)
 
 # Pullback.
-lim = pullback(FinFunction([1,1,3,2],4), FinFunction([1,1,4,2],4))
-@test ob(lim) == FinSet(5)
-@test force(proj1(lim)) == FinFunction([1,2,1,2,4], 4)
-@test force(proj2(lim)) == FinFunction([1,1,2,2,4], 4)
+span = pullback(Cospan(FinFunction([1,1,3,2],4), FinFunction([1,1,4,2],4)))
+@test apex(span) == FinSet(5)
+@test force(left(span)) == FinFunction([1,2,1,2,4], 4)
+@test force(right(span)) == FinFunction([1,1,2,2,4], 4)
 
 # Pullback from a singleton set: the degenerate case of a product.
-lim = pullback(FinFunction([1,1]), FinFunction([1,1,1]))
-@test ob(lim) == FinSet(6)
-@test force(proj1(lim)) == FinFunction([1,2,1,2,1,2])
-@test force(proj2(lim)) == FinFunction([1,1,2,2,3,3])
+span = pullback(Cospan(FinFunction([1,1]), FinFunction([1,1,1])))
+@test apex(span) == FinSet(6)
+@test force(left(span)) == FinFunction([1,2,1,2,1,2])
+@test force(right(span)) == FinFunction([1,1,2,2,3,3])
 
 # Pullback using generic limit interface
-f, g = FinFunction([1,1,3,2],4), FinFunction([1,1,4,2],4)
-lim = limit(FreeDiagram([FinSet(4),FinSet(4),FinSet(4)], [(1,3,f),(2,3,g)]))
-@test ob(lim) == FinSet(5)
-@test force.(legs(lim)[1:2]) ==
-  [FinFunction([1,2,1,2,4],4), FinFunction([1,1,2,2,4],4)]
+f,g = FinFunction([1,1,3,2],4), FinFunction([1,1,4,2],4)
+cone = limit(Diagram([FinSet(4),FinSet(4),FinSet(4)], [(1,3,f),(2,3,g)]))
+@test apex(cone) == FinSet(5)
+@test force(leg(cone,1)) == FinFunction([1,2,1,2,4],4)
+@test force(leg(cone,2)) == FinFunction([1,1,2,2,4],4)
 
 # Colimits
 ##########
 
 # Initial object.
-@test ob(initial(FinSet{Int})) == FinSet(0)
+@test initial(FinSet{Int}) == FinSet(0)
 
-# Binary coproduct.
-colim = coproduct(FinSet(2), FinSet(3))
-@test ob(colim) == FinSet(5)
-@test coproj1(colim) == FinFunction([1,2], 5)
-@test coproj2(colim) == FinFunction([3,4,5], 5)
+# Binary Coproduct.
+cospan = coproduct(FinSet(2), FinSet(3))
+@test base(cospan) == FinSet(5)
+@test left(cospan) == FinFunction([1,2], 5)
+@test right(cospan) == FinFunction([3,4,5], 5)
 
-# N-ary coproduct.
-colim = coproduct([FinSet(2), FinSet(3)])
-@test ob(colim) == FinSet(5)
-@test legs(colim) == [FinFunction([1,2], 5), FinFunction([3,4,5], 5)]
-@test ob(coproduct(FinSet{Int}[])) == FinSet(0)
+# N-ary Coproduct.
+cocone = coproduct([FinSet(2), FinSet(3)])
+@test base(cocone) == FinSet(5)
+@test leg(cocone,1) == FinFunction([1,2], 5)
+@test leg(cocone,2) == FinFunction([3,4,5], 5)
+@test base(coproduct(FinSet{Int}[])) == FinSet(0)
 
 # Coequalizer from a singleton set.
 f, g = FinFunction([1], 3), FinFunction([3], 3)
-@test proj(coequalizer(f,g)) == FinFunction([1,2,1], 2)
-@test proj(coequalizer([f,g])) == FinFunction([1,2,1], 2)
+@test coequalizer(f,g) == FinFunction([1,2,1], 2)
+@test coequalizer([f,g]) == FinFunction([1,2,1], 2)
 
 # Coequalizer in case of identical functions.
 f = FinFunction([4,2,3,1], 5)
-@test proj(coequalizer(f,f)) == force(id(FinSet(5)))
-@test proj(coequalizer([f,f])) == force(id(FinSet(5)))
+@test coequalizer(f,f) == force(id(FinSet(5)))
+@test coequalizer([f,f]) == force(id(FinSet(5)))
 
 # Coequalizer identifying everything.
 f, g = id(FinSet(5)), FinFunction([2,3,4,5,1], 5)
-@test proj(coequalizer(f,g)) == FinFunction(repeat([1],5))
-@test proj(coequalizer([f,g])) == FinFunction(repeat([1],5))
+@test coequalizer(f,g) == FinFunction(repeat([1],5))
+@test coequalizer([f,g]) == FinFunction(repeat([1],5))
 
 # Pushout from the empty set: the degenerate case of the coproduct.
 f, g = FinFunction(Int[], 2), FinFunction(Int[], 3)
-colim = pushout(f,g)
-@test ob(colim) == FinSet(5)
-@test coproj1(colim) == FinFunction([1,2], 5)
-@test coproj2(colim) == FinFunction([3,4,5], 5)
+cospan = pushout(Span(f,g))
+@test base(cospan) == FinSet(5)
+@test left(cospan) == FinFunction([1,2], 5)
+@test right(cospan) == FinFunction([3,4,5], 5)
 
 # Pushout from a singleton set.
 f, g = FinFunction([1], 2), FinFunction([2], 3)
-colim = pushout(f,g)
-@test ob(colim) == FinSet(4)
-h, k = colim
+cospan = pushout(Span(f,g))
+@test base(cospan) == FinSet(4)
+h, k = left(cospan), right(cospan)
 @test compose(f,h) == compose(g,k)
 @test h == FinFunction([1,2], 4)
 @test k == FinFunction([3,1,4], 4)
 
 # Same thing with generic colimit interface
-diag = FreeDiagram([FinSet(1),FinSet(2),FinSet(3)],[(1,2,f), (1,3,g)])
-colim = colimit(diag)
-@test ob(colim) == FinSet(4)
-_, h, k = colim
+diag = Diagram([FinSet(1),FinSet(2),FinSet(3)],[(1,2,f), (1,3,g)])
+cocone = colimit(diag)
+@test base(cocone) == FinSet(4)
+h, k = leg(cocone,2), leg(cocone,3)
 @test compose(f,h) == compose(g,k)
 @test h == FinFunction([1,2], 4)
 @test k == FinFunction([3,1,4], 4)
 
 # Pushout from a two-element set, with non-injective legs.
 f, g = FinFunction([1,1], 2), FinFunction([1,2], 2)
-colim = pushout(f,g)
-@test ob(colim) == FinSet(2)
-h, k = colim
+cospan = pushout(Span(f,g))
+@test base(cospan) == FinSet(2)
+h, k = left(cospan), right(cospan)
 @test compose(f,h) == compose(g,k)
 @test h == FinFunction([1,2], 2)
 @test k == FinFunction([1,1], 2)
 
 # Same thing with generic colimit interface
-diag = FreeDiagram([FinSet(2),FinSet(2),FinSet(2)],[(1,2,f),(1,3,g)])
-colim = colimit(diag)
-@test ob(colim) == FinSet(2)
-_, h, k = colim
+diag = Diagram([FinSet(2),FinSet(2),FinSet(2)],[(1,2,f),(1,3,g)])
+cocone = colimit(diag)
+@test base(cocone) == FinSet(2)
+h,k = leg(cocone,2), leg(cocone,3)
 @test compose(f,h) == compose(g,k)
 @test h == FinFunction([1,2], 2)
 @test k == FinFunction([1,1], 2)
diff --git a/test/categorical_algebra/FreeDiagrams.jl b/test/categorical_algebra/FreeDiagrams.jl
deleted file mode 100644
index 77598c150..000000000
--- a/test/categorical_algebra/FreeDiagrams.jl
+++ /dev/null
@@ -1,85 +0,0 @@
-module TestFreeDiagrams
-using Test
-
-using Catlab.Theories, Catlab.CategoricalAlgebra
-
-A, B, C = Ob(FreeCategory, :A, :B, :C)
-
-# General diagrams
-##################
-
-f, g, h = Hom(:f, A, C), Hom(:g, B, C), Hom(:h, A, B)
-diagram = FreeDiagram([A,B,C], [(1,3,f),(2,3,g),(1,2,h)])
-@test ob(diagram) == [A,B,C]
-@test hom(diagram) == [f,g,h]
-@test src(diagram) == [1,2,1]
-@test tgt(diagram) == [3,3,2]
-@test_throws Exception FreeDiagram([A,B,C], [(1,2,f),(2,3,g),(1,2,h)])
-
-# Diagrams of fixed shape
-#########################
-
-# Spans.
-f, g = Hom(:f, C, A), Hom(:g, C, B)
-span = Span(f,g)
-@test apex(span) == C
-@test legs(span) == [f,g]
-@test left(span) == f
-@test right(span) == g
-
-f = Hom(:f, A, B)
-@test_throws Exception Span(f,g)
-
-# Multispans.
-f, g, h = Hom(:f, C, A), Hom(:g, C, B), Hom(:h, C, A)
-span = Multispan([f,g,h])
-@test apex(span) == C
-@test legs(span) == [f,g,h]
-
-diagram = FreeDiagram(span)
-@test ob(diagram) == [C,A,B,A]
-@test hom(diagram) == [f,g,h]
-@test src(diagram) == [1,1,1]
-@test tgt(diagram) == [2,3,4]
-
-# Cospans.
-f, g = Hom(:f, A, C), Hom(:g, B, C)
-cospan = Cospan(f,g)
-@test base(cospan) == C
-@test legs(cospan) == [f,g]
-@test left(cospan) == f
-@test right(cospan) == g
-
-# Multicospans.
-f, g, h = Hom(:f, A, C), Hom(:g, B, C), Hom(:h, A, C)
-cospan = Multicospan([f,g,h])
-@test base(cospan) == C
-@test legs(cospan) == [f,g,h]
-
-diagram = FreeDiagram(cospan)
-@test ob(diagram) == [A,B,A,C]
-@test hom(diagram) == [f,g,h]
-@test src(diagram) == [1,2,3]
-@test tgt(diagram) == [4,4,4]
-
-# Parallel pairs.
-f, g = Hom(:f, A, B), Hom(:g, A, B)
-pair = ParallelPair(f,g)
-@test dom(pair) == A
-@test codom(pair) == B
-@test first(pair) == f
-@test last(pair) == g
-
-# Parallel morphisms.
-f, g, h = Hom(:f, A, B), Hom(:g, A, B), Hom(:h, A, B)
-para = ParallelMorphisms([f,g,h])
-@test dom(para) == A
-@test codom(para) == B
-
-diagram = FreeDiagram(para)
-@test ob(diagram) == [A,B]
-@test hom(diagram) == [f,g,h]
-@test src(diagram) == [1,1,1]
-@test tgt(diagram) == [2,2,2]
-
-end
diff --git a/test/categorical_algebra/Graphs.jl b/test/categorical_algebra/Graphs.jl
index 242a61420..1bc5a9eb9 100644
--- a/test/categorical_algebra/Graphs.jl
+++ b/test/categorical_algebra/Graphs.jl
@@ -32,7 +32,8 @@ add_edge!(g, 1, 2)
 @test inneighbors(g, 1) == []
 @test LightGraphs.DiGraph(g) == LightGraphs.path_digraph(3)
 
-g = Graph(4)
+g = Graph()
+add_vertices!(g, 4)
 add_edges!(g, [1,2,3], [2,3,4])
 @test LightGraphs.DiGraph(g) == LightGraphs.path_digraph(4)
 
@@ -55,7 +56,8 @@ add_edge!(g, 2, 3)
 @test neighbors(g, 3) == [2]
 @test LightGraphs.Graph(g) == LightGraphs.path_graph(3)
 
-g = SymmetricGraph(4)
+g = SymmetricGraph()
+add_vertices!(g, 4)
 add_edges!(g, [1,2,3], [2,3,4])
 lg = LightGraphs.DiGraph(4)
 map((src, tgt) -> add_edge!(lg, src, tgt), [1,2,3,2,3,4], [2,3,4,1,2,3])
diff --git a/test/categorical_algebra/Limits.jl b/test/categorical_algebra/Limits.jl
deleted file mode 100644
index 66acf7b06..000000000
--- a/test/categorical_algebra/Limits.jl
+++ /dev/null
@@ -1,42 +0,0 @@
-""" Tests of limit and colimit data structures.
-
-More extensive tests are provided by (co)limits for specific categories such as
-FinSet.
-"""
-module TestLimits
-using Test
-
-using StaticArrays
-using Catlab.Theories, Catlab.CategoricalAlgebra
-
-A, B, C = Ob(FreeCategory, :A, :B, :C)
-
-# Limits
-########
-
-# Products.
-f, g = Hom(:f, C, A), Hom(:g, C, B)
-lim = Limit(SVector(A,B), Span(f,g))
-@test lim isa BinaryProduct
-@test ob(lim) == C
-@test apex(lim) == C
-@test legs(lim) == [f,g]
-
-lim = Limit([A,B], Span(f,g))
-@test lim isa Product
-
-# Colimits
-##########
-
-# Coproducts.
-f, g = Hom(:f, A, C), Hom(:g, B, C)
-colim = Colimit(SVector(A,B), Cospan(f,g))
-@test colim isa BinaryCoproduct
-@test ob(colim) == C
-@test base(colim) == C
-@test legs(colim) == [f,g]
-
-colim = Colimit([A,B], Cospan(f,g))
-@test colim isa Coproduct
-
-end
diff --git a/test/categorical_algebra/PredicatedSets.jl b/test/categorical_algebra/PredicatedSets.jl
deleted file mode 100644
index 3b654b4f4..000000000
--- a/test/categorical_algebra/PredicatedSets.jl
+++ /dev/null
@@ -1,22 +0,0 @@
-module TestPredicatedSets
-using Test
-using Catlab.CategoricalAlgebra.PredicatedSets
-using Catlab.Theories
-
-odds = PredicatedSet(Int, x -> x % 2 == 1)
-evens = PredicatedSet(Int, x -> x % 2 == 0)
-
-plus_one_to_odd = PredicatedHom(odds,evens,x -> x+1)
-plus_one_to_even = PredicatedHom(evens,odds,x -> x+1)
-
-@test plus_one_to_odd(1) == 2
-@test_throws AssertionError plus_one_to_odd(2) == 3
-@test plus_one_to_even(2) == 3
-@test_throws AssertionError plus_one_to_even(3) == 4
-
-@test compose(plus_one_to_odd, plus_one_to_even)(3) == 5
-
-plus_two = PredicatedHom(odds,evens,x -> x+2)
-
-@test_throws AssertionError plus_two(3)
-end
diff --git a/test/categorical_algebra/ShapeDiagrams.jl b/test/categorical_algebra/ShapeDiagrams.jl
new file mode 100644
index 000000000..72a170f64
--- /dev/null
+++ b/test/categorical_algebra/ShapeDiagrams.jl
@@ -0,0 +1,48 @@
+module TestShapeDiagrams
+using Test
+
+using Catlab.Theories, Catlab.CategoricalAlgebra.ShapeDiagrams
+
+A, B, C = Ob(FreeCategory, :A, :B, :C)
+
+# Spans.
+f, g = Hom(:f, C, A), Hom(:g, C, B)
+span = Span(f,g)
+@test apex(span) == C
+@test left(span) == f
+@test right(span) == g
+f = Hom(:f, A, B)
+@test_throws Exception Span(f,g)
+
+# Cones.
+f, g = Hom(:f, C, A), Hom(:g, C, B)
+cone = Cone(C,[f,g])
+@test apex(cone) == C
+@test leg(cone,1) == f
+@test leg(cone,2) == g
+f = Hom(:f, A, B)
+@test_throws Exception Cone(C,[f,g])
+
+# Cospans.
+f, g = Hom(:f, A, C), Hom(:g, B, C)
+cospan = Cospan(f,g)
+@test base(cospan) == C
+@test left(cospan) == f
+@test right(cospan) == g
+
+# Cocones.
+f, g = Hom(:f, A, C), Hom(:g, B, C)
+cocone = Cocone(C,[f,g])
+@test base(cocone) == C
+@test leg(cocone,1) == f
+@test leg(cocone,2) == g
+f = Hom(:f, A, B)
+@test_throws Exception Cocone(C,[f,g])
+
+# Diagrams.
+
+f, g, h = Hom(:f, A, C), Hom(:g, B, C), Hom(:h, A, B)
+diag = Diagram([A,B,C],[(1,3,f),(2,3,g),(1,2,h)])
+@test_throws Exception Diagram([A,B,C],[(1,2,f),(2,3,g),(1,2,h)])
+
+end
diff --git a/test/core/GAT.jl b/test/core/GAT.jl
index 8b0a24e35..4c48befd0 100644
--- a/test/core/GAT.jl
+++ b/test/core/GAT.jl
@@ -98,7 +98,7 @@ target = Dict(:→ => :Mor)
 
 # This try-catch block is a necessary work around because of a current bug
 # where test_throws doesn't catch errors thrown from inside of a macro
-@test_throws ParseError try @eval @signature Category{Ob,Hom} begin
+@test_throws ParseError try @eval @signature Category(Ob,Hom) begin
   Ob::TYPE
   Hom(dom, codom)::TYPE ⊣ (dom::Ob, codom::Ob)
   @op (→) := Hom
@@ -118,7 +118,7 @@ end
 
 """ Theory of categories
 """
-@theory Category{Ob,Hom} begin
+@theory Category(Ob,Hom) begin
   Ob::TYPE
   Hom(dom, codom)::TYPE ⊣ (dom::Ob, codom::Ob)
   @op (→) := Hom
@@ -167,7 +167,7 @@ category_theory = GAT.Theory(types, terms, axioms, aliases)
 
 """ Equivalent shorthand definition of Category theory
 """
-@theory CategoryAbbrev{Ob,Hom} begin
+@theory CategoryAbbrev(Ob,Hom) begin
   @op begin
     (→) := Hom
     (⋅) := compose
@@ -203,12 +203,12 @@ theory = GAT.theory(Category)
 @test GAT.interface(theory) == [accessors; constructors; alias_functions]
 
 # Theory extension
-@signature Semigroup{S} begin
+@signature Semigroup(S) begin
   S::TYPE
   times(x::S,y::S)::S
 end
 
-@signature MonoidExt{M} <: Semigroup{M} begin
+@signature Semigroup(M) => MonoidExt(M) begin
   munit()::M
 end
 
@@ -255,13 +255,13 @@ context = GAT.Context((:X => :Ob, :Y => :Ob, :Z => :Ob,
 
 """ Vectors as an instance of the theory of semigroups
 """
-@instance Semigroup{Vector} begin
+@instance Semigroup(Vector) begin
   times(x::Vector, y::Vector) = [x; y]
 end
 
 @test times([1,2],[3,4]) == [1,2,3,4]
 
-@signature Monoid{M} begin
+@signature Monoid(M) begin
   M::TYPE
   munit()::M
   times(x::M,y::M)::M
@@ -269,12 +269,12 @@ end
 
 # Incomplete instance of Monoid
 # XXX: Cannot use `@test_warn` since generated code won't be at toplevel.
-#@test_warn "not implemented" @instance Monoid{String} begin
+#@test_warn "not implemented" @instance Monoid(String) begin
 #  times(x::AbsStringtractString, y::String) = string(x,y)
 #end
 
 # Complete instance of Monoid
-@instance Monoid{String} begin
+@instance Monoid(String) begin
   munit(::Type{String}) = ""
   times(x::String, y::String) = string(x,y)
 end
diff --git a/test/core/Syntax.jl b/test/core/Syntax.jl
index 3eb5e0c7a..f71190716 100644
--- a/test/core/Syntax.jl
+++ b/test/core/Syntax.jl
@@ -13,7 +13,7 @@ using Catlab
 
 """ Theory of monoids.
 """
-@signature Monoid{Elem} begin
+@signature Monoid(Elem) begin
   Elem::TYPE
   munit()::Elem
   mtimes(x::Elem,y::Elem)::Elem
@@ -67,13 +67,13 @@ e = munit(FreeMonoidAssocUnit.Elem)
 @test mtimes(e,x) == x && mtimes(x,e) == x
 
 abstract type MonoidExpr{T} <: GATExpr{T} end
-@syntax FreeMonoidTyped{MonoidExpr} Monoid
+@syntax FreeMonoidTyped(MonoidExpr) Monoid
 
 x = Elem(FreeMonoidTyped.Elem, :x)
 @test FreeMonoidTyped.Elem <: MonoidExpr
 @test isa(x, FreeMonoidTyped.Elem) && isa(x, MonoidExpr)
 
-@signature MonoidNumeric{Elem} <: Monoid{Elem} begin
+@signature Monoid(Elem) => MonoidNumeric(Elem) begin
   elem_int(x::Int)::Elem
 end
 @syntax FreeMonoidNumeric MonoidNumeric
@@ -84,7 +84,7 @@ x = elem_int(FreeMonoidNumeric.Elem, 1)
 
 """ A monoid with two distinguished elements.
 """
-@signature MonoidTwo{Elem} <: Monoid{Elem} begin
+@signature Monoid(Elem) => MonoidTwo(Elem) begin
   one()::Elem
   two()::Elem
 end
@@ -102,7 +102,7 @@ x, y = one(FreeMonoidTwo.Elem), two(FreeMonoidTwo.Elem)
 # Category
 ##########
 
-@signature Category{Ob,Hom} begin
+@signature Category(Ob,Hom) begin
   Ob::TYPE
   Hom(dom::Ob, codom::Ob)::TYPE
 
@@ -147,7 +147,7 @@ f, g = Hom(:f, X, Y), Hom(:g, Y, X)
 # Functor
 #########
 
-@instance Monoid{String} begin
+@instance Monoid(String) begin
   munit(::Type{String}) = ""
   mtimes(x::String, y::String) = string(x,y)
 end
diff --git a/test/graphics/Graphviz.jl b/test/graphics/Graphviz.jl
index 8420e7b82..27d0a7ae1 100644
--- a/test/graphics/Graphviz.jl
+++ b/test/graphics/Graphviz.jl
@@ -3,9 +3,8 @@ module TestGraphviz
 using Test
 import JSON
 
-using Catlab.CategoricalAlgebra: Graphs
-using Catlab.CategoricalAlgebra.Graphs: nv, ne, src, tgt,
-  add_vertex!, add_vertices!, add_edge!, add_edges!, get_vprop, get_eprop
+using Catlab.CategoricalAlgebra.Graphs: PropertyGraph, SymmetricPropertyGraph,
+  nv, ne, src, tgt, add_vertex!, add_vertices!, add_edge!, get_vprop, get_eprop
 using Catlab.Graphics.Graphviz
 
 # Graphviz to property graph
@@ -16,14 +15,14 @@ data_path(name::String) = joinpath(@__DIR__, "data", name)
 # Undirected simple graph.
 doc = open(JSON.parse, data_path("graphviz_graph.json"), "r")
 parsed = parse_graphviz(doc)
-@test parsed isa Graphs.SymmetricPropertyGraph
+@test parsed isa SymmetricPropertyGraph
 @test nv(parsed) == 10
 @test ne(parsed) ÷ 2 == 13
 
 # Directed simple graph
 doc = open(JSON.parse, data_path("graphviz_digraph.json"), "r")
 parsed = parse_graphviz(doc)
-@test parsed isa Graphs.PropertyGraph
+@test parsed isa PropertyGraph
 @test nv(parsed) == 5
 @test src(parsed) == [1,1,1,2,3,4]
 @test tgt(parsed) == [2,3,4,5,5,5]
@@ -35,65 +34,41 @@ parsed = parse_graphviz(doc)
 # Property graph to Graphviz
 ############################
 
-# Symmetric property graph.
-g = Graphs.SymmetricPropertyGraph{String}()
+# Undirected simple graph
+g = SymmetricPropertyGraph{String}()
 add_vertex!(g, label="v")
 add_vertices!(g, 2)
 add_edge!(g, 1, 2, xlabel="e")
-gv = to_graphviz(g)
+gv = to_graphviz(g)::Graph
 @test !gv.directed
-nodes = filter(s -> s isa Node, gv.stmts)
-edges = filter(s -> s isa Edge, gv.stmts)
+nodes = filter(s -> isa(s,Node), gv.stmts)
+edges = filter(s -> isa(s,Edge), gv.stmts)
 @test length(nodes) == 3
 @test length(edges) == 1
 @test nodes[1].attrs[:label] == "v"
 @test edges[1].attrs[:xlabel] == "e"
 
-# Property graph.
-g = Graphs.PropertyGraph{String}()
+# Directed simple graph
+g = PropertyGraph{String}()
 add_vertices!(g, 3); add_edge!(g, 1, 2); add_edge!(g, 2, 3)
-gv = to_graphviz(g)
+gv = to_graphviz(g)::Graph
 @test gv.directed
-@test length(filter(s -> s isa Node, gv.stmts)) == 3
-@test length(filter(s -> s isa Edge, gv.stmts)) == 2
+@test length(filter(s -> isa(s,Node), gv.stmts)) == 3
+@test length(filter(s -> isa(s,Edge), gv.stmts)) == 2
 
-# Property graph with multiple edges.
-g = Graphs.PropertyGraph{String}()
+# Directed multigraph
+g = PropertyGraph{String}()
 add_vertices!(g, 2)
 add_edge!(g, 1, 2, label="e1")
 add_edge!(g, 1, 2, label="e2")
-gv = to_graphviz(g)
+gv = to_graphviz(g)::Graph
 @test gv.directed
-nodes = filter(s -> s isa Node, gv.stmts)
-edges = filter(s -> s isa Edge, gv.stmts)
+nodes = filter(s -> isa(s,Node), gv.stmts)
+edges = filter(s -> isa(s,Edge), gv.stmts)
 @test length(nodes) == 2
 @test length(edges) == 2
-@test [ edge.attrs[:label] for edge in edges ] == ["e1", "e2"]
-
-# Graph to Graphviz
-###################
-
-# Graph.
-g = Graphs.Graph(3)
-add_edges!(g, [1,2], [2,3])
-gv = to_graphviz(g)
-@test gv.directed
-nodes = filter(s -> s isa Node, gv.stmts)
-@test [ node.attrs[:label] for node in nodes ] == ["", "", ""]
-
-gv = to_graphviz(g, node_labels=true, edge_labels=true)
-nodes = filter(s -> s isa Node, gv.stmts)
-edges = filter(s -> s isa Edge, gv.stmts)
-@test [ node.attrs[:label] for node in nodes ] == ["1", "2", "3"]
-@test [ edge.attrs[:label] for edge in edges ] == ["1", "2"]
-
-# Symmetric graph.
-g = Graphs.SymmetricGraph(3)
-add_edges!(g, [1,2], [2,3])
-gv = to_graphviz(g, edge_labels=true)
-@test !gv.directed
-edges = filter(s -> s isa Edge, gv.stmts)
-@test [ edge.attrs[:label] for edge in edges ] == ["(1,3)", "(2,4)"]
+@test edges[1].attrs[:label] == "e1"
+@test edges[2].attrs[:label] == "e2"
 
 # Pretty-print
 ##############
diff --git a/test/graphics/GraphvizWiringDiagrams.jl b/test/graphics/GraphvizWiringDiagrams.jl
index 0b820db21..d951e24d6 100644
--- a/test/graphics/GraphvizWiringDiagrams.jl
+++ b/test/graphics/GraphvizWiringDiagrams.jl
@@ -91,15 +91,10 @@ graph = to_graphviz(d, implicit_junctions=true)
 @test length(stmts(graph, Graphviz.Edge)) == 3
 
 # Box, port, and junction labels.
-graph = to_graphviz(d, box_labels=:name, junction_labels=:variable,
-                    port_labels=false)
-@test filter(!isempty, stmts(graph, Graphviz.Node, :label)) == ["R", "S"]
+notempty = x -> !isempty(x)
+graph = to_graphviz(d, box_labels=:name, junction_labels=:variable)
+@test filter(notempty, stmts(graph, Graphviz.Node, :label)) == ["R", "S"]
 @test stmts(graph, Graphviz.Node, :xlabel) == ["x", "y", "z"]
-@test stmts(graph, Graphviz.Edge, :taillabel) == []
-
-graph = to_graphviz(d, box_labels=true, junction_labels=true, port_labels=true)
-@test filter(!isempty, stmts(graph, Graphviz.Node, :label)) == ["1", "2"]
-@test stmts(graph, Graphviz.Node, :xlabel) == ["1", "2", "3"]
 @test stmts(graph, Graphviz.Edge, :taillabel) == ["1","1","2","1","2","2"]
 
 # Directed layout
diff --git a/test/programs/RelationalPrograms.jl b/test/programs/RelationalPrograms.jl
index 9e0d23eee..5d0fb07c5 100644
--- a/test/programs/RelationalPrograms.jl
+++ b/test/programs/RelationalPrograms.jl
@@ -65,7 +65,8 @@ parsed = @tensor_network D[i,j,k] := A[i,ℓ] * B[j,ℓ] * C[k,ℓ]
 
 # Degenerate case: single term.
 parsed = @tensor_network B[i,j,k] = A[i,j,k]
-d = singleton_diagram(UntypedRelationDiagram{Symbol}, 3, (name=:A,), (variable=[:i,:j,:k],))
+d = singleton_diagram(RelationDiagram, 3, name=:A)
+set_subpart!(d, :variable, [:i,:j,:k])
 @test parsed == d
 
 # Degenerate case: no terms.
diff --git a/test/theories/Schema.jl b/test/theories/Schema.jl
deleted file mode 100644
index 2ddfefed8..000000000
--- a/test/theories/Schema.jl
+++ /dev/null
@@ -1,41 +0,0 @@
-using Test
-using Catlab.Present
-
-# Schema
-########
-
-A, B = Ob(FreeSchema, :A), Ob(FreeSchema, :B)
-f, g = Hom(:f, A, B), Hom(:g, B, A)
-
-C = Data(FreeSchema.Data, :C)
-x, y = Attr(:x, A, C), Attr(:y, B, C)
-
-@test dom(x) == A
-@test codom(x) == C
-@test dom(compose(f,y)) == A
-@test dom(compose(g,x)) == B
-
-# CatDesc and AttrDesc
-######################
-
-@present TheoryDecGraph(FreeSchema) begin
-  V::Ob
-  E::Ob
-
-  src::Hom(E,V)
-  tgt::Hom(E,V)
-
-  X::Data
-
-  vdec::Attr(V,X)
-  edec::Attr(E,X)
-end
-
-CD = CatDescType(TheoryDecGraph)
-AD = AttrDescType(TheoryDecGraph)
-
-@test dom(CD,:src) == :E
-@test codom(CD,:tgt) == :V
-
-@test dom(AD,:vdec) == :V
-@test codom(AD,:edec) == :X
diff --git a/test/theories/Theories.jl b/test/theories/Theories.jl
index d6b4578a2..653a68700 100644
--- a/test/theories/Theories.jl
+++ b/test/theories/Theories.jl
@@ -21,8 +21,4 @@ end
   include("Preorders.jl")
 end
 
-@testset "Schemas" begin
-  include("Schema.jl")
-end
-
 end