【问题标题】:Polymorphic return type多态返回类型
【发布时间】:2013-08-20 18:56:17
【问题描述】:

函数从字节流中读取 ID。它知道 id 的大小 - 可以是 4 或 8 个字节。如何使返回类型多态?

(伪代码:)

    class (Integral a) => IdSize a where
      size :: a -> Int

    instance IdSize Int32 ...

    instance IdSize Int64 ...

    data Data = Data (Map (IdSize a) String)
    readData :: Data (Map (IdSize a) String)
    readId :: (forall a. IdSize a) => a -- kind of this, but not right

这个 readId 需要来自调用者的 IdSize 实例,但调用者不知道大小。同样,readData 返回的 Map 需要是多态的,但是调用者并不知道实际的类型。使用 Map 的函数将知道该类型。

【问题讨论】:

  • 作为一个稻草人,在您声称需要“多态”的任何地方都使用Either Int32 Int64 怎么样?
  • 当然是多态的。在 Java 中,我可以声明一个返回类型 Object 并返回任何子类。在这里,我想表达一个类似的想法。调用者不会关心返回了什么,直到它把返回的值传递给一个可以计算出要做什么的函数。存在量化可以让我走上这条道路,但我需要进一步研究和实验,看看下一个问题是什么。 :)

标签: haskell parametric-polymorphism


【解决方案1】:

如果某事物只能有两种类型,那么它就不是“多态的”。这只是两种类型的不相交并集,或“和”类型。

import Data.Int

data Idx = I32 Int32
         | I64 Int64
  deriving (Show)

readId 4 _ = I32 0x12345678
readId _ _ = I64 0x1234567812345678

idSize (I32 _) = 4
idSize _       = 8

main :: IO ()
main = do
  let input = () -- this would be your input stream
  let idx1 = readId 4 input
  let idx2 = readId 8 input
  putStrLn $ "idx 1: size " ++ (show $ idSize idx1) ++ " value: " ++ (show idx1)
  putStrLn $ "idx 2: size " ++ (show $ idSize idx2) ++ " value: " ++ (show idx2)
  return ()

当您确实需要数据类型的类型签名具有更大的灵活性时,例如当您构建一个抽象语法树并希望将其限制为类型良好的结构时,GADT 是一种不错的方法:http://en.wikibooks.org/wiki/Haskell/GADT

以下是 GADT 的示例:

