【问题标题】:Does Haskell support closed polymorphic types?Haskell 是否支持封闭的多态类型?
【发布时间】:2015-08-04 03:36:29
【问题描述】:

给定:

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

在下面的代码中,我希望handle完全三种类型之一:PlayerHandleMinionHandleWeaponHandle。这可以在 Haskell 中实现吗?

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

下面的太繁琐了:

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

编辑:

Ørjan Johansen 提议使用封闭类型族,这确实让我离我想要的更近了一步。我在使用它们时遇到的问题是我似乎无法编写以下内容:

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

GHC 在这里抱怨它不能推断出句柄是Show 的一个实例。由于各种原因,我很犹豫通过在WithEach 构造函数中移动Show 约束来解决这个问题。这些包括模块化和可扩展性。像封闭数据族这样的东西会解决这个问题吗(我知道类型族映射不是单射的......即使是封闭的也是这个问题吗?)

【问题讨论】:

  • 我觉得这很有趣,你已经得到了我的支持,但我希望你不介意这个问题:为什么不为你的处理程序使用 sum-type - 我相信你有你的理由但是这里的例子似乎对这个基本的解决方案大喊大叫。
  • @Carsten:主要是因为我希望有更好的方法。我以前从未使用过封闭类型系列(我忘记了 GHC 已经支持它们)。在这一点上,我想我可能只使用三个不同的构造函数,当模式匹配它们时,我可以将它们直接传递给enactEffect :: (Show h) => [h] -> (h -> Effect) -> IO ()。这将使我能够处理比Show 更复杂的约束(假设ShowWithEach 构造函数的先决条件),包括模块私有约束。
  • 只要你不想扩展到其他处理程序 IMO 没有 更好 方式this 似乎更容易(对我来说;))
  • @Carsten:我明白你在说什么。我真的很想强制执行静态类型。我将它用于我的 DSL 炉石模型(效果和能力)。这将排除 sum 类型允许的非法卡片结构。
  • @Thomas Eding:你不能隐藏 sum 类型本身,以排除非法卡片结构吗?

标签: haskell types polymorphism ghc


【解决方案1】:

我认为您可以使用 closed constraint 类型族获得准确的语法:

