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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bec9666e0eb392f323175bf408919397c8f5e684d38bd92eab0ba601b3163e5e
|
|
| MD5 |
f7e99444569f4a962327866d1cde25ca
|
|
| BLAKE2b-256 |
a2b7e5b47eb749058ebfb5f196e2f66c7973890225562b3867e4377d10a98c34
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
53dc45b65c9b7f2ecdc9e8193d0f09c81bda0849828721f8abdb71db3bca78ab
|
|
| MD5 |
ad00c296b52653a503e859d1210fb873
|
|
| BLAKE2b-256 |
dc8fa3bb449bb2fbb5b26d19d3194ae6134a6cf24489ccb628e1bffe921d913d
|