如何在递归结构中存储任意值或如何构建可扩展的软件体系结构?

时间:2014-10-06 20:26:29

标签: haskell architecture functional-programming

我正在研究基本的UI工具包,并试图找出整体架构。

我正在考虑使用WAI's structure for extensibility。我的UI核心结构的简化示例:

run :: Application -> IO ()
type Application = Event -> UI -> (Picture, UI)
type Middleware = Application -> Application

在WAI中,中间件的任意值被保存in the vault。我认为这对于保存仲裁值是一个糟糕的黑客,因为它不是透明的,但我想不出足够简单的结构来替换这个保险库,以便为每个中间件提供一个保存任意值的地方。

我考虑过以递归方式存储元组中的元组:

run :: (Application, x) -> IO ()
type Application = Event -> UI -> (Picture, UI)
type Middleware y x = (Application, x) -> (Application, (y,x))

或者只使用延迟列表来提供不需要分隔值的级别(这样可以提供更多自由,但也有更多问题):

run :: Application -> IO ()
type Application = [Event -> UI -> (Picture, UI)]
type Middleware = Application -> Application

实际上,我会使用修改后的懒惰列表解决方案。哪些其他解决方案可能有效?

请注意:

  • 我根本不喜欢使用镜头。
  • 我知道UI -> (Picture, UI)可以定义为State UI Picture
  • 我不知道有关monad,变压器或FRP的解决方案。很高兴看到一个。

1 个答案:

答案 0 :(得分:1)

镜头提供了引用数据类型字段的一般方法,以便您可以扩展或重构数据集,而不会破坏向后兼容性。我会使用lens-familylens-family-th库来说明这一点,因为它们的依赖性比lens更轻。

让我们从一个带有两个字段的简单记录开始:

{-# LANGUAGE Template Haskell #-}

import Lens.Family2
import Lens.Family2.TH

data Example = Example
    { _int :: Int
    , _str :: String
    }

makeLenses ''Example
-- This creates these lenses:
int :: Lens' Example Int
str :: Lens' Example String

现在,您可以编写引用数据结构字段的State个完整代码。您可以将Lens.Family2.State.Strict用于此目的:

import Lens.Family2.State.Strict

-- Everything here also works for `StateT Example IO`
example :: State Example Bool
example = do
    s <- use str     -- Read the `String`
    str .= s ++ "!"  -- Set the `String`
    int += 2         -- Modify the `Int`
    zoom int $ do     -- This sub-`do` block has type: `State Int Int`
        m <- get
        return (m + 1)

需要注意的关键是我可以更新我的数据类型,上面的代码仍然可以编译。向Example添加一个新字段,一切仍然有效:

data Example = Example
    { _int  :: Int
    , _str  :: String
    , _char :: Char
    }

makeLenses ''Example
int  :: Lens' Example Int
str  :: Lens' Example String
char :: Lens' Example Char

但是,我们实际上可以更进一步,完全重构我们的Example类型:

data Example = Example
    { _example2 :: Example
    , _char     :: Char
    }

data Example2 = Example2
    { _int2 :: Int
    , _str2 :: String
    }

makeLenses ''Example
char     :: Lens' Example Char
example2 :: Lens' Example Example2

makeLenses ''Example2
int2  :: Lens' Example2 Int
str2  :: Lens' Example2 String

我们必须打破旧代码吗?没有!我们所要做的就是添加以下两个镜头以支持向后兼容性:

int :: Lens' Example Int
int = example2 . int2

str :: Lens' Example Char
str = example2 . str2

现在,尽管我们Example类型进行了侵入式重构,但所有旧代码仍然无需任何更改即可正常运行。

事实上,这不仅仅适用于记录。对于和类型,你也可以做同样的事情(a.k.a。代数数据类型或枚举)。例如,假设我们有这种类型:

data Example3 = A String | B Int

makeTraversals ''Example3
-- This creates these `Traversals'`:
_A :: Traversal' Example3 String
_B :: Traversal' Example3 Int

我们对和类型所做的许多事情同样可以用Traversal'来重新表达。除了模式匹配之外,还有一个值得注意的例外:它实际上可以通过Traversal来实现与整体检查的模式匹配,但它目前是详细的。

然而,同样的观点仍然存在:如果你用Traversal' s来表达你所有的和类型操作,那么你可以大大重构你的和类型,只需更新相应的Traversal'来保持倒退兼容性。

最后:请注意,sum类型构造函数的真正模拟是Prism s(除了模式匹配之外,还可以使用构造函数构建值)。 lens-family系列库不支持这些库,但它们由lens提供,如果需要,您可以仅使用profunctors依赖项自行实现它们。

此外,如果您想知道新类型的lens类似物是什么,它是Iso',并且最低限度地要求profunctors依赖。

此外,我所说的一切都可用于参考递归类型的多个字段(使用Fold s)。从字面上看,您可以想象的任何想要以向后兼容的方式在数据类型中引用的内容都包含在lens库中。