The Ink Programming Language
Ink is a relation-based programming language. It is a based on a clear separation between data and processes, is reflective, offers choice between static or dynamic typing, features ddesign by contract and time-constraint programming.
Related documents:
- InkCodeScratchBook for my attempts to write interesting Ink code snippets
- InkSyntax for my attempts at writing a syntax reference and grammar for Ink
- MultiMethods, for a page collecting information on multi methods dispatching
1. Types and relations
Types and relations are the core concepts of Ink. A type is the formalisation of a data model, which consists in a composition of primitive elements (numbers, strings, lists, etc), which can be annotated with attributes.
For instance, Ink defines the Point type as follows:
type Point:
annotations are (basic, immutable)
structure is (
x : type Number
y : type Number
)
We see here that the type point is declared with the basic and immutable annotations, and that it declares its data structure to be composed of Numbers names x and y.
Relations are processes that are bound to two values (a value is an instance of type), they are pretty much like (if not equivalent to) generic functions.
For instance, here is a relation related to the Point type:
relation [p1:type Point] add [p2:type Point]: point := Point create point.x = p1.x + p2.x point.y = p1.y + p2.y [point]
This relation creates a new point which x and y values are the addition of the x and y value of both points. Note that the [point] symbol means return point (or yield point, as we will see later).
You can also note that there is something that looks like an invocation: Point create. In fact, the create symbol denotes a directive, which is a specific kind of relation. In our case, the create directive is a primitive directive, which is note declared in Ink.
Here is an example of directive which creates a clone of a point:
directive [p:type Point] clone : clone := Point create clone.x = p.x clone.y = p.y [clone]
2. Templating system
One of the particularities of Ink is to embed within the language itself a simple but effective templating system. In fact, any Ink expression or construct can be either concrete or abstract, which are defined as followed.
A concrete expression is an expression which can be directly evaluated, without requiring anything else than an evaluation context.
An abstract expression cannot be evaluated until some information is given to obtain a concrete form of the abstract expression.
Abstract and concrete expressions
For instance, look at the following expression
1 + 2
this is a simple addition, which can be easily computed, here is another form of this expression :
a + b
assuming that a = 1 and b = 2, and that both symbols are defined in the evaluation environment, this expression is equivalent to the preceding in this environment, and can be directly evaluated.
Now, let's consider the following pseudo-code :
f(x,y) = x + y
we have defined a function which can do the same operation as the two first expressions, provided it is given the proper arguments :
f(1,2)
this form can be evaluated to a concrete expression, which is 1+2. This was made possible because the function f, which is abstract, required two given values to become concrete.
Consider the following expression
f(1)
this expression cannot be evaluated because it is not concrete: we only know that f(1) = 1 + y, but we still lack information about what value the y symbol is bound to, thus f(1) is still an abstract expression.
Abstract expressions in Ink
Just as we have shown it in the previous introduction, Ink allows to define abstract expression, not only for function declarations, but also for any other expression.
For instance, the expression
1 + 2
can be abstracted as
[a, b]: a + b
which can be useful when the expression grows a little bit bigger
[a, b]: a + b , a + 10 * b
abstract expressions, as any other form of expressions can be assigned to slots:
abstractExpr := [a,b] : a + b
abstract expression values can then be made less abstract, or conrete by applying arguments to them :
abstractExpr (1) --> [b] : 1 + b abstractExpr (1,2) --> 3
You can also choose which arguments you would like to leave abstract, with the following forms :
abstractExpr ( _, 2 ) --> [a] : a + 2 abstractExpr ( b = 2 ) --> [a] : a + 2
In the first form we used the _ (underscore) as the explicit declaration for an abstract argument, and specified the value for the second argument. In this case, we explicitely set a as abstract.
In the second form, we used explicit naming to bind to the symbol b of the abstractExpr argument list the value 2. In this case, a was implicitely left as abstract.
Specifying constraints
For now, you may wonder what this mechanism is called a template system, as it does not seem to be more than a way to represent blocks of code as primitive types.
Ink template system is used to define arguments to any abstract expression, and more specifically for relations. In Ink paradigm, there can be multiple abstract expressions bound to the same relation name. For instance, we could consider the join relation as follows :
relation [a] join [b]: ...
which would evaluate this way
1 join 2 --> 12 "Hello" join "World" --> "Hello World"
In this case, we see that the join relation is an abstract expression with two arguments, but the same relation produces different results with different concrete arguments.
So the question is, how to declare a relation, or abstract expression to behave differently according to the given arguments ? This is the purpose of the template system : conditionnaly conretize abstract expression according to a specific logic. This what makes Ink template system a system.
Abstract expression can be conditionnaly expanded according to constraints. The above cases could be defined as follows :
relation [a:as Number] join [b:as Number]: [a * 10 + b ] relation [a:as String] join [b:as String]: [a + " " + b ]
The argument templates have now been expanded with a type constraint on every argument.
We could go further and specify different relations depending on the values of certain arguments :
relation [a:as Number, < 10] join [b: as Number, <10]:
[a * 10 + b]
relation [a:as Number, < 10] join [b: as Number, <100]:
[a * 100 + b]
relation [a:as Number, < 10] join [b: as Number, <1000]:
[a * 1000 + b]
you can note that it is not required to specify constraints such as b>10 for the second relation, as the relations are evaluated sequentially, in their reverse order of declaration, until one is matched.
How to determine to order constraints ? Which are more generic, or less generic ?
3. Object model
Ink supports object-oriented design. As we presented it, the basic elements of Ink are types and relations. Roughly, types can be considered as classes, and relations as functions that can be bound to classes.
For instance, looking back at our Point class, we could have declared the type as follows:
type Point:
annotations are (basic, immutable)
structure is (
x : type Number
y : type Number
)
relations are (
add! [p:Point]:
point := Point create
point.x = .x
point.y = .y
[point]
)
Here we can notice two things:
- The left member of the relation
addwas omitted - The
.xand.yexpressions have an implicit left member, which a value of the current type.
This notation is stricly equivalent to the form we presented above, it is expanded by the Ink compiler in the same way.
2.1 Data encapsulation
One of the basic mechanisms required by object-oriented design is encapsulation. Encapsulation means that direct access to an object internal state is forbidden, and always go through a set of accessors. Accessors are interface between the object internal state and the environment.
Accessors are split between accessors and mutators. An accessor allows to access the value of a part of the object state, while a mutator allows to change this value.
Here is an example of accessors for the upper-left attribute of the Rectangle type:
type Rectangle:
structure is (
upper-left: type Point
lower-right: type Point
)
.upper-left access is:
[.upper-left]
.upper-left mutate! <p:Point> is:
.upper-left = p
Now, the data structure attributes can be simply accessed:
r := Rectangle create r.upper-left = Point create (1, 1) r.lower-right = Point create (2, 2) print r.lower-right.x --> 2
2.2 Polymorphism
A second important point of object-orientation is polymorphism.
Polymorphism is handled in two different ways in Ink. The first one is by explicit typing: you simply express to which specific types a relation will apply. The other way is by expressing constraints on a value: what structure it should have, which annotations its type should have, and so on.
Polymorphism is then easily realised by specifying constraints on the parameters of a relation. Consider the following examples:
[p!:Point] add [p2:Point]: p : (.x, .y) = p2 : (.x, .y) [p!:.x Number, .y Number] add [p:.x Number, .y Number] p : (.x, .y) = p2 : (.x, .y) [p!] add [p2] p : (.x, .y) = p2 : (.x, .y)
The above declarations seem all similar, but only their process is similar: they apply to different types of values.
- First expression only applies to
Pointvalues - Second expression applies to any value which as
xandyattributes of typeNumber - Third expression applies to any values which have
xandyattributes.
Actually, there is one more constraint for the two last constraints : there must exist a mutator for the x attribute of the left member that accepts the value of the x attribute of the right member, and this is the same for the y attribute.
Scoping rules
Design patterns
Concurrency
Application operator and template system
One of the most important Ink idiom the application operator :. It is the basic way of expressing the program structure.
If we go back to our point addition relation, we see the following block of code:
point.x = p1.x + p2.x point.y = p1.y + p2.y
Can be rewritten :
(x, y) : <a> point.a = p1.a + p2.a
The application operator is a bit like the shell | operator: you apply the right member of the | to the output of the evaluation of the left member.
Pattern-Matching
The following examples adds any list that starts with two Number values to the point:
relation [p:Point] add ([x:Number], [y:Number], _ )
can also be written:
relation [p:Point] add ([x,y:Number], _ )
Syntax overview
Before getting into the depths of Ink, it would be good to have a slight overview of its syntax. Ink syntax shares some common points with Python, Io, Haskell and... ahem... C++ (just for templates, as you will see).
Ink syntax was designed to be both clear and flexible, trying to limit the possible ambiguities, trying to enforce the code source presentation to be the most coherent. Ink source code tries to be explicit, clear and clearly differentiate the important structural and semantic elements.
Relation naming convention
When a relation does not have any side effect (it is purely functional), it does not need any suffix.
If the relation has a side effects, i.e. it mutates one of the relation members, it must be suffixed by !.
If the relation is a predicate, i.e. it is purely functional and returns a Boolean value, it must be suffixed with ?.
This convention, inspired from Scheme, allow to clearly identify whether invoking a relation will have a side effect or not.
Parameters naming convention
Formal parameters in relations must be suffixed by ! when the given parameter value is mutated. By default, parameters are immutable, unless they are indicated as mutable by the ! suffix.
References
- UML and DSL a small comment by EhudLamm? on an article related to UML and DSL which I find close to the perspective I have in designing Ink.
Here is a very intersting library for Python which allows transparent access to C functions:
- CTypes, seems like a very straightforward way to use foreign libraries in a Python program. This could be adapted to Ink as well.
