Restricting arguments to a list
Oh yes this is awesome.
Many WebGL functions take a GLenum
value, secretly an
integer Number
in JavaScript. The problem with this is that
it’s not a compile error to pass an invalid value, even though this
could be statically determined. For instance:
glCreateShader GL_POINTS
is valid at compile time, even though it is nonsensical.
glCreateShader
wants a shader type, not a draw mode.
Typically, we would remedy this by creating a sum type:
data ShaderType = GL_VERTEX_SHADER | GL_FRAGMENT_SHADER
But this still has a few problems: some values belong to different
input sets, and we have added overhead to convert from
ShaderType
to Int
and vice-versa.
While these problems can be fixed, the solution is not particularly elegant.
So, I’ve been diving into Idris2 library code to find one of its less
well-documented features (it’s
there, it’s just not very obvious if you don’t already know what it
is). I found exactly what I was looking for in auto proofs. I will
illustrate with a simplified version of the glCreateShader
function. This is the (simplified) regular version:
glCreateShader : GLenum -> GL ()
This function signature pretend to accept any GLenum
,
but will cause errors on anything that is not a
GL_VERTEX_SHADER
or GL_FRAGMENT_SHADER
. With
auto proofs we can write:
glCreateShader : (shaderType : GLenum) -> {auto 0 prf : Elem shaderType [GL_VERTEX_SHADER, GL_FRAGMENT_SHADER]} -> GL ()
There’s a few things going on here.
(shaderType : GLenum)
is not substantially different from
GLenum
, but it gives a name to the variable in the type
signature, so that we can use it there. Then we have
{auto 0 prf : Elem shaderType [GL_VERTEX_SHADER, GL_FRAGMENT_SHADER]}
,
let’s ignore auto
for now, {0 prf : X}
indicates we have a variable prf
of type X
that only exists at compile time (it has multiplicity 0).
Elem x xs
is the type for a proof that x
is in
the list xs
. By prepending auto
, we tell Idris
to find the proof automatically, rather than relying on the programmer.
So,
{auto 0 prf : Elem shaderType [GL_VERTEX_SHADER, GL_FRAGMENT_SHADER]}
is a proof that shaderType
is a value found in
[GL_VERTEX_SHADER, GL_FRAGMENT_SHADER]
, the proof only
exists at compile time, is constructed automatically if possible, and we
don’t have to pass it explicitly.
To call glCreateShader
, we can still write
glCreateShader GL_VERTEX_SHADER
, but if we try
glCreateShader GL_POINTS
, the Idris compiler will complain
that it cannot find an implementation (an implementation is a proof) for
Elem GL_POINTS [GL_VERTEX_SHADER, GL_FRAGMENT_SHADER]
.
There remains the situation where we want to pass a variable to
glCreateShader
with a value unknown at compile time. I find
that these cases are rare, but when they occur, the programmer simply
has to provide the proof, or create a context from which the proof can
be automatically derived. While this is a restriction, that was exactly
the point, not just trusting the programmer is always right, because
we’re really not.
I am really happy I discovered this feature. Not only does it fix
this annoying issue I had with GLenum
s, but I believe it
was the missing piece for a couple of problems I’ve had in the past,
e.g. the creation of open sums and open products.