> > [Is there anyone here? ]
> >
> > Yes 🤗
>
> Are you sure you’re all there?
>
> No.
Lets see who is paying attention. Here’s a Felix test.
What does this program output?
var x = (a=1, b=2, b= "hello",c=42.3);
println$ (match x with | (b=_ | r ) => r)._strr;
Pattern matching is pretty awesome with row polymorphism!
===================================================
Ok, that one was too easy. So here’s one that I don’t think
I can do:
Write a function that can reverse any tuple.
Is it possible?
You will need
* polymorphic recursion with type classes
* virtual types
* AND GADTs.
For sure you cannot do this in Ocaml or Haskell.
The actual question is: what is missing from Felix, if anything,
to make this work?
Felix can already do things you cannot do in Ocaml or Haskell.
When you say
println$ (1,”hello”, 4.2, true);
some very sophisticated front end stuff is used to do it. You saw above
in the first test the use of _strr to convert a record to a string. This is a
generic function provided by the compiler. In other words, it’s not written
in Felix. But converting a *tuple* to a string can be done in Felix using
row polymorphism and polymorphic recursion. The method is the same
as you’d use with C++ templates, using generic overloads to perform
polymorphic recursion.
Here’s the code, actually used, in the library:
First, we need a helper class.
class Tuple[U] {
virtual fun tuple_str (x:U) => str x;
}
The reason for this class is rather stupid. If you have
a virtual function f in a class, and you want f to call itself
on a different type .. aka polymorphic recursion .. you have
problems because the inner most f at the point of call
will be bound, which only gives you ordinary recursion: in
fun f(x:T) = > f (stuff);
the calls to f calls itself, which means stuff has to be of type T
or Felix barfs. You actually want to call f “as seen from the outside”
but it isn’t possible unless you use explicit qualification which means
you need to know the whole scoping environment of f.
The solution to all problems in computer science is to drink
more coffee. In programming, tip the coffee on your keyboard.
That works because Apple will take at least 3 months to get
replacement parts. In the meantime, you just use indirection:
fun g(x:T) => f (x);
fun f(x:T) => g (stuff);
by introducing a helper function. We use tuple_str as our helper.
Now the code is done by template instantiations,
first an inner tuple print that just puts commas in:
instance[U,V with Str[U], Tuple[V]] Tuple[U ** V] {
fun tuple_str (x: U ** V) =>
match x with
| a ,, b => str a +", " + tuple_str b
endmatch
;
}
instance[U,V with Str[U], Str[V]] Tuple[U * V] {
fun tuple_str (x: U * V) =>
match x with
| a , b => str a +", " + str b
endmatch
;
}
Now, the top level, which just puts parens around stuff:
// actual Str class impl.
instance [U, V with Tuple[U ** V]] Str[U ** V] {
fun str (x: U ** V) => "(" + tuple_str x +")";
}
instance[T,U] Str[T*U] {
fun str (t:T, u:U) => "("+str t + ", " + str u+")";
}
instance[T] Str[T*T] {
fun str (t1:T, t2:T) => "("+str t1 + ", " + str t2+”)”;
}
You will notice an ANNOYANCE. You need three overloads,
not two. This is because Felix tuples are constructed with
a “Cons” like operator: if TAIL is a tuple, then Consing a
new value of the front makes a new tuple. That operator
is spelled
head ,, TAIL
The obvious base type, (), does not work. Its a tuple, and so’
x,, ()
is a tuple, right?
Yes indeed, a tuple with 1 value. The problem is, a tuple with one
value in Felix is EQUAL to that value. Its not merely an isomorphism.
Its an equality. So you cannot cons a new element on front, because
x may not be recognised as a tuple.
I said “may not” deliberately: of course, x is an arbitrary value,
so it might just happen to be a tuple! We don’t want to cons a new
element onto x if its a tuple, we want to make a pair, with x as the
RHS, and that’s what we have to do:
1,,2,,3,4
We have to use a pair as the BASE of the induction because
“single element tuples don’t exist”. Actually they do but they’re
equal to the single element. This is a f’ing PITA because all tuplic
polymorphic recursions need to use a pair as the base, and handle
unit separately. But it is worse because it means there are no
arrays of one element .. since an array is just a tuple with all
components the same type.
=================================
So, the way Felix does polymorphic recursion is the same as C++,
except Felix is properly type checked. Haskell can also do polymorphic
recursion, but it us more advanced …
because it is more retarded.
Haskell uses boxing which is really bad stupidity of lame morons that
are still using 1960’s tech. But the advantage over expanded types is
how Haskell can do run time polymorphic recursion. Whilst Felix and
C++ actually expand the code, Haskell can just generate binary for
a single function because all types have a universal representation,
namely a pointer (box).
However this has another advantage: Haskell can also do polymorphism
on dependent outputs. in C++ this is done with typedefs. For example
Container::value_type
Because it uses delayed type checking this bullshit works. In other
words C++ templates are not type checked until they’re monomorphised,
and monomorphisation is just done by lookup and gluing terms in,
in other words its just macro processing.
Felix can do this too. I introduced “virtual types” to do it.
A virtual type is not specified in the class, but is made concrete
in the instance.
The effect is this: you CANNOT do ordinary overloading on a virtual
type because you don’t know the actual type until monomorphisation
is done. But you CAN do polymorphic recursion, in other words,
you CAN call a virtual function on a virtual type, because the instance
isn’t resolved until monomorphisation.
So to reverse a tuple, unlike the string case, the OUTPUT of the
class virtual is no long invariant. In Felix, I had str as shown above
for tuples, and also comparisons! But these only worked because
the result of the function is invariant. To make the output vary
with the input required virtual types.
It’s worse! It doesn’t just require virtual types. It also required GADTS.
The reason is we are BUILDING a value not just decoding one.
To build a tuple we start with a pair of some type:
A * B
and then we need to cons a new head on it:
C ** (A * B) == C * A * B
where the ** constructor is the type of the ,, operator. It real easy to write out
a sequence:
E ** D ** C ** A * B
and extend it, but the output is a single type which has to be built up by a recursion.
So again solve the problem with more coffee and indirection. We have to make
a new type variable of tuple type to pack all the types into so we can prepend
a new head.
Its very hard. Roughly we have to build a function which is not evaluated yet,
that packs more and more things on the left of the base tuple. When finally
the analytical part of the polymorphic recursion is done, we have to
finally unravel that suspended computation.
Its easy to do this stuff with code. But we have to do it with the type system.
Gadts provides the suspensions. virtual types provide the indirection.
but with the extra complication of starting with a pair instead of a unit,
its really hard .. especially since Felix error messages are messy.
Note you CAN do this kind of thing in Haskell: you cannot reverse
a tuple because Haskell doesn’t actually have products: it uses
exponentials instead. [Technically there is no N-functor to create
tuples: the exponential is used to hack it instead. This simplifies
Haskell because the Curry/Uncurry adjunction doesn’t require a
representation. In fact, that is precisely how products are built,
so any implementation is a representative]
—
John Skaller
ska...@internode.on.net