Idris2+WebGL, part #10: Implicit arguments & monad transformers
DrBearhands
Posted on February 16, 2021
I found something interesting while implementing existentially quantified phantom types as explained in my last log entry. Note the last line of the createProgram
function:
export
data Program : Type -> ProgramStatus -> Type where
MkProgram : (1 _ : AnyPtr) -> (Program p s)
public export
programCont : ProgramStatus -> Type -> Type
programCont s a = (1 _ : forall p . (1 _ : Program p s) -> a) -> a
export
createProgram : MonadLState GLContext m => m (programCont Unlinked a)
createProgram =
state $ \gl =>
prim__createProgram gl $ \prog, gl' =>
(\f => f $ MkProgram { p=() } prog) # gl'
The MkProgram
constructor, in its definition, appears to only take an AnyPtr
as argument. In actuality, it also needs to know the type values for p
and s
in order to construct a Program p s
. These arguments are implicit. In most common cases the values p
and s
can be derived from context, but not always, and not on this line. We need to specify the value for p
, set to the unit type in this case. Without it, we get the following error:
Error: Unsolved holes:
JSFFI.WebGL2.{p:1210} introduced at:
JSFFI/WebGL2.idr:309:14--309:32
|
309 | (\f => f $ MkProgram prog) # gl'
| ^^^^^^^^^^^^^^^^^^
Knowledge that p=()
is lost outside the scope of the function, so we retain the functionality of using p
as an existential type.
In contrast, Haskell is fine with leaving a phantom type unspecified:
glCreateProgram :: MonadGL m => GLIO m s s (ProgramContinuation i)
glCreateProgram = LessSafe.glCreateProgram >>= (\prog -> return (ProgCont (\f -> f (Program prog))))
Where Program
is a similar constructor with unknown phantom type. Perhaps Haskell's type checker is more advanced, but I expect having types as first class citizens inevitably requires more specificity.
In this particular case the implicit argument is hidden in a fairly deep layer of code so I'm not concerned, but "unresolved holes" have started popping up more and more, and I'm not really happy about having to specify what applicative I intend to use on every other use of pure
.
I'll see how this develops, particularly after cleaning up the code base.
Speaking of cleaning up the code base, I failed to realize in part 6 that the specialized monads I use to replace IO
, and really even IO
itself, are just linear variants of the State
monad. So I made a linear state monad transformer. The code is pretty straightforward if you're familiar with monad transformers and linear types, so I won't post it here. Here's the repository at the relevant commit if you want to check it out yourself.
The change took a while but was pretty good for code quality. I no longer need to define weird lifting functions for specialized monads (like GL
and Alloc
). It's just:
Graphics : Type -> Type
Graphics =
LStateT Allocator (LState GLContext)
and things just work.
An interesting problem is how to deal with errors. My current approach is to return linear variants of Either
, but that has a big problem: a sum of a linear value is not a linear monad or functor. Consider the following:
public export
data LResult : Type -> Type -> Type where
Err : (_ : e) -> LResult e a
Ok : (1 _ : a) -> LResult e a
export
LFunctor (LResult e) where
map fn res = case res of
Err e => Err e
Ok val => Ok $ fn val
This code will not type check! In the map
function, fn
is linear, but it is not consumed in the Err
case branch. That's not allowed.
We can still use LResult
as a return type without relying on monadic composition, but the resulting code will be far more nested. Our happy flow will become contaminated with edge-case handling... not the best.
Apparently the author of Idris has already considered the problem of error handling in conjunction with linear types and has a solution, which I look forward to reading about and sharing next time.
Posted on February 16, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.