【问题标题】:How to structure app in purescript如何在纯脚本中构建应用程序
【发布时间】:2015-05-19 08:04:42
【问题描述】:

我决定尝试函数式编程和 Purescript。在阅读了"Learn you a Haskell for great good""PureScript by Example" 并稍微玩了一下代码之后,我认为我可以说我了解基础知识,但有一件事让我很困扰 - 代码看起来非常耦合。我通常会经常更改库,在 OOP 中我可以使用洋葱架构将我自己的代码与特定于库的代码分离,但我不知道如何在 Purescript 中执行此操作。

我试图找出人们在 Haskell 中是如何做到这一点的,但我只能找到诸如“没有人在 Haskell 中制作过复杂的应用程序,所以没有人知道如何做到这一点”或“你有输入和你有输出,中间的一切都只是纯函数”。但是现在我有一个玩具应用程序,它使用了虚拟 dom、信号、Web 存储、路由器库,它们每个都有自己的效果和数据结构,所以听起来不像一个输入和一个输出。

所以我的问题是我应该如何构建我的代码或我应该使用什么技术,这样我就可以在不重写我的应用程序的一半的情况下更改我的库?

更新:

在主模块中使用多层并保持效果的建议也很常见,我明白为什么要这样做。
这是一个简单的例子,希望能说明我正在谈论的问题:

btnHandler :: forall ev eff. (MouseEvent ev) => ev -> Eff (dom :: DOM, webStorage :: WebStorage, trace :: Trace | eff) Unit
btnHandler e = do
  btn <- getTarget e
  Just btnId <- getAttribute "id" btn
  Right clicks <- (getItem localStorage btnId) >>= readNumber
  let newClicks = clicks + 1
  trace $ "Button #" ++ btnId ++ " has been clicked " ++ (show newClicks) ++ " times"
  setText (show newClicks) btn
  setItem localStorage btnId $ show newClicks
  -- ... maybe some other actions
  return unit

-- ... other handlers for different controllers

btnController :: forall e. Node -> _ -> Eff (dom :: DOM, webStorage :: WebStorage, trace :: Trace | e) Unit
btnController mainEl _ = do
  delegateEventListener mainEl "click" "#btn1" btnHandler
  delegateEventListener mainEl "click" "#btn2" btnHandler
  delegateEventListener mainEl "click" "#btn3" btnHandler
  -- ... render buttons
  return unit

-- ... other controllers

main :: forall e. Eff (dom :: DOM, webStorage :: WebStorage, trace :: Trace, router :: Router | e) Unit
main = do
  Just mainEl <- body >>= querySelector "#wrapper"
  handleRoute "/" $ btnController mainEl
  -- ... other routes each with it's own controller
  return unit

这里我们有一个简单的计数器应用程序,其中包含路由、网络存储、dom 操作和控制台日志记录。如您所见,没有单一的输入和单一的输出。我们可以从路由器或事件监听器获取输入,并使用控制台或 dom 作为输出,所以它变得有点复杂。

在主模块中包含所有这些有效的代码对我来说感觉不对,原因有两个:

  1. 如果我继续添加路由和控制器,这个模块很快就会变成一千行混乱。
  2. 将路由、dom 操作和数据存储保持在同一个模块中违反了单一职责原则(我认为这在 FP 中也很重要)

我们可以将此模块拆分为多个模块,例如每个控制器一个模块并创建某种有效层。但是当我有十个控制器模块并且我想更改我的 dom 特定库时,我应该全部编辑它们。

这两种方法都远非理想,所以问题是我应该选择哪一种?或者也许还有其他方法可以走?

【问题讨论】:

    标签: haskell architecture purescript


    【解决方案1】:

    没有理由不能有一个中间层来抽象依赖关系。假设您想为您的应用程序使用路由器。您可以定义如下所示的“路由器抽象”库:

    module App.Router where
    
    import SomeRouterLib
    
    -- Type synonym to make it easy to change later
    type Route = SomeLibraryRouteType
    
    -- Just an alias to the Router library
    makeRoute :: String -> Route -> Route
    makeRoute = libMakeRoute
    

    然后新的闪亮出来了,你想切换你的路由库。您需要创建一个新模块,该模块符合相同的 API,但具有相同的功能 - 一个适配器,如果您愿意的话。

    module App.RouterAlt where
    
    import AnotherRouterLib
    
    type Route = SomeOtherLibraryType
    
    makeRoute :: String -> Route -> Route
    makeRoute = otherLibMakeRoute
    

    在您的主应用程序中,您现在可以交换导入,并且一切正常。可能需要进行更多的按摩才能使类型和功能按您的预期工作,但这是一般的想法。

    您的示例代码本质上是非常必要的。这不是惯用的功能代码,我认为您指出它不可持续是正确的。更多函数式成语包括purescript-halogenpurescript-thermite

    将 UI 视为当前应用程序状态的纯函数。换句话说,考虑到事物的当前价值,我的应用程序是什么样的?此外,考虑到应用程序的当前状态可以通过将一系列纯函数应用于某个初始状态而得出。

    你的应用程序状态是什么?

    data AppState = AppState { buttons :: [Button] }
    data Button = Button { numClicks :: Integer }
    

    你在看什么样的活动?

    data Event = ButtonClick { buttonId :: Integer }
    

    我们如何处理该事件?

    handleEvent :: AppState -> Event -> AppState
    handleEvent state (ButtonClick id) =
        let newButtons = incrementButton id (buttons state)
        in  AppState { buttons = newButtons }
    
    incrementButton :: Integer -> [Button] -> [Button]
    incrementButton _ []      = []
    incrementButton 0 (b:bs)  = Button (1 + numClicks b) : bs
    incrementButton i (b:bs)  = b : incrementButton (i - 1) buttons
    

    如何根据当前状态呈现应用程序?

    render :: AppState -> Html
    render state =
        let currentButtons = buttons state
            btnList = map renderButton currentButtons
            renderButton btn = "<li><button>" ++ show (numClicks btn) ++ "</button></li>" 
        in  "<div><ul>" ++ btnList ++ "</ul></div>"
    

    【讨论】:

      【解决方案2】:

      这是一个有点开放式的问题,所以如果没有具体的例子很难具体回答。

      你有输入,你有输出,中间的一切都是纯函数

      这样的陈述实际上非常接近事实。由于 Haskell 和 PureScript 中没有有状态的对象,应用程序中的大部分代码将基于纯函数和简单的数据类型(或记录),因此它不会与任何特定的库紧密耦合(除了诸如MaybeEitherTuple 等等,它们并不是你所说的真正意义上的库。

      您应该尽可能尝试将使用效果的代码推到“外部”。在这里,您可以交错处理所需的各种库以处理任何输入并生成应用程序所需的任何输出。这种分层使切换库变得容易,因为在这里您主要将核心纯代码提升到 Eff monad 以“连接”到外部输入和输出。

      看待它的一种方式是,如果您发现自己在应用程序的主模块或顶层之外使用Eff,那么您可能“做错了”。

      如果您正在编写 Haskell,请将我提到的任何地方 Eff 替换为 IO

      【讨论】:

      • 感谢您的回答。我已经编辑了我的问题并添加了一个简单的示例。
      猜你喜欢
      • 1970-01-01
      • 2012-03-15
      • 1970-01-01
      • 2021-04-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-08-12
      • 1970-01-01
      相关资源
      最近更新 更多