2015-05-19 21 views
5

我決定嘗試函數式編程和Purescript。在閱讀"Learn you a Haskell for great good""PureScript by Example"後,稍微玩了一段代碼,我認爲我可以說我明白了基本知識,但有一件事讓我很困擾 - 代碼看起來非常耦合。我通常經常更換庫,並且在OOP中,我可以使用洋蔥體系結構將我自己的代碼與庫特定的代碼解耦,但我不知道如何在Purescript中執行此操作。如何在purescript中構建應用程序

我試圖找到人們在Haskell中如何做到這一點,但我能找到的答案都是「沒有人在Haskell中製作過複雜的應用程序,所以沒有人知道如何做」或「你有輸入你有輸出,它們之間的一切都只是純粹的功能「。但在這一刻我有一個玩具應用程序,它使用虛擬儀器,信號,網絡存儲,路由器庫,並且它們每個都有自己的效果和數據結構,所以聽起來不像一個輸入和一個輸出。

所以我的問題是我應該如何構造我的代碼或我應該使用什麼工藝以便我可以更改我的庫而不必重寫我的一半應用程序?

更新:

建議使用幾層,並保持在主模塊中的效果是很常見也和我明白爲什麼我應該這樣做。
下面是一個簡單的例子,希望能說明我在談論的問題:

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操作和控制檯日誌記錄簡單的計數器的應用程序。正如你所看到的,沒有單一的輸入和單一的輸出。我們可以從路由器或事件監聽器獲得輸入,並使用console或dom作爲輸出,所以它變得稍微複雜一些。

擁有這一切effectful碼主模塊中的原因有兩個感覺不對我:

  1. 如果我將不斷增加航線和控制器該模塊將很快變成了一千行一塌糊塗。
  2. 飼養路由,DOM操作和數據在同一模塊中存儲違反了單一職責原則(我認爲這是很重要的FP太)

我們可以在這個模塊分割成幾個的,例如一個模塊每個控制器並創建一些有效的圖層。但是,當我有十個控制器模塊,並且我想更改我的dom特定庫時,我應該編輯它們。

這兩種方法都很不理想,所以問題是我應該選擇哪一個?或者也許還有其他方法可以去?

回答

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 

然後再新建閃亮出來,並要切換你的路由庫。您需要創建一個符合相同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>" 
3

這是一個開放式問題,所以如果沒有具體的例子就很難回答。

你必須輸入你有輸出,兩者之間的一切都只是純函數

聲明這樣實際上是非常接近真理。由於Haskell和PureScript中沒有有狀態的對象,應用程序中的大部分代碼將基於純函數和簡單數據類型(或記錄),因此它不與任何特定庫緊密耦合(除了類似Maybe,Either,Tuple等等,這些都不是你所說的意義上的庫)。

儘可能多地嘗試將使用效果的代碼推送到「外部」。這是您交錯處理任何輸入所需的各種庫並生成您的應用所需的任何輸出的位置。這種分層功能可以輕鬆切換庫,因爲在這裏您將主要將核心純代碼放入Eff monad中,以將其「連線」到外部輸入和輸出。

查看它的一種方法是,如果您發現自己在應用程序的主模塊或頂層以外使用Eff,則可能是「做錯了」。

如果您正在編寫Haskell,請用IO代替Eff

+0

謝謝您的回答。我編輯了我的問題並添加了一個簡單的示例。 – starper