如何使用记录设计扩展

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.Item1cInstance.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 AComplete BComplete 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
  }

现在,您甚至可以只在 AC 阶段(而不是 B)拥有非 () 的字段:item4 :: stage `RestrictedTo` [A,C] SomeOtherOtherType