TL;DR: How do I write interpreters (run :: Lang a b -> whatever) for my Lang GADT?
I have a DSL:
data Lang a b where
F :: Lang x y
G :: Lang x y
I want to build programs like:
prog :: Lang a b
prog = G . F
And interpret them arbitrarily, where the replacement for F and G can be any function, so long as their types compose properly.
-- run ~= (+1) . fst
run :: Lang a b -> ((Int, b) -> Int)
run F = fst
run G = (+1) -- error: couldn't match type (Int, b) with Int
Intepretation is my big issue. How do I write functions like:
run :: Lang a b -> whatever
-- eg reusing the variables
run :: Lang a b -> (a -> IO b)
I've added notions of composition to my language:
data Lang a b where
...
Lift :: (a -> b) -> Lang a b
Comp :: Lang b c -> Lang a b -> Lang a c
instance C.Category Lang where
id = Lift id
(.) = Comp
This allows me to build programs, but not interpret them. I understand the compiler's errors to me, when I try to instantiate one specific term to some type that is not the overall run fn's type.
This seems a bit like Free approaches, perhaps an Indexed Free? Relatedly, this is a much simplified version of my real problem, that largely uses Arrows, but, I think this simplified formulation encapsulates my issue.
Here's a runnable test environment:
{-# LANGUAGE GADTs #-}
module T15_GADT_Lang where
import qualified Control.Category as C
------------------------------
-- * My Lang
data Lang a b where
-- | DSL
F :: Lang x y
G :: Lang x y
-- | Composition
Lift :: (a -> b) -> Lang a b
Comp :: Lang b c -> Lang a b -> Lang a c
instance C.Category Lang where
id = Lift id
(.) = Comp
------------------------------
-- * Program A can be interpreted in 2 different ways
progA :: Lang a b
progA = F
runA1 :: Lang a b -> (Int -> String)
runA1 F x = show x <> "!"
runA2 :: Lang a b -> ([Double] -> String)
runA2 F x = show x <> "!"
progAWorksNicely = do
putStrLn $ runA1 progA 123
putStrLn $ runA2 progA [1..3]
------------------------------
-- * Program B, I can't interpret the interim functions indepenedently
progB :: Lang a b
progB = G C.. F
-- | get the fst and increment it
runB1 :: Lang a b -> ((Int, b) -> Int)
runB1 F = fst
runB1 G = (+1) -- error: couldn't match type (Int, b) with Int
-- | Get the head and duplicate it
runB2 :: Lang a b -> ([a] -> (a, a))
runB2 F xs = head xs -- you're giving an `a`, but it should be `(a,a)`
runB2 G x = (x, x)
You can write these different interpretations using only
LiftandComp, but it is less obvious how to interpretFas bothfstandheadin a way that makes senseThe problem with
FandGis that their indices are completely universally quantified, that means that composing with them might as well be untyped because any input and any output is valid!If you want type-safe composition of
G . Ftheir interpretation must connect to those types somehow so you must provide indexes forLangthat unify these two usesIn the case of
Liftthe input and output types are clearly reflected in the indices. Each of them could be made into a separate constructors that are indexed differently.If you want to interpret
FasfstandGas(+ 1)you you have the types matchyou can similarly interpret as an IO-kleisli arrow
tangent
You can also interpret it by abstracting the constructors into methods of a type class and interpreting it constrained by that class
I imagine the aim is to treat
Lift idas the identity ofComplike the category operations are. Language can be expressed as a "freeCategory"or a free
Arrowwhich hasarr :: Arrow arr => (a -> b) -> arr a bWhich can be interpreted at
Langc (->)andLangc (Kleisli IO).In fact you can skip having an "initial"
Langencoding and define it as "final"