8

我正在尝试在 Haskell 中实现某种消息解析器,所以我决定将类型用于消息类型,而不是构造函数:

data DebugMsg  = DebugMsg String
data UpdateMsg = UpdateMsg [String]

.. 等等。我相信它对我更有用,因为我可以定义类型类,例如,Msg为带有与该消息相关的所有信息/解析器/操作的消息。但是我这里有问题。当我尝试使用以下方法编写解析函数时case

parseMsg :: (Msg a) => Int -> Get a
parseMsg code = 
    case code of
        1 -> (parse :: Get DebugMsg)
        2 -> (parse :: Get UpdateMsg)

..所有分支的案例结果类型应该相同。有什么解决办法吗?甚至可能只为函数结果指定类型类并期望它是完全多态的?

4

2 回答 2

9

可以用存在类型来完成这样的事情,但是它不会按照你想要的方式工作,所以你真的不应该这样做。

正如您在示例中所做的那样,使用正常的多态性根本行不通。你的类型说的是该函数对所有人都 a有效——也就是说,调用者可以选择接收什么样的消息。但是,您必须根据数字代码选择消息,所以这显然是行不通的。

澄清一下:默认情况下,所有标准 Haskell 类型变量都是通用量化的。您可以将类型签名读取为∀a. Msg a => Int -> Get a. 这说明该函数是为 的每个值定义的a,而不管参数可能是什么。这意味着它必须能够返回a调用者想要的任何特定内容,而不管它得到什么参数。

你真正想要的是类似∃a. Msg a => Int -> Get a. 这就是为什么我说你可以用存在类型来做到这一点。然而,这在 Haskell 中相对复杂(你不能完全写出这样的类型签名)并且实际上并不能正确解决你的问题;这只是未来要记住的事情。

从根本上说,在 Haskell 中使用这样的类和类型并不是很习惯,因为这不是类的本意。您最好为您的消息坚持使用正常的代数数据类型。

我会有一个像这样的类型:

data Message = DebugMsg String
             | UpdateMsg [String]

因此,不必为parse每种类型设置一个函数,只需在parseMsg函数中进行适当的解析:

parseMsg :: Int -> String -> Message
parseMsg n msg = case n of
  1 -> DebugMsg msg
  2 -> UpdateMsg [msg]

(显然,填写您实际拥有的任何逻辑。)

本质上,这是正常代数数据类型的经典用途。没有理由为不同类型的消息使用不同的类型,如果它们具有相同的类型,生活会容易得多。

看起来您正在尝试模仿其他语言的子类型。根据经验,您使用代数数据类型来代替其他语言中大多数子类型的使用。这当然是其中一种情况。

于 2013-01-31T03:17:47.507 回答
9

是的,所有子案例的所有右侧都必须具有完全相同的类型;并且此类型必须与整个case表达式的类型相同。这是一个特点;语言需要能够在编译时保证在运行时不会出现任何类型错误。

关于您的问题的一些评论提到最简单的解决方案是使用 sum (又名变体)类型:

data ParserMsg = DebugMsg String | UpdateMsg [String]

这样做的结果是提前定义了一组替代结果。这有时是有利的(您的代码可以确定没有未处理的子案例),有时是不利的(子案例的数量是有限的,它们是在编译时确定的)。

在某些情况下,一种更高级的解决方案(您可能不需要,但我将把它扔进去)是重构代码以将函数用作数据。这个想法是你创建一个数据类型,它有函数(或单子动作)作为它的字段,然后不同的行为 = 不同的函数作为记录字段。

将这两种样式与此示例进行比较。首先,将不同的情况指定为总和(这使用 GADT,但应该足够简单易懂):

{-# LANGUAGE GADTs #-}

import Data.Vector (Vector, (!))
import qualified Data.Vector as V

type Size = Int    
type Index = Int

-- | A 'Frame' translates between a set of values and consecutive array 
-- indexes.  (Note: this simplified implementation doesn't handle duplicate
-- values.)
data Frame p where 
    -- | A 'SimpleFrame' is backed by just a 'Vector'
    SimpleFrame  :: Vector p -> Frame p
    -- | A 'ProductFrame' is a pair of 'Frame's.
    ProductFrame :: Frame p -> Frame q -> Frame (p, q)

getSize :: Frame p -> Size
getSize (SimpleFrame v) = V.length v
getSize (ProductFrame f g) = getSize f * getSize g

getIndex :: Frame p -> Index -> p
getIndex (SimpleFrame v) i = v!i
getIndex (ProductFrame f g) ij = 
    let (i, j) = splitIndex (getSize f, getSize g) ij
    in (getIndex f i, getIndex g j)

pointIndex :: Eq p => Frame p -> p -> Maybe Index
pointIndex (SimpleFrame v) p = V.elemIndex v p
pointIndex (ProductFrame f g) (p, q) = 
    joinIndexes (getSize f, getSize g) (pointIndex f p) (pointIndex g q)

joinIndexes :: (Size, Size) -> Index -> Index -> Index
joinIndexes (_, rsize) i j = i * rsize + j

splitIndex :: (Size, Size) -> Index -> (Index, Index)
splitIndex (_, rsize) ij = (ij `div` rsize, ij `mod` rsize)

在第一个示例中, aFrame只能是 aSimpleFrame或 a ,并且必须定义ProductFrame每个函数来处理这两种情况。Frame

其次,具有函数成员的数据类型(我省略了两个示例共有的代码):

data Frame p = Frame { getSize    :: Size
                     , getIndex   :: Index -> p
                     , pointIndex :: p -> Maybe Index }

simpleFrame :: Eq p => Vector p -> Frame p
simpleFrame v = Frame (V.length v) (v!) (V.elemIndex v)

productFrame :: Frame p -> Frame q -> Frame (p, q)
productFrame f g = Frame newSize getI pointI
    where newSize = getSize f * getSize g
          getI ij = let (i, j) = splitIndex (getSize f, getSize g) ij 
                    in (getIndex f i, getIndex g j)
          pointI (p, q) = joinIndexes (getSize f, getSize g) 
                                      (pointIndex f p) 
                                      (pointIndex g q)

在这里,Frame类型将getIndexpointIndex操作作为Frame自身的数据成员。没有固定的编译时子案例集,因为 a 的行为Frame由它的元素函数决定,这些元素函数是在运行时提供的。因此,无需触及这些定义,我们可以添加:

import Control.Applicative ((<|>))

concatFrame :: Frame p -> Frame p -> Frame p
concatFrame f g = Frame newSize getI pointI
    where newSize = getSize f + getSize g
          getI ij | ij < getSize f = ij
                  | otherwise      = ij - getSize f
          pointI p = getPoint f p <|> fmap (+(getSize f)) (getPoint g p)

我称这第二种风格为“行为类型”,但这真的只是我自己。

请注意,GHC 中的类型类的实现与此类似——传递了一个隐藏的“字典”参数,并且该字典是一个记录,其成员是类方法的实现:

data ShowDictionary a { primitiveShow :: a -> String }

stringShowDictionary :: ShowDictionary String
stringShowDictionary = ShowDictionary { primitiveShow = ... }

-- show "whatever"
-- ---> primitiveShow stringShowDictionary "whatever"
于 2013-01-31T06:00:50.313 回答