2014-10-06 203 views
3

我正在研究一個基本的UI工具包,並試圖找出整體架構。我正在考慮使用WAI's structure for extensibility。我的UI的核心結構的一個減小例如:如何在遞歸結構中存儲任意值或如何構建可擴展的軟件體系結構?

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

在WAI,對中間件的任意值被保存in the vault。我認爲保存任意值是一件很糟糕的事情,因爲它不是透明的,但我想不出一個足夠簡單的結構來替換這個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
  • 我不知道關於monads,變壓器或玻璃鋼的解決方案。看到一個會很高興。
+2

你說你不喜歡使用鏡頭,但這正是鏡頭解決問題的那種問題 – 2014-10-06 23:31:58

+0

請注意,WAI的設計目標可能與您的目標完全不同。我傾向於在這裏同意Gabriel,這就是*不常使用鏡頭的人。 – 2014-10-07 07:02:17

+0

@MichaelSnoyman其實,我不知道我想要什麼。一些東西進出,控制其輸入對於簡約用戶界面來說已經足夠了。中間件定義似乎足夠了。應用程序是UI的核心。通常的用戶不寫應用程序。這是一個不同的目的,也許不是最好的方法,但關於中間件的問題就在那裏。 – Vektorweg 2014-10-07 10:51:07

回答

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 FUL代碼引用您的數據結構的字段。您可以使用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類型的侵入重構。

實際上,這不僅僅適用於記錄。對於和類型也可以做同樣的事情(也稱爲代數數據類型或枚舉)。例如,假設我們有這種類型的:

data Example3 = A String | B Int 

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

許多我們與和類型做的事情同樣可以在Traversal'條款再表達。模式匹配有一個明顯的例外:它實際上可以通過使用總體檢查來實現模式匹配,但是它目前是冗長的。

但是,同樣的觀點成立:如果您用Traversal' s表示所有總和類型操作,那麼您可以大大重構您的總和類型,只需更新適當的Traversal'即可,以保持向後兼容性。

最後:請注意sum類型構造函數的真實模擬是Prism s(它允許您使用構造函數除模式匹配外還構建值)。這些不受lens-family系列庫的支持,但是它們由lens提供,如果需要,您可以使用profunctors依賴關係自行實施它們。此外,如果您想知道新類型的lens模擬是什麼,它是Iso',並且最低限度需要profunctors依賴關係。

此外,我所說的一切都適用於遞歸類型的多個字段(使用Fold s)。從字面上看,任何您想要以向後兼容的方式引用數據類型的內容都包含在lens庫中。