{-# LANGUAGE TypeFamilies, ConstraintKinds, GADTs #-}

import GHC.Exts (Constraint)

newtype PlayerHandle = PlayerHandle Int
newtype MinionHandle = MinionHandle Int
newtype WeaponHandle = WeaponHandle Int

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

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

编辑:另一个尝试,包括Show

{-# LANGUAGE TypeFamilies, ConstraintKinds, GADTs,
             UndecidableInstances #-}

import GHC.Exts (Constraint)
import Control.Monad (forM_)

newtype PlayerHandle = PlayerHandle Int
newtype MinionHandle = MinionHandle Int
newtype WeaponHandle = WeaponHandle Int

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

type IsHandle h = (IsHandle' h, Show h)

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

-- Assume my each (IsHandle a) already is an instance of a class I want to use, such as (Show).
enactEffect :: Effect -> IO ()
enactEffect (WithEach handles cont) = forM_ handles $ \handle -> do
    print handle  -- (*)
    enactEffect $ cont handle

我不太明白如何避免使用两个不同的类、类型或系列,并获得您似乎想要的 API,而无需在另一个模块中添加其他类型。我也不知道生成的IsHandle 约束有什么方法可以自动继承这三种类型共有的所有类,而无需您在某处列出它们。

但我认为根据您的需求/风格,还有一些类似于我上一个的选项:

  • 您可以将IsHandle 设为一个类,并将IsHandle'Show 等作为超类。
  • 您可以将IsHandle' 设为一个类,在这种情况下,防止添加更多类型的唯一方法是不导出IsHandle'

最后一个的一个优点是它可以大大减少为此所需的扩展数量:

{-# LANGUAGE GADTs, ConstraintKinds #-}

class IsHandle' h
instance IsHandle' (PlayerHandle)
instance IsHandle' (MinionHandle)
instance IsHandle' (WeaponHandle)

type IsHandle h = (IsHandle' h, Show h)

【讨论】:

    【解决方案2】:

    这是一个基于 GADT 的解决方案:

    {-# LANGUAGE GADTs, RankNTypes #-}
    {-# OPTIONS -Wall #-}
    module GADThandle where
    
    import Control.Monad
    
    newtype PlayerHandle = PlayerHandle Int deriving (Show)
    newtype MinionHandle = MinionHandle Int deriving (Show)
    newtype WeaponHandle = WeaponHandle Int deriving (Show)
    
    data HandleW a where
       WPlayer :: HandleW PlayerHandle
       WMinion :: HandleW MinionHandle
       WWeapon :: HandleW WeaponHandle
    
    handlewShow :: HandleW a -> (Show a => b) -> b
    handlewShow WPlayer x = x
    handlewShow WMinion x = x
    handlewShow WWeapon x = x
    
    data Effect where
       WithEach :: HandleW handle -> [handle] -> (handle -> Effect) -> Effect 
    
    enactEffect :: Effect -> IO ()
    enactEffect (WithEach handlew handles cont) = handlewShow handlew $ 
       forM_ handles $ \handle -> do
          print handle
          enactEffect $ cont handle
    

    这里的想法是使用类型见证HandleW a,证明a 是您的三种类型之一。然后,“引理”handlewShow 证明如果HandleW a 成立,那么a 必须是Show-able 类型。

    也可以将上面的代码推广到任意类型的类。下面的引理证明,如果您的三种类型T 中的每一种都有c T,并且您知道HandleW a 成立,那么c a 也必须成立。你可以通过选择c = Show来获得之前的引理。

    handlewC :: (c PlayerHandle, c MinionHandle, c WeaponHandle) => 
       HandleW a -> Proxy c -> (c a => b) -> b
    handlewC WPlayer Proxy x = x
    handlewC WMinion Proxy x = x
    handlewC WWeapon Proxy x = x
    
    enactEffect' :: Effect -> IO ()
    enactEffect' (WithEach handlew handles cont) = handlewC handlew (Proxy :: Proxy Show) $ 
       forM_ handles $ \handle -> do
          print handle
          enactEffect' $ cont handle
    

    【讨论】:

    • 如果添加class Handle a where { handlew :: p a -> HandleW a } 和实例instance Handle PlayerHandle where { handlew _ = WPlayer } 等,您可以摆脱手动传递HandleW 见证并完全匹配所需的语法。
    • 看起来很有希望。有机会我会试试看的。
    【解决方案3】:

    为您的Handle 类型添加一个类型参数,并使用DataKinds 将其值限制为仅三个之一,因此:

    {-# LANGUAGE DataKinds      #-}
    {-# LANGUAGE KindSignatures #-}
    {-# LANGUAGE GADTs          #-}
    
    import Control.Monad
    
    data Entity = Player | Minion | Weapon
    
    newtype Handle (e :: Entity) = Handle Int
        deriving (Eq, Ord, Read, Show)
    
    data Effect where
        WithEach :: [Handle e] -> (Handle e -> Effect) -> Effect
    
    enactEffect :: Effect -> IO ()
    enactEffect (WithEach handles cont) = forM_ handles $ \handle -> do
        print handle
        enactEffect $ cont handle
    

    【讨论】:

    • 这正是我所需要的。我不需要使用 DataKinds(我可以使用类型来代替),但最终结果是一样的。
    • @ThomasEding 我猜“我可以改用类型”,你的意思是“我可以写data Player; data Minion; data Weapon; newtype Handle e = Handle Int”。注意两者之间的区别:与您的 Handle :: * -> *,因此可以应用于任何愚蠢的旧类型,如 Handle ()Handle Bool;与我的 Handle :: Entity -> *,因此类型检查器将强制 Handle 应用于您关心的三种类型之一。
    • 我还为每个句柄变体创建了三个不同的单态构造函数。我已经在我的问题中发布了我的派生解决方案。
    • @ThomasEding 标准做法是将解决方案作为答案,而不是作为问题。编写并接受您自己问题的答案被认为是非常好的。
    • 啊,好吧。当 SO 允许我这样做时(从现在开始的几个小时),我会接受我的回答。
    【解决方案4】:

    除非您想对类型做一些复杂的事情,否则我会使用 class 的简单解决方案:

    {-# LANGUAGE GADTs #-}
    
    import Control.Monad
    
    newtype PlayerHandle = PlayerHandle Int deriving (Show)
    newtype MinionHandle = MinionHandle Int deriving (Show)
    newtype WeaponHandle = WeaponHandle Int deriving (Show)
    
    class (Show h) => Handle h
    instance Handle PlayerHandle
    instance Handle MinionHandle
    instance Handle WeaponHandle
    
    data Effect where
        WithEach :: (Handle handle) => [handle] -> (handle -> Effect) -> Effect
    
    enactEffect :: Effect -> IO ()
    enactEffect (WithEach handles cont) = forM_ handles $ \handle -> do
        print handle
        enactEffect $ cont handle
    

    【讨论】:

    • 这种方法的问题是我不知道在定义Handle 类时需要添加到Handle 类的所有类约束。理想情况下,Handle 的消费者将能够创建 Player/Minion/WeaponHandle 将全部实例化并与通用 Handle 一起使用的新类。
    【解决方案5】:

    我会使用 GADT:

    {-# LANGUAGE KindSignatures, GADTs, RankNTypes, DataKinds #-}
    
    data K = Player | Minion | Weapon
      deriving (Eq, Show)
    
    newtype PlayerHandle = PlayerHandle Int deriving (Show)
    newtype MinionHandle = MinionHandle Int deriving (Show)
    newtype WeaponHandle = WeaponHandle Int deriving (Show)
    
    -- Plain ADT might be enough
    -- see below
    data Handle (k :: K) where
      PlayerHandle' :: PlayerHandle -> Handle Player
      MinionHandle' :: MinionHandle -> Handle Minion
      WeaponHandle' :: WeaponHandle -> Handle Weapon
    
    data SomeHandle where
      SomeHandle :: Handle k -> SomeHandle
    
    data Effect where
      WithEach :: (SomeHandle -> IO ()) -> Effect
    
    printEffect :: Effect
    printEffect = WithEach f
      where f (SomeHandle h) = g h
            g :: Handle k -> IO ()
            g (PlayerHandle' p) = putStrLn $ "player :" ++ show p
            g (MinionHandle' p) = putStrLn $ "minion :" ++ show p
            g (WeaponHandle' p) = putStrLn $ "weapon :" ++ show p
    
    -- GADTs are useful, if you want to have maps preserving handle kind:
    data HandleMap where
      -- HandleMap have to handle all `k`, yet cannot change it!
      HandleMap :: (forall k. Handle k -> Handle k) -> HandleMap
    
    zeroWeaponHandle :: HandleMap
    zeroWeaponHandle = HandleMap f
      where f :: forall k. Handle k -> Handle k
            f (PlayerHandle' h) = PlayerHandle' h
            f (MinionHandle' h) = MinionHandle' h
            f (WeaponHandle' _) = WeaponHandle' $ WeaponHandle 0
    

    【讨论】:

    • 看起来很有希望。有机会我会试试看的。
    【解决方案6】:

    感谢所有解决方案的家伙。它们都有助于各种用例。对于我的用例,事实证明将句柄类型变成单个 GADT 解决了我的问题。

    这是我为感兴趣的人提供的衍生解决方案:

    {-# 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]
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-10-26
      • 2020-01-07
      • 2018-01-30
      • 2018-03-03
      • 2019-04-24
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多