{-# LANGUAGE GADTs              #-}
{-# LANGUAGE StandaloneDeriving #-}

import           Data.Int

data Idx a where
  I32 :: Int32 -> Idx Int32
  I64 :: Int64 -> Idx Int64

deriving instance Show (Idx a)

readId32 :: t -> Idx Int32
readId32 _ = I32 0x12345678

readId64 :: t -> Idx Int64
readId64 _ = I64 0x1234567812345678

idSize :: Num a => Idx t -> a
idSize (I32 _) = 4
idSize _       = 8

main :: IO ()
main = do
  let idx1 = readId32 ()
  let idx2 = readId64 ()
  putStrLn $ "idx 1: size " ++ (show $ idSize idx1) ++ " value: " ++ (show idx1)
  putStrLn $ "idx 2: size " ++ (show $ idSize idx2) ++ " value: " ++ (show idx2)
  return ()

我不确定这是否正是您所追求的,但它确实可以让您专门化类型,这样您就不能将Idx Int32s 与Idx Int64s 混合,但您仍然可以编写多态@987654326 @ 函数,如idSize

【讨论】:

  • 是的,使用 union 是对当前问题的简单回答。我希望找到一个可以更好地约束地图的表达式。 Map (Either Int32 Int64) String 允许将 Int32 和 Int64 混合作为键。最好以这样一种方式约束它,即只使用其中一种类型作为键。 (而且它不是唯一取决于 id 大小的东西,所以现在我将使用 Either,最好把它限制得更紧)
  • 我更新了一个不允许混合 Idx Int32 和 Idx Int64 的 GADT 版本。
  • 感谢您的想法。看来我毕竟可能需要存在量化。有没有办法像你的情况一样用定义的 Idx 编写 mkH ?模式是这样的:调用者使用 mkH :: Int -> H 来构造一个 H;然后可以使用 use :: (a->b) -> H -> b 来使用它。下面看我的最新回答(不知道怎么把代码贴在评论里)
  • @SassaNF 我仍然认为你拔出太大的枪来射击这只鸟。如果您想要每种类型的地图,只需使用(Map Int32 String, Map Int64 String);或者,如果您的解码函数肯定会解码所有大 ID 或所有小 ID,则只需使用 Either (Map Int32 String) (Map Int64 String)。我认为,当只涉及两种类型时,使用非常简单的老式类型可以获得很长的路要走。
  • @DanielWagner 我试图想象使用带有任一类型键的映射的函数的用户代码是什么样的。你的意思是老式的模式匹配 f(Left m) = lookup readInt32 m; f (Right m) = 查找 readInt64 m?我认为 readInt* 和 lookup 之间的路径越长,用户代码就越难组织和维护两段相同的代码。
【解决方案2】:

啊,好的,我们可以使用额外的包装类型来解决其中的一些问题:

        {-# LANGUAGE RankNTypes, ConstraintKinds, ExistentialQuantification #-}

        import Data.Int

        data Idx = forall a. IdSize a => Idx a

        instance Show Idx where
          show (Idx a) = show a

        class (Integral a, Show a) => IdSize a where
          size :: a -> Int

        instance IdSize Int32 where
          size _ = 4

        instance IdSize Int64 where
          size _ = 8

        readId :: Int -> Idx
        readId 4 = Idx (4 :: Int32)
        readId _ = Idx (8 :: Int64)

        main = print $ readId 8

那么 Data 可能会保存一个 Map Id 字符串。

【讨论】:

  • 请注意,这在语义上与从readId 直接返回大小为Int 相同,因为这是您能够对Idx 执行的唯一操作。
  • @is7s 当然,我尝试了各种方法后忘记了它们。
  • @shang 这对调用者来说是正确的——调用者只能做一件事。诀窍在于能够使用 Idx 特定实例的其他函数。有关示例模式,请参见我稍后的答案。 mkH 的调用者不能对 IdSize a 做任何事情——从这个意义上说,调用者在函数 mkH 的结果类型中是多态的——但是调用者可以构造一个函数,该函数可以使用 IdSize a 的实例来访问同一个 H 中的映射从中获取 IdSize a。此外, readId 仅在此示例中如此愚蠢;在实际应用中,它是 readId :: M a 用于特定的 monad M。
【解决方案3】:

以下内容可以满足我的要求:

    {-# LANGUAGE RankNTypes, ExistentialQuantification #-}

    import Data.Int
    import Data.Typeable
    import qualified Data.Map as M

    data H = forall a. IdSize a => H a (M.Map a String)

    class (Integral a, Show a, Typeable a) => IdSize a where
      size :: a -> Int
      readId :: a

    instance IdSize Int32 where
      size _ = 4
      readId = 4

    instance IdSize Int64 where
      size _ = 8
      readId = 8

    use :: (forall a. IdSize a => a -> M.Map a String -> b) -> H -> b
    use f (H i m) = f i m

    idSize :: H -> Int
    idSize (H i _) = size i

    mkH :: Int -> H
    mkH 4 = H (4 :: Int32) (M.singleton (4 :: Int32) "int32")
    mkH _ = H (8 :: Int64) (M.singleton (8 :: Int64) "int64")

    main = print $ use (M.lookup . const readId) $ mkH 4

mkH 可用于构造一个对调用者不透明的 H。然后调用者可以传递一个要使用的函数,该函数将解构 H 并调用给定的函数。该函数必须是 RankN 多态的 - 它应该适用于任何 IdSize 实例。这就是隐藏IdSize实现的设计意图。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-11-15
    • 1970-01-01
    • 1970-01-01
    • 2021-04-13
    • 1970-01-01
    相关资源
    最近更新 更多