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.1.tar.gz (29.0 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.1-py3-none-any.whl (27.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: transformation_algebra-0.1.1.tar.gz
  • Upload date:
  • Size: 29.0 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.1.tar.gz
Algorithm Hash digest
SHA256 bec9666e0eb392f323175bf408919397c8f5e684d38bd92eab0ba601b3163e5e
MD5 f7e99444569f4a962327866d1cde25ca
BLAKE2b-256 a2b7e5b47eb749058ebfb5f196e2f66c7973890225562b3867e4377d10a98c34

See more details on using hashes here.

File details

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

File metadata

  • Download URL: transformation_algebra-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 27.6 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 53dc45b65c9b7f2ecdc9e8193d0f09c81bda0849828721f8abdb71db3bca78ab
MD5 ad00c296b52653a503e859d1210fb873
BLAKE2b-256 dc8fa3bb449bb2fbb5b26d19d3194ae6134a6cf24489ccb628e1bffe921d913d

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