使用类型对Haskell中的顺序值转换建模的好方法?
假设我有一个名为使用类型对Haskell中的顺序值转换建模的好方法?,haskell,Haskell,假设我有一个名为Report的假设类型,它如下所示: data Report = Report { juniorReview :: Maybe (Person, Bool) , seniorReview :: Maybe (Person, Bool) ... many other fields } addJuniorReport :: Report -> (Person, Bool) -> JuniorReport addSeniorReport :: Jun
Report
的假设类型,它如下所示:
data Report = Report {
juniorReview :: Maybe (Person, Bool)
, seniorReview :: Maybe (Person, Bool)
... many other fields
}
addJuniorReport :: Report -> (Person, Bool) -> JuniorReport
addSeniorReport :: JuniorReport -> (Person, Bool) -> SeniorReport
data Report stage = Report { ... }
data JuniorReview
data SeniorReview
...
addJuniorReview :: Report () -> (Person, Bool) -> Report JuniorReview
addSeniorReview :: Report JuniorReview -> (Person, Bool) -> SeniorReview
以及简化的功能,如:
addJuniorReview :: Report -> (Person, Bool) -> Report
addSeniorReview :: Report -> (Person, Bool) -> Report
一份报告
必须经过一个序列,在这个序列中,初级审查员被附加并批准或不批准(元组中的Bool),然后高级审查员也会这样做。它必须始终按此顺序执行序列。但是类型系统并不强制执行这一点。我希望是这样。最好的方法是什么?我也愿意重新设计数据类型
另外,假设还有许多其他步骤必须通过Report
才能达到其完成状态,每个步骤都会向其字段添加更多的数据位。我想要一个解决方案,可以很容易地扩展到一个多步骤的多阶段过程
编辑
另一个要求是,该值必须在每个中间状态下都可以显示。中间状态不能是像
(Person,Bool)->Report这样的不完整构造函数
当前类型与域基本不匹配:它可能表示无效状态。特别是,我们可以有各种没有意义的Nothing
组合:
Report Nothing Nothing
Report Nothing (Person, True)
这个问题的一个很好的解决方案是用一种类型(或者更可能是多种类型)替换报告的模型,这种类型可以排除这样的无效状态
这个特殊的情况非常简单,在您的序列中有有限数量的不同事物。我将直接将两者建模为两种不同的类型:
data Report = Report { ... many other fields ... }
data JuniorReport = JuniorReport (Person, Bool) Report
data SeniorReport = SeniorReport (Person, Bool) JuniorReport
然后,您的函数将如下所示:
data Report = Report {
juniorReview :: Maybe (Person, Bool)
, seniorReview :: Maybe (Person, Bool)
... many other fields
}
addJuniorReport :: Report -> (Person, Bool) -> JuniorReport
addSeniorReport :: JuniorReport -> (Person, Bool) -> SeniorReport
data Report stage = Report { ... }
data JuniorReview
data SeniorReview
...
addJuniorReview :: Report () -> (Person, Bool) -> Report JuniorReview
addSeniorReview :: Report JuniorReview -> (Person, Bool) -> SeniorReview
如果您的整个过程不太复杂或动态,那么扩展这种方法以在您的类型中显式编码它是合理的
一种更灵活、简洁但稍微困难的方法是将流程的当前阶段编码为报告上的幻影类型。幻象类型是类型本身中未使用的类型参数,允许您向类型添加任意附加约束。它可能是这样的:
data Report = Report {
juniorReview :: Maybe (Person, Bool)
, seniorReview :: Maybe (Person, Bool)
... many other fields
}
addJuniorReport :: Report -> (Person, Bool) -> JuniorReport
addSeniorReport :: JuniorReport -> (Person, Bool) -> SeniorReport
data Report stage = Report { ... }
data JuniorReview
data SeniorReview
...
addJuniorReview :: Report () -> (Person, Bool) -> Report JuniorReview
addSeniorReview :: Report JuniorReview -> (Person, Bool) -> SeniorReview
每个步骤的基础报表数据结构都是相同的,您只是添加了一个类型注释,说明它在流程的哪个步骤上
如果有大量步骤,则可以使用类型级别符号,而不是空数据类型作为注释。这将允许您在类型级别使用字符串文字来表示每个步骤:
{-# LANGUAGE DataKinds, KindSignatures #-}
import GHC.TypeLits
data Report (a :: Symbol) = Report {}
addJuniorReview :: Report "Start" -> Report "JuniorReview"
但是,它仍然为您提供了良好的bug预防和自我文档。此外,您可以通过保持报表类型的抽象性而不导出构造函数来避免大多数可能出现的问题,确保只能使用模块中的函数来创建它。您可以为此使用数据类型和幻影类型变量:
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
data Report = Report deriving (Eq, Show)
data Person = Person deriving (Eq, Show)
data ReviewerType
= New
| Junior
| Senior
deriving (Eq, Show)
data Review :: ReviewerType -> * where
Review :: Report -> Review rt
type NewReview = Review New
type ReviewedByJunior = Review Junior
type ReviewedBySenior = Review Senior
finishReview :: ReviewedBySenior -> Report
finishReview (Review report) = report
addJuniorReview :: Review New -> (Person, Bool) -> ReviewedByJunior
addJuniorReview (Review report) (person, b) = Review Report
addSeniorReview :: ReviewedByJunior -> (Person, Bool) -> ReviewedBySenior
addSeniorReview (Review report) (person, b) = Review Report
reviewChain :: Report -> (Person, Bool) -> (Person, Bool) -> Report
reviewChain report junior senior
= finishReview
$ flip addSeniorReview senior
$ flip addJuniorReview junior
$ Review report
如果你想展示它,只要有
showReview :: Review a -> String
showReview (Review report) = show report
使用此设置,您可以通过ReviewerType
定义任意数量的步骤,但是您将无法构造Review Int
或类似的内容,并且您可以非常明确地说明每个函数可以执行的阶段。这种方法的缺点是,您可以将函数限制为接受1种或全部类型,两者之间没有任何区别(没有额外的样板和丑陋)。“最后”,是的,但请参阅我的编辑。@CarstenKönig是的,我想到了单独的类型和求和类型,但是,当你有大量的步骤时,这似乎是难以管理的。看起来我们都有相同的想法,但实现略有不同。与简单求和类型相比,使用TypeLits
有什么好处?对我来说,我更希望有一组有限的数据类型,而Symbol
允许您放置任意字符串。这可能对扩展性很有好处,但也会带来更多的问题空间。@bheklillr:对,这取决于您希望如何组织流程,以及流程需要多么灵活或可扩展。