如何在purescript中构建app

sta*_*per 5 architecture haskell purescript

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

我试图找到人们如何在Haskell中做到这一点,但我能找到的答案就像"没有人在Haskell中制作过复杂的应用程序,因此没有人知道如何做到这一点"或"你有输入而且你有输出,介于两者之间的只是纯粹的功能".但此时我有一个玩具应用程序,它使用虚拟dom,信号,网络存储,路由器库,每个都有自己的效果和数据结构,所以它听起来不像一个输入和一个输出.

所以我的问题是我应该如何构建我的代码或我应该使用哪些技术,以便我可以更改我的库而不重写我的应用程序的一半?

更新:

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

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
Run Code Online (Sandbox Code Playgroud)

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

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

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

我们可以将这个模块分成几个模块,例如每个控制器一个模块,并创建一些有效的层.但是当我有十个控制器模块并且我想要更改我的dom特定库时,我应该编辑它们.

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

eph*_*ion 6

您没有理由不能使用中间层来抽象依赖项.假设您想为您的应用程序使用路由器.您可以定义一个类似于以下内容的"路由器抽象"库:

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
Run Code Online (Sandbox Code Playgroud)

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

module App.RouterAlt where

import AnotherRouterLib

type Route = SomeOtherLibraryType

makeRoute :: String -> Route -> Route
makeRoute = otherLibMakeRoute
Run Code Online (Sandbox Code Playgroud)

在您的主应用程序中,您现在可以交换导入,一切都应该正常.可能会有更多的按摩需要发生,以使类型和功能按预期工作,但这是一般的想法.

您的示例代码本质上是非常必要的.这不是惯用的功能代码,我认为你注意到它不可持续是正确的.更多功能性成语包括purescript-halogenpurescript-thermite.

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

你的申请状态是什么?

data AppState = AppState { buttons :: [Button] }
data Button = Button { numClicks :: Integer }
Run Code Online (Sandbox Code Playgroud)

你在看什么样的活动?

data Event = ButtonClick { buttonId :: Integer }
Run Code Online (Sandbox Code Playgroud)

我们如何处理该事件?

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
Run Code Online (Sandbox Code Playgroud)

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

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>"
Run Code Online (Sandbox Code Playgroud)