Haskell 如何使用记录设计扩展
我想知道Haskell社区的人是如何处理以下设计的。 假设是一个类似工作流的系统,其中您通过系统中的多个步骤传递一些数据(结构)。 随着数据在系统中流动,越来越多的数据项将添加到该结构中,而这些数据项在前面的步骤中不可用。 现在,我想确保在前面的步骤中不可用的数据项是不可访问的,最好是通过编译时检查 到目前为止,我提出了两种不同的方法 方法1:反复重新创建所有类型:Haskell 如何使用记录设计扩展,haskell,functional-programming,records,algebraic-data-types,Haskell,Functional Programming,Records,Algebraic Data Types,我想知道Haskell社区的人是如何处理以下设计的。 假设是一个类似工作流的系统,其中您通过系统中的多个步骤传递一些数据(结构)。 随着数据在系统中流动,越来越多的数据项将添加到该结构中,而这些数据项在前面的步骤中不可用。 现在,我想确保在前面的步骤中不可用的数据项是不可访问的,最好是通过编译时检查 到目前为止,我提出了两种不同的方法 方法1:反复重新创建所有类型: module Step1 where data A = A { item1 :: SomeType } module
module Step1 where
data A = A { item1 :: SomeType }
module Step2 where
data B = B { item1 :: SomeType, item2 :: SomeOtherType }
fromAtoB :: A -> B
module Step3 where
data C = C { item1 :: SomeType, item2 :: SomeOtherType, item3 :: SomeOtherTypeAgain }
fromBtoC :: B -> C
显然,定义的步骤越多,定义的数据类型越深、范围越广,这就变得非常麻烦
方法2:组合类型:
module Step1 where
data A = A { item1 :: SomeType }
module Step2 where
data B = B { a :: A , item2 :: SomeOtherType }
fromAtoB :: A -> B
module Step3 where
data C = C { b :: B, item3 :: SomeOtherTypeAgain }
fromBtoC :: B -> C
这种方法存在一个问题,即任何给定步骤的用户突然暴露于之前的所有步骤,因为对某些属性的访问与对其他属性的访问不同(例如,
cInstance.b.a.Item1
与cInstance.Item1
),即使对于任何给定步骤的用户,数据结构自然是平坦的。
事实上,他/她甚至不一定知道在他们自己的步骤之前有步骤。在OO系统中,我将简单地从B扩展C,从A扩展B
任何想法都是非常受欢迎的。如果您想避免语言扩展,您提出的两种解决方案是可行的。对于嵌套的变体,我建议您
{-#UNPACK#-}
嵌套数据。这样至少可以避免运行时的间接寻址
如果你真的想使用一些子类型,比如,看看我几天前提出的
然而,我认为对于这个问题,最好采用一种通常用于在各个阶段之间转换数据的方法(GHC使用类似的方法来处理Haskell AST)。基本上,您可以创建一个类型family
,通过在正确的阶段之前输入()
,将字段“隐藏”到正确的阶段
{-# LANGUAGE TypeFamilies, DataKinds #-}
data Stage = A | B | C
-- | A data type containing the final set of fields
data Complete (stage :: Stage) = Complete
{ item1 :: RestrictedUntilAfter A stage SomeType
, item2 :: RestrictedUntilAfter B stage SomeOtherType
, item3 :: RestrictedUntilAfter C stage SomeOtherTypeAgain
}
-- | Compares the two given stages to determine if the result type should be hidden
-- as `()` or not
type family RestrictedUntilAfter (s1 :: Stage) (s2 :: Stage) x :: * where
RestrictedUntilAfter B A _ = ()
RestrictedUntilAfter C A _ = ()
RestrictedUntilAfter C B _ = ()
RestrictedUntilAfter _ _ t = t
然后,经过管道的类型是完成A
、完成B
和完成C
。在某个阶段之前被限制的字段将在该阶段之前具有类型()
c1 = Complete { item1 = x, item2 = (), item3 = () } :: Complete A -- x :: SomeType
c2 = Complete { item1 = x, item2 = y, item3 = () } :: Complete B -- y :: SomeOtherType
c3 = Complete { item1 = x, item2 = y, item3 = z } :: Complete C -- z :: SomeOtherTypeAgain
(类型族最好是开放的,或者模式以不同的顺序匹配,但想法是一样的)
编辑
正如我所怀疑的,有一种更清洁的家庭方式。事实上,使用这种方法,您甚至不需要定义任何类型族,并且在添加阶段和字段时,它可以很好地根据LOC进行扩展。最后,它更加灵活。然而,这确实取决于
现在,您甚至可以在阶段
a
和C
(但不是B
)中有一个非()
的字段:item4::stage`RestrictedTo`[a,C]其他类型
“这种方法存在一个问题,即任何给定步骤的用户都会突然暴露在前面的所有步骤中。”对于任何给定步骤的用户,期望通过someInstance.itemX
vssomeInstance.b.a.itemX
访问任何属性,以实现类型族技巧(第4页)@BenjaminHodgson谢谢!在这一点上,我假设一个场总是从受限变为可用(再也不会回来),所以我只需要一个类型族,所以我玩得更快更脏。为了表示不同阶段字段的任意屏蔽,每个field.thx需要一个类型族作为答案。很不错的。如果不使用类型族而只使用更高级的类型,是否可以模拟相同的效果?@robkuz我不这么认为,但如果您不想自己定义任何类型族,请参阅我的编辑。
{-# LANGUAGE TypeFamilies, DataKinds, TypeOperators #-}
import Data.Type.List
import Data.Type.Bool
data Stage = A | B | C
type RestrictedTo stage validStages ty = If (Find stage validStages) ty ()
-- | A data type containing the final set of fields
data Complete (stage :: Stage) = Complete
{ item1 :: stage `RestrictedTo` [A,B,C] SomeType
, item2 :: stage `RestrictedTo` [B,C] SomeOtherType
, item3 :: stage `RestrictedTo` [C] SomeOtherTypeAgain
}