我正在尝试在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)
..案例结果的类型应所有分支中的相同。有什么解决方案吗?甚至可能只指定函数结果的类型类并期望它是完全多态的?
答案 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
只能是SimpleFrame
或ProductFrame
,并且必须定义每个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
类型将getIndex
和pointIndex
操作作为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"