I’m losing track here! So …
I am developing a POWERFUL generalised algebra that seems obvious but I have
not seen implemented anywhere else.
There are several parts to this so I have to present them in pieces.
The problem is managing the combination of all the pieces!
In most programming languages you can index a tuple:
(1,”hello”,42.3).1 // “hello”
This is actually an application of a projection. In Felix you can separate
out the projection:
typedef isd = int * string * double;
var prj = proj 1 of (isd); // stand alone projection function
var x = 1,”hello”,42.3; // tuple
println$ x.prj; // application
The projection is an ordinary function of type
isd -> string
so you can compose projections: if you have a tuple
typedef outer = int * isd * long;
pouter = proj 1 of (outer);
pinner = proj 1 of (isd);
prj = pouter \odot pinner; // reverse composition
var x = 2, (1, “hello”, 42.3),33L;
println$ x.prj; // “hello” again
The fact that projections are ordinary functions and can therefore be
composed by ordinary function composition is basic category theory.
Felix goes another step! You can have projections of *pointers* as well
as values. Given a pointer to a tuple, a projection can be applied
to produce a pointer to a component:
var x = (1,”hello”,42.3);
var pprj = proj 1 of (&isd);
ps = &x . pprj;
println$ *ps; // “hello”
ps <- “bye”; // store value in component
println$ *ps; // “bye”
println$ x; // (1,”bye”, 42.3);
This is fairy easy to understand but the implications are PROFOUND.
The address calculus is purely functional and referentially transparent.
There are no lvalues or references. The only imperative code involved
is the store operator <-. Just to repeat LVALUES AND REFERENCES
ARE ELIMINATED ENTIRELY.
Felix goes another step to keep up with C++: we have read only,
write only and read-write pointers. The normal pointer is read-write,
which is a subtype of both read pointers and write pointers.
The syntax for address taking is:
&x // read-write pointer
&<x // read only pointer, aka pointer to const
&>x // write only pointer
and the type notation follows the pattern (&int, &<int, &>int).
The next complication is compact linear tuples. These work the
same as non-compact linear types: that’s the specification!
However the type of pointers is different:
_pclt<D,C>
is the type of a pointer to a C stored in a compact linear product D.
The representation consists of a pointer to D, a machine word,
together with a divisor and modulus used to obtain the relevent
component by the formula
*p / divisor % modulus
This is a generalisation of the well know method for obtaining
a sub-bit-string from a bit array, where the divisor and modulus
are powers of 2.
Projection functions are a bit tricky because there are
TWO cases: the argument pointer of the projection
can be either an ordinary pointer or a compact linear pointer.
The result in both cases is a compact linear pointer.
The value composition for compact linear projections is
v / (divisor1 * divisor2) % modulus2
Pointer projections store a divisor and modulus
the same as value projections.
In the Ocaml all the above complex interaction is made harder
because the representation of a tuple type is a list of types,
unless all the types are the same, in which case the type
is an array and we just use the base type and the index count.
In fact, arrays are more general: the index can be any compact
linear type, not just a unit sum, that is, arrays don’t have to be
linear. But we’ll get to that later .. or rather .. we won’t because
that’s where stuff hits the fan.
Now back again. For arrays, there is a second kind of projection,
called an array projection. Unlike tuple projections, array
projections allow the projection index to be an expression,
not just a constant. This can work because all the components
have the same type.
We have to make array projections work for both values
and pointers, and for both ordinary types and compact linear
types. The complexity is around cubic now!
But we’re not done yet!
Category theory has a concept called duality. Products have
a dual type called a coproduct or sum type. Instead of
projections, we have injections. These are ordinary functions
too, just as projections are.
Coproducts work backwards from products in the following sense:
a product constructor produces a set of projections, the projections
are destructors (analysers, eliminators, extractors). There can be
many kinds of constructors for products: tuples are one kind,
but in Felix we also have records (with named projections instead
of numbered ones), and structs. The important thing is that the
projections for any product obey laws: the projections *characterise*
the product.
For coproducts there are many kinds of constructors too,
and there are laws, so now, the injections characterise the
corproduct. We use pattern matching usually for destructors.
We can have compact linear sums as well as compact
linear products.
You think its getting messy .. we’re nowhere near done yet!
As with products, where products of the same type repeatedly
are arrays, we have repeated sums, which, naturally,
should be called coarrays! For example unitsums are coarrays!
Here is another:
4 *+ int = int + int + int + int
The operator *+ is used for a repeated sum, just as ^ is used for
a repeated product. Technically it is isomorphic to a product,
however
4 * int
is a pair of an index and integer, whereas 4 *+ int is a coarray.
Pity about the notation: the fact is the universal representation
of coproduct is in fact precisely and index and a pointer
to a heap allocated value of the type so it really is a pair,
but there are optimisations so the representation isn’t uniform.
So we need another type constructor.
The thing about coarrays is the same as arrays. You can
index the injections used to create a sum type with
an expression, whereas for an ordinary sum the index
has to be a constant.
Getting messy? We’re not done yet! Don’t forget
compact linear types!
But there is much worse to come!
It is possible to use an expression to index a tuple!
You may wonder, how can we do that, if the types of
the components are different???
Here is the type of a *generalised projection*:
3 -> int * string * double -> int + string + double
We have unified the types in the tuple by just adding them
together. There is another way to look at this, it is
NOT equal but it is isomorphic: any tuple IS an array:
(int + string + long) ^ 3
and now an ordinary projection on that is isomorphic to
a generalised projection on a tuple.
What makes arrays special is that we can *smash* the index
of the generalised projection away:
3 -> int * int * int -> int + int + int |— smash -> int
Smash throws away the information about which component was accessed
which is otherwise required so you can do a pattern match so the constructor
arguments can be different types.
The point is that with smash, which is a generic operator
WE NO LONGER NEED TUPLE PROJECTIONS.
Since ALL projections can be dynamically indexed, we only need
generalised projections, to get back to an ordinary projection
you can just apply smash if the result is a coarray, or use pattern
matches for each case otherwise. In other words, we can define
a standard constant projection in terms of an array projection,
even for tuples.
Unfortunately things are getting really complicated now.
The problem is, although we can fully generalise the computations
to a very small number of operations, they will run dead slow.
For example you do NOT want this:
(1,”hello”, 42.3).1
to actually construct a tuple, then construct a projection,
then apply the projection! NO! You actually want to reduce
the expression to just
“hello”
immediately. So actually we DO need the compiler to handle a
lot of special cases, because the generalised computations
aren’t fast enough.
Don’t forget that when we have generalised projections
we have to have generalised injections too!
BUT WE ARE NOT EVEN CLOSE TO BEING FINISHED YET.
So far we have assumed the index of an array is a unit sum.
Ditto, coarrays.
What programmer hasn’t heard of a thing called a matrix?
A matrix is NOT an array of arrays:
double ^ 3 ^ 4 // array of arrays
No way! A matrix is a two dimensional array:
double ^ (4 * 3) // matrix
you access a matrix like this:
x. (i,j)
Notice the index is a tuple. Of course there are isomorphisms here
but that’s the point.
And OF COURSE matrix is only an example.
THE INDEX TYPE OF AN ARRAY CAN BE ANY COMPACT LINEAR TYPE.
Actually an array is just a function so we can go even further, and that’s
another complication because Felix treats arrays as object to which
we apply projection functions, but there is another view, that an array
is a function applied to an index value.
And of course don’t forget that the repeat count of a coarray also
can be any compact linear type.
And again, for an array, the base type can be compact linear and itself
an array and, the index type could also be an array! In fact:
double ^ ( 4 ^ 2 )
is of course just a square matrix with both dimension 4.
You can see why it is absolutrely essential to have a compact
calculus to cover all the intricate combinations, and then
we can do special cases knowing that everything will work
even if it is slow.
The problem here isn’t quadratic, cubic, or even quintic.
I have no idea of the order of the problem but it feels like
there are at leastr 8 bifurcations so the problem is octonioc.
2^8 of course is 256. That’s a LOT of cases to write out
in the compiler!!!!!
—
John Skaller
ska...@internode.on.net