Search code examples
haskelltypespolymorphismghc

Does Haskell support closed polymorphic types?


Given:

newtype PlayerHandle = PlayerHandle Int deriving (Show)
newtype MinionHandle = MinionHandle Int deriving (Show)
newtype WeaponHandle = WeaponHandle Int deriving (Show)

In the following code, I would like handle to be exactly one of three types: PlayerHandle, MinionHandle, and WeaponHandle. Is this possible to do in Haskell?

data Effect where
    WithEach :: (??? handle) => [handle] -> (handle -> Effect) -> Effect -- Want `handle' to be under closed set of types.

The following is too tedious:

data Effect' where
    WithEachPlayer :: [PlayerHandle] -> (PlayerHandle -> Effect) -> Effect
    WithEachMinion :: [MinionHandle] -> (MinionHandle -> Effect) -> Effect
    WithEachWeapon :: [WeaponHandle] -> (WeaponHandle -> Effect) -> Effect

EDIT:

Ørjan Johansen has proposed using closed type families, which indeed gets me a step closer to what I want. The issue I'm having using them is that I can't seem to write the following:

type family IsHandle h :: Constraint where
    IsHandle (PlayerHandle) = ()
    IsHandle (MinionHandle) = ()
    IsHandle (WeaponHandle) = ()

data Effect where
    WithEach :: (IsHandle handle) => [handle] -> (handle -> Effect) -> Effect

enactEffect :: Effect -> IO ()
enactEffect (WithEach handles cont) = forM_ handles $ \handle -> do
    print handle  -- Eeek! Can't deduce Show, despite all cases being instances of Show.
    enactEffect $ cont handle

Here GHC complains that it cannot deduce that the handle is an instance of Show. I am hesitant to solve this by moving the Show constraint in the WithEach constructor for various reasons. These include modularity and scalability. Would something like a closed data family solve this (as I know type family mappings are not injective... Is that the problem even with closed ones?)


Solution

  • Thanks for all the solutions guys. They all are helpful for various use cases. For my use case, it turned out that making the handle types into a single GADT solved my problem.

    Here's my derived solution for those interested:

    {-# LANGUAGE FlexibleInstances #-}
    {-# LANGUAGE GADTs #-}
    {-# LANGUAGE LambdaCase #-}
    
    data Player
    data Minion
    data Weapon
    
    data Handle a where
        PlayerHandle :: Int -> Handle Player
        MinionHandle :: Int -> Handle Minion
        WeaponHandle :: Int -> Handle Weapon
    
    data Effect where
        WithEach :: [Handle h] -> (Handle h -> Effect) -> Effect
        PrintSecret :: Handle h -> Effect
    
    -------------------------------------------------------------------------------
    -- Pretend the below code is a separate file that imports the above data types
    -------------------------------------------------------------------------------
    
    class ObtainSecret a where
        obtainSecret :: a -> String
    
    instance ObtainSecret (Handle a) where
        obtainSecret = \case
            PlayerHandle n -> "Player" ++ show n
            MinionHandle n -> "Minion" ++ show n
            WeaponHandle n -> "Weapon" ++ show n
    
    enactEffect :: Effect -> IO ()
    enactEffect = \case
        WithEach handles continuation -> mapM_ (enactEffect . continuation) handles
        PrintSecret handle -> putStrLn (obtainSecret handle)
    
    createEffect :: [Handle h] -> Effect
    createEffect handles = WithEach handles PrintSecret
    
    main :: IO ()
    main = do
        enactEffect $ createEffect $ map PlayerHandle [0..2]
        enactEffect $ createEffect $ map MinionHandle [3..5]
        enactEffect $ createEffect $ map WeaponHandle [6..9]