我在编写解析器时遇到了问题。具体来说,我想成为不同类型的返回值。例如,我有两种不同的数据类型FA
和PA
来表示两种不同的脂类 -
data FA = ClassLevelFA IntegerMass
| FA CarbonChain
deriving (Show, Eq, Ord)
data PA = ClassLevelPA IntegerMass
| CombinedRadylsPA TwoCombinedRadyls
| UnknownSnPA Radyl Radyl
| KnownSnPA Radyl Radyl
deriving (Show, Eq, Ord)
使用attoparsec,我已经构建了解析器来解析脂质速记符号。对于上面的数据类型,我有解析器faParser
和paParser
。我希望能够解析FA
或PA
。但是,由于FA
和PA
是不同的数据类型,我无法执行以下操作 -
inputParser = faParser
<|> paParser
我最近了解了GADT,我认为这可以解决我的问题。因此,我去做了一个GADT和一个eval
函数,并更改了解析器faParser
和paParser
。 -
data ParsedLipid a where
ParsedFA :: FA -> ParsedLipid FA
ParsedPA :: PA -> ParsedLipid PA
eval :: ParsedLipid a -> a
eval (ParsedFA val) = val
eval (ParsedPA val) = val
这让我接近但看起来好像ParsedFA
和ParsedPA
是不同的数据类型?例如,解析"PA 17:1_18:1"
会给我一个类型为ParsedLipid PA
的值(不仅仅是ParsedLipid
,正如我所料)。因此,解析器inputParser
仍然不进行类型检查。
let lipid = use "PA 17:1_18:1"
*Main> :t lipid
lipid :: ParsedLipid PA
关于如何解决这个问题的任何建议?
答案 0 :(得分:5)
@MathematicalOrchid指出你可能不需要GADT,而简单的和类型就足够了。您可能有一个XY problem,但我对您的用例了解不够,无法做出明确的判断。这个答案是关于如何将无类型数据转换为GADT。
正如您所注意到的,您的解析功能无法返回ParsedLipid a
,因为这使得调用上下文可以自由选择a
,这是没有意义的; a
实际上是由输入数据决定的。并且您无法返回ParsedLipid FA
或ParsedLipid PA
,因为输入数据可能属于任何一种类型。
因此,从运行时数据构建GADT时的标准技巧 - 当您事先不知道索引的类型时 - 是使用existential quantification。
data AParsedLipid = forall a. AParsedLipid (ParsedLipid a)
类型参数a
显示在AParsedLipid
的右侧,但不在左侧。值AParsedLipid
保证包含格式良好的ParsedLipid
,但其精确类型保密。存在主义类型是一个包装器,它可以帮助我们从凌乱的,无类型的现实世界转变为干净,强烈类型的GADT。
将存在量化的包装器推到系统的边缘是一个好主意,在那里你必须与外部世界进行通信。您可以在核心模型中编写带有ParsedLipid a -> a
等签名的函数,并将它们应用于边缘存在的包装器下的数据。您验证输入,将其包装为存在类型,然后使用强类型模型安全地处理它 - 因为您已经检查了输入,所以不必担心错误。
您可以解压缩AParsedLipid
以获取ParsedLipid
,并在其上进行模式匹配,以便在运行时确定a
是什么 - 它可能是{{1 }或FA
。
PA
您注意到,由于上述原因,showFA :: FA -> String
showFA = ...
showPA :: PA -> String
showPA = ...
showLipid :: AParsedLipid -> String
showLipid (AParsedLipid (ParsedFA x)) = "AParsedLipid (ParsedFA "++ showFA x ++")"
showLipid (AParsedLipid (ParsedPA x)) = "AParsedLipid (ParsedPA "++ showPA x ++")"
无法出现在a
的函数的返回类型中。返回类型必须为编译器所知;此技术不允许您使用不同的返回类型定义&#34;函数&#34;。
当您构建AParsedLipid
时,您必须生成足够的证据来说服编译器包裹的AParsedLipid
格式正确。在您的示例中,这归结为解析良好类型的ParsedLipid
或PA
然后将其包装起来。
FA
与运行时数据一起使用时,GADT有点尴尬。存在性包装器有效地擦除parser :: Parser AParsedLipid
parser = AParsedLipid <$> (fmap ParsedFA faParser <|> fmap ParsedPA paParser)
中的额外编译时信息 - ParsedLipid
与AParsedLipid
同构。 (证明代码中的同构是一个很好的练习。)根据我的经验,GADT在构建程序方面比在构建数据方面要好得多 - 他们擅长实现强类型嵌入式域特定语言的类型&#39;索引可以在编译时知道。例如,Yampa和extensible-effects都使用GADT作为其中心数据类型。这有助于编译器检查您在编写的代码中是否正确使用了特定于域的语言(在某些情况下允许某些优化)。您不太可能在运行时从真实数据中构建FRP网络或monadic效果。
答案 1 :(得分:3)
你到底想要什么?
如果您在编译时知道 是否需要FA
或PA
,那么GADT是一种很好的方法。
如果您想在运行时决定 来解析FA
或PA
,您可以使用... Either FA PA
。