案例表达式中的不同类型导致Haskell

时间:2013-01-31 03:08:56

标签: haskell types case

我正在尝试在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)

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

2 个答案:

答案 0 :(得分:9)

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

使用正常的多态性,就像在您的示例中一样,根本不起作用。你的类型所说的是该函数对 all 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]

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

基本上,这是正常代数数据类型的经典用法。没有理由为不同类型的消息设置不同的类型,如果它们具有相同的类型,则生活会更容易。

看起来你正试图模仿其他语言的子类型。根据经验,您可以使用代数数据类型代替其他语言中子类型的大多数用法。这肯定是其中之一。

答案 1 :(得分:8)

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

关于你的问题的一些评论提到最简单的解决方案是使用总和(a.k.a.变体)类型:

data ParserMsg = DebugMsg String | UpdateMsg [String]

这样做的结果是提前定义了一组替代结果。这有时是一个好处(你的代码可以确定没有未处理的子句),有时是一个缺点(有一些有限数量的子句,它们是在编译时确定的。)

在某些情况下更高级的解决方案 - 您可能不需要,但我只是将其抛入 - 重构代码将函数用作数据。这个想法是你创建一个数据类型,其中包含函数(或monadic actions)作为其字段,然后不同的行为=不同的函数作为记录字段。

将这两个样式与此示例进行比较。首先,将不同的情况指定为一个总和(这使用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)

在第一个示例中,Frame只能是SimpleFrameProductFrame,并且必须定义每个Frame函数来处理这两种情况。< / p>

第二,带有函数成员的数据类型(我的两个示例共同的代码):

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本身的数据成员。没有固定的编译时子集子集,因为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"