4

I have an AST which describes both pure and impure computations, with the impure computations keeping track of state at the type level. I'm using a HOAS representation; the relevant constructors of the AST GADT are as follows:

data AST a where
  Lam :: (AST a -> AST b) -> AST (a -> b)
  (:$) :: AST (a -> b) -> AST a -> AST b

  Return :: AST (a -> (a := i) i)
  Bind :: AST ( (a := j) i -> ( a -> q j ) -> q i )

  -- then some stateful operations
  -- for instance if the indexed state keeps track of a CD player tray:
  -- OpenCDPlayer :: AST ( ( () := 'Open ) 'Closed )
  -- CloseCDPlayer :: AST ( ( () := 'Closed ) 'Open )

Here (a := j) i is McBride's AtKey type, which consists of a type annotated with beginning and end states (i and j, respectively). Bind corresponds to McBride's "angelic bind", which boils down to the provision of a function

bind :: AST ( (a := j) i ) -> Codensity AST (a := j) i

where Codensity is the indexed version of the usual codensity transformation.

My question is about how to implement evaluation in this situation. Usually, one would have an evaluation function of the form:

eval :: AST a -> a

However in this case I am at a loss as to what type give to the evaluation function, as when state-annotated types are involved I think evaluation should be formulated as having a type close to the following:

evalM :: Codensity AST (a := j) i -> Codensity ((->) St) (a := j) i

where St allows us to keep track of state at the value-level, so that the right-hand side is an indexed state monad (Codensity ((->) s) ~ State s).
This type signature for evalM seems rather intuitive, as it describes a natural transformation of indexed monads, and seems to be the core of the task at hand.

Question: how can I formulate and implement evaluation in this context? What type should it have, and how would I implement the evalM function above?

Sam Derbyshire
  • 719
  • 1
  • 4
  • 12
  • There seems to be an interesting problem here but the types most likely will need a lot of fixing (e.g., `(() := 'Closed) 'Open)` is an empty type, so probably not what you're looking for). Can you give some more context about the problem you are trying to solve? – Li-yao Xia Dec 15 '19 at 16:12
  • 1
    `( () := 'Closed ) 'Open` is uninhabited, but I am only dealing with values of type `AST ( ( () := 'Closed ) 'Open) `. – Sam Derbyshire Dec 15 '19 at 16:16
  • 2
    For context, the AST (as formulated in the OP) is already being used to implement an EDSL that compiles to a low-level shading language on the GPU, and I want to implement an evaluator to be able to run that same code on the CPU side within Haskell. The types as written in the OP are working in my situation, although they might of course need adjustement in order to implement evaluation (although I would hope to be able to keep disruption to a minimum). – Sam Derbyshire Dec 15 '19 at 16:19
  • 1
    The indexed state I am using is quite complex, but as far as evaluation is concerned in my situation it should boil down to a map of bindings. For instance I have an AST constructor akin to `NewBinding :: LacksBinding name st => Proxy name -> AST ( a -> ( () := InsertBinding '(name, a) st ) st )`. (The rest of the indexed monadic state is being used for type-level validation and can be discarded for the purposes of writing an evaluator.) – Sam Derbyshire Dec 15 '19 at 16:27
  • You are of course right that the uninhabitedness of `( a := j ) i` for distinct i, j is a central problem. I was envisioning an instantiation of the form `eval' :: AST ( ( a := j ) i ) -> IndexedStateMonad i j a` and then the user can use some `runIndexedState` function to evaluate the right hand side given a starting state compatible with the starting state type annotation `i`. – Sam Derbyshire Dec 15 '19 at 16:33
  • Ok so the index of `AST` is just meant to be some syntax of the types of the EDSL. That the index is a `Type` and that `:=`/`AtKey` is used does not really matter. I'd be curious to see how your compiler already handles `Lam`, `Bind` and maybe one of the operations. – Li-yao Xia Dec 15 '19 at 16:48
  • 1
    It handles lambdas and bind by doing code-generation for their arguments, applying the functions to these arguments, and doing code-generation for the result. For an operation such as adding a binding, it adds the value of the binding to the state of the code-generator monad, so that code-generation for the "get value of binding" operation can use that monadic state to retrieve the value. I'm afraid the comment section here is a bit small for a more detailed answer. – Sam Derbyshire Dec 15 '19 at 17:17

0 Answers0