Search code examples
haskellpattern-matchingexistential-type

Deconstructing an existential type


I am using an existential type as a wrapper. At a point in my code where I know the enclosed type, I want to do something with it that is specific to the enclosed type. This is the closest I can get:

 {-# LANGUAGE ExistentialQuantification #-}

class Agent a where
  agentId :: a -> String
  speciesId :: a -> String
  -- plus other functions that all agents support

-- | A wrapper allowing my daemon to read and write agents of any species.
--   (Agents are stored in files that contain a tag so I know which function
--   to call to read the agent.)
data AgentBox = forall a. Agent a => AgentBox { unbox :: a }

instance Agent AgentBox where
  agentId (AgentBox a) = agentId a
  speciesId (AgentBox a) = speciesId a
  -- plus other functions that all agents support

bugTag :: String
bugTag = "Bug"

data Bug = Bug String

instance Agent Bug where
  agentId (Bug name) = name
  speciesId _ = bugTag

doSomethingWith :: AgentBox -> IO ()
doSomethingWith a = do
  if speciesId a == bugTag
    then do
      -- Now I know it's a bug, and I want to do something bug-specific
      doBugStuff2 a
      return ()
    else return ()

doBugStuff :: Bug -> IO ()
doBugStuff a = putStrLn $ agentId a ++ " does bug stuff"

doBugStuff2 AgentBox{unbox=a} = doBugStuff (a `asTypeOf` model) -- line 39
  where model = undefined :: Bug

The error I get is:

Amy30.hs:39:45:
    Could not deduce (a ~ Bug)
    from the context (Agent a)
      bound by a pattern with constructor
                 AgentBox :: forall a. Agent a => a -> AgentBox,
               in an equation for `doBugStuff2'
      at Amy30.hs:39:13-29
      `a' is a rigid type variable bound by
          a pattern with constructor
            AgentBox :: forall a. Agent a => a -> AgentBox,
          in an equation for `doBugStuff2'
          at Amy30.hs:39:13
    In the first argument of `asTypeOf', namely `a'
    In the first argument of `doBugStuff', namely
      `(a `asTypeOf` model)'
    In the expression: doBugStuff (a `asTypeOf` model)
Failed, modules loaded: none.

How can I accomplish this? Thank you in advance for any suggestions.


Solution

  • Use Data.Dynamic.

    import Data.Dynamic
    
    class Typeable a => Agent a where
      agentId :: a -> String
      -- no need for speciesId
    
    fromAgentBox :: Agent a => AgentBox -> Maybe a
    fromAgentBox (AgentBox inner) = fromDynamic (toDyn inner)
    
    instance Agent Bug where
      agentId (Bug name) = name
      -- no need for speciesId
    
    doSomethingWith :: AgentBox -> IO ()
    doSomethingWith a = do
      case fromAgentBox a of
        Just bug -> do
          -- Now the compiler knows it's a bug, and I can do something bug-specific
          doBugStuff2 bug
          return ()
        Nothing -> return ()
    

    Alternatively, consider declaring doSomethingWith in the Agent class, perhaps with a default definition.

    class Agent a where
      agentId :: a -> String
      -- still don't need speciesId
      doSomethingWith :: a -> IO ()
      doSomethingWith _ = return ()
    
    instance Agent Bug where
      agentId (Bug name) = name
      -- still don't need speciesId
      doSomethingWith bug = do
        -- Now the compiler knows it's a bug, and I can do something bug-specific
        doBugStuff2 bug
        return ()
    

    Finally, I should point out that your AgentBox type is an example of the existential typeclass anti-pattern, so you should perhaps ignore what I've written above and redesign your Agent class as an ordinary datatype.