Skip to main content

Type inference for transformation algebras.

Project description

transformation-algebra

A transformation algebra describes tools (in some domain) as abstract data transformations. An expression of such an algebra has an interpretation, but there is not necessarily any concrete data structure or implementation assigned to it. It merely describes its input and output in terms of some conceptual properties that are deemed relevant.

To define such an algebra, we implemented type inference in Python. The system accommodates both subtype and parametric polymorphism, which, divorced from implementation, enable powerful type inference. To make it work, some magic happens under the hood --- for now, refer to the source code to gain a deeper understanding. This document is merely intended to be a user's guide.

The library was developed for the core concept transformation algebra (CCT) for geographical information, which can act as an example.

Concrete types and subtypes

To specify type transformations, we first need to declare base types. To this end, we use the Type.declare function.

>>> from transformation_algebra import *
>>> Any = Type.declare("Any")

Base types may have supertypes. For instance, anything of type Int is also automatically of type Any, but not necessarily of type UInt:

>>> Int = Type.declare("Int", supertype=Any)
>>> UInt = Type.declare("UInt", supertype=Int)
>>> Int <= Any
True
>>> Int <= UInt
False

Complex types take other types as parameters. For example, Set(Int) could represent a set of integers. This would automatically be a subtype of Set(Any).

>>> Set = Type.declare("Set", params=1)
>>> Set(Int) <= Set(Any)
True

A special complex type is Function, which describes a transformation. For convenience, to create functions, the right-associative infix operator ** has been overloaded, resembling the function arrow. A function that takes multiple types can be rewritten to a sequence of functions.

>>> add = Int ** Int ** Int
>>> abs = Int ** UInt

When we apply an input type to a function signature, we get its output type, or, if the type was inappropriate, an error:

>>> add.apply(Int).apply(UInt)
Int
>>> add.apply(Any)
...
Subtype mismatch. Could not satisfy:
    Any <= Int

Schematic types and constraints

Our types are polymorphic in that any type also represents its supertypes: the signature Int ** Int also applies to UInt ** Int or indeed to Int ** Any. We additionally allow parametric polymorphism, using the TypeSchema class.

>>> compose = TypeSchema(lambda α, β, γ: (β ** γ) ** (α ** β) ** (α ** γ))
>>> compose.apply(abs).apply(add.apply(Int))
Int ** UInt

Don't be fooled by the lambda keyword --- this is an implementation artefact and does not refer to lambda abstraction. Instead, the TypeSchema is initialized with an anonymous Python function whose parameters declare the variables that occur in its body. This is akin to quantifying those variables. When instantiating the schema, these generic variables are automatically populated with instances of type variables.

When you need a variable, but you don't care how it relates to others, you may use the wildcard variable _. The purpose goes beyond convenience: it communicates to the type system that it can always be a sub- and supertype of anything. (Note that it must be explicitly imported.)

>>> from transformation_algebra import _
>>> size = Set(_) ** Int

Often, variables in a schema are not universally quantified, but constrained to some typeclass. We can use the t | c operator to attach a typeclass constraint c to a term t. c can then specify the typeclass in an ad-hoc manner, using the t @ [ts] operator (meaning that a term t must be a subtype of one of the types specified in [ts]). For instance, we might want to define a function that applies to both single integers and sets of integers:

>>> sum = TypeSchema(lambda α: α ** α | α @ [Int, Set(Int)])
>>> sum.apply(Set(UInt))
Set(UInt)

Typeclass constraints can often aid in inference, figuring out interdependencies between types:

>>> f = TypeSchema(lambda α, β: α ** β | α @ [Set(β), Map(_, β)])
>>> f.apply(Set(Int))
Int

Finally, operators is a helper function for specifying typeclasses: it generates type terms that contain certain parameters.

>>> Map = Type.declare("Map", params=2)
>>> operators(Map, param=Int)
[Map(Int, _), Map(_, Int)]

Algebra and expressions

Now that we know how types work, we can use them to define the data inputs and operations of an algebra.

>>> example = TransformationAlgebra(
        one=Data(Int),
        add=Operation(Int ** Int ** Int)
        ...
)

In fact, for convenience, you may provide your definitions globally and automatically incorporate them into the algebra:

>>> one = Data(Int)
>>> add = Operation(Int ** Int ** Int)
>>> example = TransformationAlgebra(**globals())

It is possible to define composite transformations: transformations that are derived from other, simpler ones. This definition should not necessarily be thought of as an implementation: it merely represents a decomposition into more primitive conceptual building blocks.

>>> add1 = Operation(
        Int ** Int,
        derived=lambda x: add(x, one)
    )
>>> compose = Operation(
        lambda α, β, γ: (β ** γ) ** (α ** β) ** (α ** γ),
        derived=lambda f, g, x: f(g(x))
    )

With these definitions added to example, we are able to parse an expression using the TransformationAlgebra.parse() function. If the result typechecks, we obtain an Expr object. This object knows its inferred type as well as that of its sub-expressions:

>>> e = example.parse("compose add1 add1 one")
>>> e.type
Int
>>> print(e.tree())
Int
 ├─Int ** Int
 │  ├─(Int ** Int) ** Int ** Int
 │  │  ├─╼ compose : (β ** γ) ** (α ** β) ** α ** γ
 │  │  └─╼ add1 : Int ** Int
 │  └─╼ add1 : Int ** Int
 └─╼ one : Int

If desired, the underlying primitive expression can be derived using Expr.primitive():

>>> print(e.primitive().tree())
Int
 ├─Int ** Int
 │  ├─╼ add : Int ** Int ** Int
 │  └─Int
 │     ├─Int ** Int
 │     │  ├─╼ add : Int ** Int ** Int
 │     │  └─╼ one : Int
 │     └─╼ one : Int
 └─╼ one : Int

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

transformation_algebra-0.1.2.tar.gz (29.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

transformation_algebra-0.1.2-py3-none-any.whl (28.3 kB view details)

Uploaded Python 3

File details

Details for the file transformation_algebra-0.1.2.tar.gz.

File metadata

  • Download URL: transformation_algebra-0.1.2.tar.gz
  • Upload date:
  • Size: 29.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.4.1 importlib_metadata/3.10.0 pkginfo/1.7.0 requests/2.25.1 requests-toolbelt/0.9.1 tqdm/4.57.0 CPython/3.9.2

File hashes

Hashes for transformation_algebra-0.1.2.tar.gz
Algorithm Hash digest
SHA256 35abc216c9d55ea4a59193e2c7253d545e684cef3c67ffdcf7c45390ded4a53a
MD5 cfb9703dcf2983c0b18924713dbdd200
BLAKE2b-256 b817519adbfe2c0a55cffc0351754616e72553fc4bbf9a7cf321691bae2dfc08

See more details on using hashes here.

File details

Details for the file transformation_algebra-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: transformation_algebra-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 28.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.4.1 importlib_metadata/3.10.0 pkginfo/1.7.0 requests/2.25.1 requests-toolbelt/0.9.1 tqdm/4.57.0 CPython/3.9.2

File hashes

Hashes for transformation_algebra-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 33f534dfc94ab1b0985e943ce056cc358eb5cdb85e212f54d4ee8b5397c78182
MD5 d745fbd1236eadc50d5855c0d9c76660
BLAKE2b-256 4326491d1c97ae608f88fde96a896ed5430bd2000fc4b4a7cbba78f9483e7ed5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page