In Type-Safe Observable Sharing in Haskell Andy Gill shows how to recover sharing that existed on the Haskell level, in a DSL. His solution is implemented in the data-reify package. Can this approach be modified to work with GADTs? For example, given this GADT:
data Ast e where
IntLit :: Int -> Ast Int
Add :: Ast Int -> Ast Int -> Ast Int
BoolLit :: Bool -> Ast Bool
IfThenElse :: Ast Bool -> Ast e -> Ast e -> Ast e
I'd like to recover sharing by transforming the above AST to
type Name = Unique
data Ast2 e where
IntLit2 :: Int -> Ast2 Int
Add2 :: Ast2 Int -> Ast2 Int -> Ast2 Int
BoolLit2 :: Bool -> Ast2 Bool
IfThenElse2 :: Ast2 Bool -> Ast2 e -> Ast2 e -> Ast2 e
Var :: Name -> Ast2 e
by the way of a function
recoverSharing :: Ast -> (Map Name, Ast2 e1, Ast2 e2)
(I'm not sure about the type of recoverSharing.)
Note that I don't care about introducing new bindings via a let construct, but only in recovering the sharing that existed on the Haskell level. That's why I have recoverSharing return a Map.
If it can't be done as reusable package, can it at least be done for specific GADT?
Interesting puzzle! It turns out you can use data-reify with GADTs. What you need is a wrapper that hides the type in an existential. The type can later be retrieved by pattern matching on the Type data type.
data Type a where
Bool :: Type Bool
Int :: Type Int
data WrappedAst s where
Wrap :: Type e -> Ast2 e s -> WrappedAst s
instance MuRef (Ast e) where
type DeRef (Ast e) = WrappedAst
mapDeRef f e = Wrap (getType e) <$> mapDeRef' f e
where
mapDeRef' :: Applicative f => (forall b. (MuRef b, WrappedAst ~ DeRef b) => b -> f u) -> Ast e -> f (Ast2 e u)
mapDeRef' f (IntLit i) = pure $ IntLit2 i
mapDeRef' f (Add a b) = Add2 <$> (Var Int <$> f a) <*> (Var Int <$> f b)
mapDeRef' f (BoolLit b) = pure $ BoolLit2 b
mapDeRef' f (IfThenElse b t e) = IfThenElse2 <$> (Var Bool <$> f b) <*> (Var (getType t) <$> f t) <*> (Var (getType e) <$> f e)
getVar :: Map Name (WrappedAst Name) -> Type e -> Name -> Maybe (Ast2 e Name)
getVar m t n = case m ! n of Wrap t' e -> (\Refl -> e) <$> typeEq t t'
Here's the whole code: https://gist.github.com/3590197
Edit: I like the use of Typeable in the other answer. So I did a version of my code with Typeable too: https://gist.github.com/3593585. The code is significantly shorter. Type e -> is replaced by Typeable e =>, which also has a downside: we no longer know that the possible types are limited to Int and Bool, which means there has to be a Typeable e constraint in IfThenElse.
I will try show that this can be done for specific GADTs, using your GADT as an example.
I will use the Data.Reify package. This requires me to define a new data structure in which the recusive positions are replaced by a parameter.
data AstNode s where
IntLitN :: Int -> AstNode s
AddN :: s -> s -> AstNode s
BoolLitN :: Bool -> AstNode s
IfThenElseN :: TypeRep -> s -> s -> s -> AstNode s
Note that I remove a lot of type information that was available in the original GADT. For the first three constructors it is clear what the associated type was (Int, Int and Bool). For the last one I will remember the type using TypeRep (available in Data.Typeable). The instance for MuRef, required by the reify package, is shown below.
instance Typeable e => MuRef (Ast e) where
type DeRef (Ast e) = AstNode
mapDeRef f (IntLit a) = pure $ IntLitN a
mapDeRef f (Add a b) = AddN <$> f a <*> f b
mapDeRef f (BoolLit a) = pure $ BoolLitN a
mapDeRef f (IfThenElse a b c :: Ast e) =
IfThenElseN (typeOf (undefined::e)) <$> f a <*> f b <*> f c
Now we can use reifyGraph to recover sharing. However, a lot of type information was lost. Lets try to recover it. I altered your definition of Ast2 slightly:
data Ast2 e where
IntLit2 :: Int -> Ast2 Int
Add2 :: Unique -> Unique -> Ast2 Int
BoolLit2 :: Bool -> Ast2 Bool
IfThenElse2 :: Unique -> Unique -> Unique -> Ast2 e
The graph from the reify package looks like this (where e = AstNode):
data Graph e = Graph [(Unique, e Unique)] Unique
Lets make a new graph data structure where we can store Ast2 Int and Ast2 Bool separately (thus, where the type information has been recovered):
data Graph2 = Graph2 [(Unique, Ast2 Int)] [(Unique, Ast2 Bool)] Unique
deriving Show
Now we only need to find a function from Graph AstNode (the result of reifyGraph) to Graph2:
recoverTypes :: Graph AstNode -> Graph2
recoverTypes (Graph xs x) = Graph2 (catMaybes $ map (f toAst2Int) xs)
(catMaybes $ map (f toAst2Bool) xs) x where
f g (u,an) = do a2 <- g an
return (u,a2)
toAst2Int (IntLitN a) = Just $ IntLit2 a
toAst2Int (AddN a b) = Just $ Add2 a b
toAst2Int (IfThenElseN t a b c) | t == typeOf (undefined :: Int)
= Just $ IfThenElse2 a b c
toAst2Int _ = Nothing
toAst2Bool (BoolLitN a) = Just $ BoolLit2 a
toAst2Bool (IfThenElseN t a b c) | t == typeOf (undefined :: Bool)
= Just $ IfThenElse2 a b c
toAst2Bool _ = Nothing
Lets do an example:
expr = Add (IntLit 42) expr
test = do
graph <- reifyGraph expr
print graph
print $ recoverTypes graph
Prints:
let [(1,AddN 2 1),(2,IntLitN 42)] in 1
Graph2 [(1,Add2 2 1),(2,IntLit2 42)] [] 1
The first line shows us that reifyGraph has correctly recovered sharing. The second line shows us that only Ast2 Int types have been found (which is also correct).
This method is easily adaptable for other specific GADTs, but I don't see how it could be made entirely generic.
The complete code can be found at http://pastebin.com/FwQNMDbs .
Related
I have a very simple continuation function (avoided using Monads for simplicity sake):
data C a = C {unwrap :: (a -> a) -> a}
Essentially, I'm trying to perform different implementations based on input type, something similar to (sudo-code):
data Gadt a where
AString :: String -> Gadt Bool
AInt :: Int -> Gadt Bool
data C a = C {unwrap :: (a -> Gadt a) -> a}
example :: a -> C a
example v = C $ \f -> | (v :: Int) == True = f (AInt v)
| (v :: String) == True = f (AString v)
cont :: Gadt a -> a
cont (AInt v) = ...
cont (AString v) = ...
Am I overlooking a basic solution here? I'm new to continuations so I may of just overlooked something simple.
First you cannot use :: as a predicate to test the runtime type (as far as I know). Second your a parameter of Gadt is entirely phantom. Is that intended? example can either be polymorphic by treating different types uniformly or you'd need to use class (parametric vs ad-hoc polymorphism). You might have wanted something like
data Gadt where
AInt :: Int -> Gadt
AString :: String -> Gadt
data C a = C { unwrap :: (a -> Gadt) -> Gadt }
class Ex a where
example :: a -> C a
instance Ex Int where
example v = C ...
instance Ex String where
example v = C ...
This is still pseudo-code, as it makes little sense but at least type-checks for me.
I've come across a potential solution, following advice from here:
newtype C r a = C {runC :: (a -> r) -> r}
data Hole = Hole1 Int | Hole2 String | Hole3 Bool
example :: String -> C Bool a
example s = C $ \f -> do
x <- f (Hole1 22)
y <- f (Hole2 "string")
k (Hole3 False)
cont :: Hole -> Bool
cont (Hole1 x) = ...
cont (Hole2 x) = ...
cont (Hole3 x) = ...
This enables specific implementations based on type by wrapping the input type inside the Hole data structure.
Let's imagine I have an existential type T
T = ∃X { a :: X, f :: X -> Int}
Of which I produce a value
v :: T
v = pack {Int, { a = 0, f x = 0 } } as T
So :
users of this value are forbidden to know which type X is actually used in the implementation. In order to consume this value, their code has to be polymorphic in X
the implementor, on the other hand, is in full knowledge that X is actually an Int and can use the capacities of the underlying type as he wishes
I would like to know if there are variant of this mechanism which do not destroy evidence :
w, v = pack {Int, { a = 0, f x = 0 } } as T
s = unpack w v -- recovers type information
where w would be a value level proof of the type equation tying X to int. the idea would be to selectively reuse the implementation in another part of the code, and have non polymorphic code. to have the usual existential behaviour, we can just ignore the w returned.
I guess one could cast X to Int and abandon type safety, but that's another story: If I know the secret about v, wouldn't it make sense for me to be able to tell the secret to someone else and have the compiler verify that the secret only get used by code it has been given to.
Has it been tried / what's the most wrong part of this?
Use singletons
-- singleton for some types we are interested in
data S a where
Sint :: S Int
Sbool :: S Bool
-- existential type, with a singleton inside
data T where
T :: S a -> a -> (a -> Int) -> T
-- producer
t :: T
t = T Sint 3 succ
-- consumer
foo :: T -> Int
foo (T Sint n f) = f (n + 10)
foo (T Sbool True f) = 23
foo (T Sbool False f) = f 3
If you need to go full monty, use Typeable.
data T where
T :: Typeable a => a -> (a -> Int) -> T
-- consumer
foo :: T -> Int
foo (T x f) = case cast x of
Just n -> f ((n :: Int) + 10)
Nothing -> 12 -- more casts can be attempted here
Pack up a GADT which gives you a way to learn by pattern-matching what the existentially quantified type was. This is a way to emulate a dependent pair type.
data Ty a where
IntTy :: Ty Int
CharTy :: Ty Char
data T = forall a. T {
ty :: Ty a,
x :: a,
f :: a -> Int
}
consumeT :: T -> Int
consumeT (T IntTy x _) = {-# GHC knows (x :: Int) in this branch #-} x + 3
consumeT (T CharTy x f) = {-# GHC knows (x :: Char) in this branch #-} f x + 3
Given a simple "language":
data Expr a where
ConstE :: a -> Expr a
FMapE :: (b -> a) -> Expr b -> Expr a
instance Functor Expr where
fmap = FMapE
interpret :: Expr a -> a
interpret (ConstE a) = a
interpret (FMapE f a) = f (interpret a)
From that I would like to extract a call graph, eg:
foo = fmap show . fmap (*2) $ ConstE 1
Should result in the graph Node 1 -> Node (*2) -> Node show. Ideally I'd like to store this in a Data.Graph.
What I've come up to this point is that it should be possible to use System.Mem.StableNames to identify individual nodes and store them in a HashMap (StableName (Expr a)) (Expr a).
toHashMap :: Expr a -> HashMap (StableName (Expr a)) (Expr a)
toHashMap n#ConstE = do
sn <- makeStableName n
return $ HashMap.singleton sn n
The problem is, that there seems to be no way to get through the FMapE nodes:
toHashMap n#(FMapE _ a) = do
snN <- makeStableName n
snA <- makeStableName a
-- recurse
hmA <- toHashMap a
-- combine
return $ HashMap.singleton snN n `HashMap.union` hmA
GHC will complain along the lines of this:
Couldn't match type ‘t’ with ‘b’
because type variable ‘b’ would escape its scope
This (rigid, skolem) type variable is bound by
a pattern with constructor
FMapE :: forall a b. (b -> a) -> Expr b -> Expr a,
in an equation for ‘toHashMap’
I can see that this won't match ... but I have no clue on how to make this work.
Edit
This probably boils down to writing a children function:
children :: Event a -> [Event a]
children (ConstE) = []
children (FMapE _ a) = [a] -- doesn't match ...
For the same reason I can't uniplate on this ...
You can get a postorder traversal, which is a tolopogical sort for a tree, of a type of kind * -> * from the Uniplate1 class I've described previously.
{-# LANGUAGE RankNTypes #-}
import Control.Applicative
import Control.Monad.Identity
class Uniplate1 f where
uniplate1 :: Applicative m => f a -> (forall b. f b -> m (f b)) -> m (f a)
descend1 :: (forall b. f b -> f b) -> f a -> f a
descend1 f x = runIdentity $ descendM1 (pure . f) x
descendM1 :: Applicative m => (forall b. f b -> m (f b)) -> f a -> m (f a)
descendM1 f a = uniplate1 a f
transform1 :: Uniplate1 f => (forall b. f b -> f b) -> f a -> f a
transform1 f = f . descend1 (transform1 f)
transform1 is a generic postorder tranformation. A generic postorder Monadic traversal of a Uniplate1 is
transformM1 :: (Uniplate1 f, Applicative m, Monad m) =>
(forall b. f b -> m (f b)) ->
f a -> m (f a)
transformM1 f = (>>= f) . descendM1 (transformM1 f)
We can write a Uniplate1 instance for Expr:
instance Uniplate1 Expr where
uniplate1 e p = case e of
FMapE f a -> FMapE f <$> p a
e -> pure e
We'll make a simple dump function for demonstration purposes and bypass to restore the data after a monadic effect.
dump :: Expr b -> IO ()
dump (ConstE _) = putStrLn "ConstE"
dump (FMapE _ _) = putStrLn "FMapE"
bypass :: Monad m => (a -> m ()) -> a -> m a
bypass f x = f x >> return x
We can traverse your example in topological order
> transformM1 (bypass dump) (fmap show . fmap (*2) $ ConstE 1)
ConstE
FMapE
FMapE
I have defined a simple algebraic (concrete) data type, MyType :
data MyTpe = MyBool Bool | MyInt Int
... and I am trying to find a way to "convert" arbitrary functions (a->b), where a and b are either Bool or Int, into the associated (MyType->MyType) functions.
This does the job, it converts (a->b) into Maybe (MyType->MyType) (see [1] below) :
import Data.Typeable
data MyTpe = MyBool Bool | MyInt Int deriving Show
liftMyType :: (Typeable a, Typeable b) => (a -> b) -> Maybe (MyTpe -> MyTpe)
liftMyType f = case castIntInt f of
Just g -> Just $ liftIntInt g
Nothing ->
case castIntBool f of
Just g -> Just $ liftIntBool g
Nothing ->
case castBoolInt f of
Just g -> Just $ liftBoolInt g
Nothing ->
case castBoolBool f of
Just g -> Just $ liftBoolBool g
Nothing -> Nothing
castIntInt :: (Typeable a, Typeable b) => (a -> b) -> Maybe (Int -> Int)
castIntInt f = cast f :: Maybe (Int -> Int)
castIntBool :: (Typeable a, Typeable b) => (a -> b) -> Maybe (Int -> Bool)
castIntBool f = cast f :: Maybe (Int -> Bool)
castBoolInt :: (Typeable a, Typeable b) => (a -> b) -> Maybe (Bool -> Int)
castBoolInt f = cast f :: Maybe (Bool -> Int)
castBoolBool :: (Typeable a, Typeable b) => (a -> b) -> Maybe (Bool -> Bool)
castBoolBool f = cast f :: Maybe (Bool -> Bool)
liftIntInt :: (Int -> Int) -> (MyTpe -> MyTpe)
liftIntInt f (MyInt x) = MyInt (f x)
liftIntBool :: (Int -> Bool) -> (MyTpe -> MyTpe)
liftIntBool f (MyInt x) = MyBool (f x)
liftBoolInt :: (Bool -> Int) -> (MyTpe -> MyTpe)
liftBoolInt f (MyBool x) = MyInt (f x)
liftBoolBool :: (Bool -> Bool) -> (MyTpe -> MyTpe)
liftBoolBool f (MyBool x) = MyBool (f x)
However that's quite ugly and does not scale well : what if I want to extend MyType that way?
data MyTpe = MyBool Bool | MyInt Int | MyString String
... Or what if I also want to convert (a1 -> a2 -> b), where a1,a2 and b are Bool or Int, into the associated (MyType->MyType->MyType) function?...
My question : is there a simple, more elegant and more Haskell-like way to handle this issue?
[1]: liftIntInt function and the like are not defined over all MyType elements (eg liftIntInt is not defined for (MyBool x) element). The code is just a reduced case example and in real life I handle this properly.
You're looking for a type
goal :: (a -> b) -> (MyType -> MyType)
for some "suitable" choices of a and b. These "suitable" choices are known statically as the definition of MyType is known statically.
What you're looking for is a typeclass. In particular, we'll want the MultiParamTypeClasses pragma
{-# LANGUAGE MultiParamTypeClasses #-}
class MapMyType a b where
liftMyType :: (a -> b) -> (MyType -> MyType)
so now the full type for liftMyType is
liftMyType :: MapMyType a b => (a -> b) -> (MyType -> MyType)
and we can use the typeclass machinery to store the various instantiations of liftMyType having it be usable only and exactly when a and b can be resolved to be types where liftMyType is inhabited.
instance MapMyType Int Int where liftMyType f (MyInt x) = MyInt (f x)
instance MapMyType Int Bool where liftMyType f (MyInt x) = MyBool (f x)
instance MapMyType Bool Int where liftMyType f (MyBool x) = MyInt (f x)
instance MapMyType Bool Bool where liftMyType f (MyBool x) = MyBool (f x)
-- (as a side note, this is a dangerous function to instantiate since it
-- has incomplete pattern matching on its `MyType` typed argument)
Now, it's worth mentioning that MultiParamTypeClasses often damages inference when used like this. In particular, if we're looking at a fragment of code liftMyType a b we have to be able to infer the type of a and b on their own (e.g., without help from hints being passed "down" from the call to liftMyType) otherwise we'll get an ambiguous instance compilation failure. Actually, what makes this especially bad, is that we'll get that compilation failure if either a or b cannot be directly inferred.
In many circumstances, you would want to control this issue using FunctionalDependencies allowing a little more inference to "flow" between the two parameters and making ambiguity errors less common.
But in this case, I'd consider it to be a code smell. While the code above works (with caveat to the commented note) it has the feeling of a fragile solution.
To answer your question: "is there a simple, more elegant and more Haskell-like way to handle this issue?" There is no elegant or Haskell-like way to solve this problem. Haskell is not a dynamically typed language, and while the designers have managed to fake dynamic typing, you really should avoid it. This question makes it seem like you are trying to fix bad design somewhere with dynamic typing.
You can, however, write a simplified version of your code which is also extensible, using generics:
{-# LANGUAGE DeriveGeneric #-}
import GHC.Generics
import Data.Typeable
liftFun :: forall a b c . (Generic c, GLiftFun (Rep c), Typeable a, Typeable b)
=> (a -> b) -> c -> Maybe c
liftFun f x = do
a <- gGet (from x)
b <- gPut (f a)
return (to b)
class GLiftFun f where
gPut :: Typeable a => a -> Maybe (f q)
gGet :: Typeable a => f q -> Maybe a
instance Typeable a => GLiftFun (K1 i a) where
gPut = fmap K1 . cast
gGet = cast . unK1
instance GLiftFun f => GLiftFun (M1 i c f) where
gPut = fmap M1 . gPut
gGet = gGet . unM1
instance (GLiftFun f, GLiftFun g) => GLiftFun (f :+: g) where
gPut a | Just r <- gPut a = Just (L1 r)
| Just r <- gPut a = Just (R1 r)
| otherwise = Nothing
gGet (L1 a) = gGet a
gGet (R1 a) = gGet a
liftFun will work for any type which is a simple sum type, like Either or any type you define which is isomorphic to a series of nested Eithers. It probably has a sensible extension to product types as well. For example any of the following will work:
data MyType = MyBool Bool | MyInt Int deriving (Show, Generic)
data MyType2 = B2 Bool | I2 Int | S2 String deriving (Show, Generic)
type MyType3 = Either String Int
Here's how you could do it in a scalable way:
{-# LANGUAGE DeriveDataTypeable #-}
import Data.Typeable
data MyTpe = MyBool Bool | MyInt Int deriving (Show,Typeable)
d :: (Typeable a, Typeable b) => (a->b) -> Maybe (a -> MyTpe)
d f = case (cast f :: (Typeable a) => Maybe (a->Int)) of
Just f -> Just $ MyInt . f
_ -> case (cast f :: (Typeable a) => Maybe (a->Bool)) of
Just f -> Just $ MyBool . f
_ -> Nothing -- add more constructor matching here
e :: (Typeable a, Typeable b) => a -> Maybe (b->MyTpe) -> Maybe MyTpe
e x = (>>= \f -> fmap ($ x) (cast f :: (Typeable a => Maybe (a->MyTpe))))
liftMyType :: (Typeable a, Typeable b) => (a->b) -> MyTpe -> Maybe MyTpe
liftMyType f (MyInt x) = e x $ d f
liftMyType f (MyBool x) = e x $ d f
-- add more constructor matching here
...
> liftMyType ((+1) :: Int->Int) (MyInt 100)
> Just (MyInt 101)
You could even get the type you wanted - i.e. Maybe (MyTpe->MyTpe) - you don't need to pattern-match on the argument, just then you won't get a total function MyTpe -> MyTpe even if it is Just.
liftMyType = fmap h . d where
h g = case (cast g :: Maybe (Int->MyTpe)) of
Just g -> (\(MyInt x)) -> g x
_ -> case (cast g :: Maybe (Bool->MyTpe)) of
Just g -> (\(MyBool x)) -> g x
_ -> Nothing -- add more type matching here
...
> fmap ($ MyInt 100) $ liftMyType ((+1) :: Int->Int)
> Just (MyInt 101)
How can I lower a Haskell function to an embedded language in as typesafe a manner as possible. In particular, let's assume I have a value type like
data Type t where
Num :: Type Int
Bool :: Type Bool
data Ty = TNum | TBool deriving Eq
data Tagged t = Tagged (Type t) t deriving Typeable
data Dynamic = forall t . Typeable t => Dynamic (Tagged t) deriving Typeable
forget :: Typeable t => Tagged t -> Dynamic
forget = Dynamic
remember :: Typeable b => Dynamic -> Maybe b
remember (Dynamic c) = cast c
and I want to convert a function like (isSucc :: Int -> Int -> Bool) to product of its dynamic form and some type information, like this
data SplitFun = SF { dynamic :: [Dynamic] -> Dynamic
, inputTypes :: [Ty]
, outputType :: Ty
}
such that for some apply function
(\(a:b:_) -> isSucc a b) == apply (makeDynamicFn isSucc)
modulo some possible exceptions that could be thrown if the dynamic types actually don't match. Or, more explicitly, I'd like to find makeDynamicFn :: FunType -> SplitFun. Obviously that isn't a proper Haskell type and there's unlikely to be a way to pull the types from isSucc itself, so it might be something more like
anInt . anInt . retBool $ isSucc :: SplitFun
where anInt and retBool have printf-style types.
Is such a thing possible? Is there a way to simulate it?
To implement a function of type FunType -> SplitFun, we'll use standard type class machinery to deconstruct function types.
Now, implementing this function directly turns out to be fairly hard. To get inputTypes and outputType from the recursive case, you have to apply your function; but you can only apply the function inside the dynamic field, which gives you no way to fill the other fields. Instead, we'll split the task into two: one function will give us the Ty information, other will construct the [Dynamic] -> Dynamic function.
data Proxy a = Proxy
class Split r where
dynFun :: r -> [Dynamic] -> Dynamic
tyInfo :: Proxy r -> ([Ty], Ty)
split :: r -> SplitFun
split f = let (i, o) = tyInfo (Proxy :: Proxy r)
in SF (dynFun f) i o
Now, tyInfo doesn't actually need the function, we use Proxy just to pass the type information without needing to use undefined all over the place. Note that we need ScopedTypeVariables to be able to refer to the type variable r from instance declaration. Clever use of asTypeOf might also work.
We have two base cases: Bool and Int:
instance Split Int where
dynFun i _ = forget (Tagged Num i)
tyInfo _ = ([], TNum)
instance Split Bool where
dynFun b _ = forget (Tagged Bool b)
tyInfo _ = ([], TBool)
There are no input types and since we already have a value, we do not need to ask for more Dynamic values and we simply return Dynamic of that particular value.
Next, we have two recursive cases: Bool -> r and Int -> r
instance (Split r) => Split (Int -> r) where
dynFun f (d:ds) = case remember d :: Maybe (Tagged Int) of
Just (Tagged _ i) -> dynFun (f i) ds
Nothing -> error "dynFun: wrong dynamic type"
dynFun f [] = error "dynFun: not enough arguments"
tyInfo _ = case tyInfo (Proxy :: Proxy r) of
(i, o) -> (TNum:i, o)
instance (Split r) => Split (Bool -> r) where
dynFun f (d:ds) = case remember d :: Maybe (Tagged Bool) of
Just (Tagged _ b) -> dynFun (f b) ds
Nothing -> error "dynFun: wrong dynamic type"
dynFun f [] = error "dynFun: not enough arguments"
tyInfo _ = case tyInfo (Proxy :: Proxy r) of
(i, o) -> (TBool:i, o)
These two need FlexibleInstances. dynFun examines the first Dynamic argument and if it's okay, we can safely apply the function f to it and continue from there. We could also make dynFun :: r -> [Dynamic] -> Maybe Dynamic, but that's fairly trivial change.
Now, there's some duplication going on. We could introduce another class, such as:
class Concrete r where
getTy :: Proxy r -> Ty
getType :: Proxy r -> Type r
And then write:
instance (Typeable r, Concrete r) => Split r where
dynFun r _ = forget (Tagged (getType (Proxy :: Proxy r)) r)
tyInfo _ = ([], getTy (Proxy :: Proxy r))
instance (Typeable r, Concrete r, Split s) => Split (r -> s) where
dynFun f (d:ds) = case remember d :: Maybe (Tagged r) of
Just (Tagged _ v) -> dynFun (f v) ds
-- ...
tyInfo _ = case tyInfo (Proxy :: Proxy s) of
(i, o) -> (getTy (Proxy :: Proxy r):i, o)
But this needs both OverlappingInstances and UndecidableInstances.