如何使用记录设计扩展
How to design for extension using records
我想知道 Haskell 社区的人们如何处理以下设计。
假设一个类似工作流的系统,您通过系统中的多个步骤传输一些数据(结构)。
随着数据流经系统,越来越多的数据项将被添加到该结构中,而这些数据项在之前的步骤中是不可用的。
现在我想确保无法访问先前步骤中不可用的数据项 - 最好通过编译时检查。
到目前为止,我想出了两种不同的方法。
方法 1:一遍又一遍地重新创建所有类型:
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
),尽管对于任何给定步骤的用户来说,数据结构自然是扁平的。
确实he/she连自己的step之前还有steps都不一定知道。在 OO 系统中,我会简单地从 B 扩展 C,从 A 扩展 B。
非常欢迎任何想法。
如果你想避免语言扩展,你提出的两个解决方案是可行的。对于嵌套的变体,我建议您 {-# UNPACK #-}
嵌套数据。这样你至少可以在运行时避免间接寻址。
如果您真的想要使用某些东西subtyping-like,请查看this solution我几天前想到的。
但是,我认为对于这个问题,您最好采用一种通常用于阶段到阶段转换的数据的方法(GHC 使用类似的方法来处理 Haskell AST)。基本上,您可以创建一个 type familiy
,"hides" 字段直到正确的阶段,方法是给它们输入 ()
直到正确的阶段。
{-# 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
然后,您通过管道的类型是 Complete A
、Complete B
和 Complete 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
(类型族打开可能更好,或者pattern-matched顺序不同,但思路是一样的)
编辑
正如我所怀疑的那样,有一种更清洁的家庭方法。事实上,使用这种方法,您甚至不需要定义任何类型系列,并且在添加阶段和字段时它可以根据 LOC 很好地扩展。最后,它更加灵活。但是,它确实取决于 type-list
.
{-# 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
}
现在,您甚至可以只在 A
和 C
阶段(而不是 B
)拥有非 ()
的字段:item4 :: stage `RestrictedTo` [A,C] SomeOtherOtherType
我想知道 Haskell 社区的人们如何处理以下设计。 假设一个类似工作流的系统,您通过系统中的多个步骤传输一些数据(结构)。 随着数据流经系统,越来越多的数据项将被添加到该结构中,而这些数据项在之前的步骤中是不可用的。 现在我想确保无法访问先前步骤中不可用的数据项 - 最好通过编译时检查。
到目前为止,我想出了两种不同的方法。
方法 1:一遍又一遍地重新创建所有类型:
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
),尽管对于任何给定步骤的用户来说,数据结构自然是扁平的。
确实he/she连自己的step之前还有steps都不一定知道。在 OO 系统中,我会简单地从 B 扩展 C,从 A 扩展 B。
非常欢迎任何想法。
如果你想避免语言扩展,你提出的两个解决方案是可行的。对于嵌套的变体,我建议您 {-# UNPACK #-}
嵌套数据。这样你至少可以在运行时避免间接寻址。
如果您真的想要使用某些东西subtyping-like,请查看this solution我几天前想到的。
但是,我认为对于这个问题,您最好采用一种通常用于阶段到阶段转换的数据的方法(GHC 使用类似的方法来处理 Haskell AST)。基本上,您可以创建一个 type familiy
,"hides" 字段直到正确的阶段,方法是给它们输入 ()
直到正确的阶段。
{-# 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
然后,您通过管道的类型是 Complete A
、Complete B
和 Complete 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
(类型族打开可能更好,或者pattern-matched顺序不同,但思路是一样的)
编辑
正如我所怀疑的那样,有一种更清洁的家庭方法。事实上,使用这种方法,您甚至不需要定义任何类型系列,并且在添加阶段和字段时它可以根据 LOC 很好地扩展。最后,它更加灵活。但是,它确实取决于 type-list
.
{-# 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
}
现在,您甚至可以只在 A
和 C
阶段(而不是 B
)拥有非 ()
的字段:item4 :: stage `RestrictedTo` [A,C] SomeOtherOtherType