Linearity continued
Sifting through Idris’ literature on quantitative type theory I discovered that the issues with linearity I had in my last log are not just a complete misunderstanding on my part.
One of the larger underlying issues is a lack of polymorphism over
quantities, which Edwin Brady points out explicitly in the paper. This
is a bit annoying by itself (see my previous log entry) but also causes
troubles with do-notation as the >>=
operator is
non-linear. Specifying a linear variant of >>=
solves
this issue as Idris allows for overloaded operators:
(>>=) : (1 act : IO a) -> (1 k : a -> IO b) -> IO b
>>=) = io_bind (
This is noted somewhat as an afterthought at the end of section 3 of the paper. IMO it should be more prominently displayed in the Idris crash course. I’m making a mental note to make a merge request once I have linear types figured out.
I’m still fuzzy on why quantities seem to only be defined on negative positions, forcing programmers to use continuations, but think I’m starting to understand, though not necessarily agreeing.
If we have a function f : (1 _ : A) -> B
, and some
1 x : A
, then in order to ensure x
is consumed
only one, we must ensure f x
is only consumed once, since
it “contains” x
. So the result of application of a function
to a linear argument results in a linear value either way. Noting
linearity for all outputs while currying seems excessive:
f : (1 _ : A) -> (1 _ : (B -> 1 _ : C))
-- vs
f : (1 _ : A) -> B -> C
On the other hand - and this is where I lose track of things a bit - it is possible, and necessary, to have non-linear outputs for destructors:
destroyA : (1 _ : A) -> IO ()
Which isn’t entirely non-linear of course since the “world” inside IO is linear. Similarly, it’s possible to mix linear with non-linear data:
data MyComplexType : Type where
-- the first argument to MkMyComplexType is non-linear, the second is linear
MkMyComplexType : (valConst : A) -> (1 valLin : B) -> MyComplexType
which can be a return type. Well fuck me…
It looks like linear type systems have some fundamental,
near-philosophical questions to answer. Is a function from linear to
non-linear data valid? It seems quite acceptable in principle to have a
function consume a resource and create knowledge. But this breaks
referential transparency. What about lazy evaluation? That would ensure
f x
would be evaluated only once anyway. Perhaps there’s
something to be said for dumping functions as first-class citizens in
favor of linear types? We might have the mathematics, but not yet, I
believe, the practical insights.
With that said, I did manage to use linear types in my WebGL program
so that it compiles / type checks. Unfortunately there are still some
issues left on the FFI front. For instance, some functions, such as
glAttachShader
, will consume / modify 2 linear values. It
makes sense to then return them in a tuple:
attachShader : GLContext -> (1 _ : GLProgram) -> (1 _ : GLShader) -> (GLProgram, GLShader)
However, the FFI does not support tuples! So we have to use a continuation again. This seems ugly and inefficient. Ugly because of excessive nesting, and inefficient because it loses optimization opportunities, the compiler cannot know that values are merely modified and would have to treat the “new” linear value as though it were really new. At least not without knowledge of the backend. I therefore remain of the opinion that some form of “inout” notation to denote modified values would have value in FFI definitions at least.
We can at least get rid of the uglyness of continuations, but I have
a feeling this is a bit naughty and it has already led to problems.
Rather than having an IO ()
return value, we use a
polymorphic return type and create a tuple in a wrapper function:
%foreign "browser:lambda: (type, gl, program, shader, cbk) => {gl.attachShader(program, shader); return cbk(program)(shader);}"
prim__attachShader2 : AnyPtr
-> (1 _ : AnyPtr)
-> (1 _ : AnyPtr)
-> (1 _ : (1 _ : AnyPtr) -> (1 _ : AnyPtr) -> a)
-> a
export
attachShader2 : GLContext -> (1 _ : GLProgram) -> (1 _ : GLShader) -> (GLProgram, GLShader)
GLContextWrapper gl) (GLProgramWrapper program) (GLShaderWrapper shader) =
attachShader2 ($ \program', shader' =>
prim__attachShader2 gl program shader GLProgramWrapper program', GLShaderWrapper shader') (
This will avoid deep indentation in the context of a larger program:
-- create program, vs, and fs
let
= attachShader2 gl program vs
(program', vs') = attachShader2 gl program' fs
(program'', fs') = linkProgram gl program''
program''' in do
-- delete vs' and fs' and "return" program'''
vs
-- create program, vs, and fs
$ \program', vs' =>
attachShader gl program vs $ \program'', fs' =>
attachShader gl program' fs let
= linkProgram gl program''
program''' in do
-- delete' vs and fs' and "return" program'''
AFAIK we still need continuations when creating resources, but at least that does not happen as often.
Now you may have noticed the little type
argument in the
JavaScript definition for the polymorphic function. This took me a while
to figure out. When you have polymorphism, Idris code compiled to JS
will pass the type (undefined
in this case) to the function
as the first argument. This makes perfect sense for a dependently typed
language when you think about it, but was definitely a gotcha when
receiving runtime “undefined is not a function” exceptions… not what you
want when using a type-safe language. I am unsure if I can use
quantitative types again in this case, but this time with 0 quantity, to
remove this type argument at runtime; it certainly sounds like the
purpose of erasure. Either way, this way of defining foreign functions
seems rather off-spec, but I’ve decided to go with it anyway because of
the effect on code readability.
I’ve learned a lot about linear typing in Idris, and not all that information is easy to find, but this log was never meant as a tutorial, so I will probably write one in the near future.
And now I have the boring task of redoing all my foreign function bindings to apply what I learned…