簡體   English   中英

處理函數式編程中的增量數據建模更改

[英]Handling incremental Data Modeling Changes in Functional Programming

在我作為開發人員的工作中我必須解決的大多數問題都與數據建模有關。 例如,在OOP Web應用程序世界中,我經常需要更改對象中的數據屬性以滿足新的要求。

如果我很幸運,我甚至不需要以編程方式添加新的“行為”代碼(函數,方法)。 相反,我可以通過注釋屬性(Java)來聲明添加驗證甚至UI選項。

在函數式編程中,由於模式匹配和數據構造函數(Haskell,ML),添加新數據屬性似乎需要大量代碼更改。

如何最大限度地減少此問題?

這似乎是一個公認的問題,因為Xavier Leroy在“對象和類與模塊”的第24頁很好地說明了 - 總結那些沒有PostScript查看器的人,它基本上說FP語言比OOP語言更好地添加新的對數據對象的行為,但OOP語言更適合添加新的數據對象/屬性。

FP語言中是否有任何設計模式可以幫助緩解此問題?

我已經閱讀了Phillip Wadler 建議使用Monads來幫助解決這個模塊化問題,但我不確定我是怎么理解的?

正如Darius Bacon指出的那樣,這基本上就是表達問題,這是一個長期存在的問題,沒有普遍接受的解決方案。 然而,缺乏兩全其美的方法並不能阻止我們有時想要采用這種方式。 現在,您要求“功能語言的設計模式” ,所以讓我們來看看它。 下面的示例是用Haskell編寫的,但不一定是Haskell(或任何其他語言)的慣用語。

首先,快速回顧一下“表達問題”。 考慮以下代數數據類型:

data Expr a = Lit a | Sum (Expr a) (Expr a)

exprEval (Lit x) = x
exprEval (Sum x y) = exprEval x + exprEval y

exprShow (Lit x) = show x
exprShow (Sum x y) = unwords ["(", exprShow x, " + ", exprShow y, ")"]

這表示簡單的數學表達式,僅包含文字值和加法。 使用我們在這里的函數,我們可以使用表達式並對其進行求值,或將其顯示為String 現在,假設我們要添加一個新函數 - 比如,將函數映射到所有文字值:

exprMap f (Lit x) = Lit (f x)
exprMap f (Sum x y) = Sum (exprMap f x) (exprMap f y)

簡單! 我們可以整天寫作功能而不會出汗! 代數數據類型真棒!

事實上,它們太棒了,我們希望讓我們的表達類型更多,更有表現力。 讓我們擴展它以支持乘法,我們只是......呃......親愛的,那會很尷尬,不是嗎? 我們必須修改我們剛寫的每個函數。 絕望!

事實上,擴展表達式本身比添加使用它們的函數更有趣。 所以,讓我們說我們願意在另一個方向進行權衡。 我們怎么可能這樣做?

好吧,沒有意義中途做事。 讓我們上傳所有內容並反轉整個程序。 那是什么意思? 那么,這是函數式編程,還有什么比高階函數更有用? 我們要做的是將表示表達式值的數據類型替換為表示表達式上的操作的數據類型 我們不需要選擇構造函數,而是需要記錄所有可能的操作,如下所示:

data Actions a = Actions {
    actEval :: a,
    actMap  :: (a -> a) -> Actions a }

那么我們如何創建一個沒有數據類型的表達式呢? 好吧,我們的功能現在是數據,所以我想我們的數據需要是函數。 我們將使用常規函數創建“構造函數”,返回操作記錄:

mkLit x = Actions x (\f -> mkLit (f x))

mkSum x y = Actions 
    (actEval x + actEval y) 
    (\f -> mkSum (actMap x f) (actMap y f))

我們現在可以更容易地添加乘法嗎? 當然可以!

mkProd x y = Actions 
    (actEval x * actEval y) 
    (\f -> mkProd (actMap x f) (actMap y f))

哦,等等 - 我們忘了actShow添加一個actShow動作,讓我們補充一下,我們只是......呃,好吧。

無論如何,使用兩種不同風格的樣子是什么樣的?

expr1plus1 = Sum (Lit 1) (Lit 1)
action1plus1 = mkSum (mkLit 1) (mkLit 1)
action1times1 = mkProd (mkLit 1) (mkLit 1)

當你沒有擴展時,幾乎一樣。

作為一個有趣的旁注,請考慮在“動作”樣式中,表達式中的實際值是完全隱藏的 - actEval字段僅承諾給我們一些正確的類型,它如何提供它是它自己的業務。 由於延遲評估,該字段的內容甚至可以是精細的計算,僅在需要時執行。 對於外部檢查, Actions a值完全不透明,僅向外界呈現已定義的操作。

這種編程風格 - 用一捆“動作”替換簡單數據,同時將實際的實現細節隱藏在黑盒子中,使用類似構造函數的函數來構建新的數據位,能夠用相同的方式交換非常不同的“值”一系列“行動”,等等 - 很有趣。 它可能有一個名字,但我似乎無法回想起......

我聽過這個抱怨多次了,總是讓我感到困惑。 提問者寫道:

在函數式編程中,由於模式匹配和數據構造函數(Haskell,ML),添加新數據屬性似乎需要大量代碼更改。

但這基本上是一個功能,而不是一個錯誤! 例如,當您更改變體中的可能性時,通過模式匹配訪問該變體的代碼將被迫考慮新的可能性出現的事實。 這很有用,因為實際上您確實需要考慮該代碼是否需要更改以對其操作的類型中的語義更改做出反應。

我認為需要“大量代碼更改”的說法。 通過編寫良好的代碼,類型系統通常能夠很好地突出需要考慮的代碼,而不是更多。

也許這里的問題是,如果沒有更具體的例子,很難回答這個問題。 考慮在Haskell或ML中提供一段代碼,你不確定如何干凈地進化。 我想你會以這種方式獲得更精確和有用的答案。

這種權衡在編程語言理論文獻中被稱為表達問題

目標是按案例定義數據類型,其中可以在數據類型上添加新案例,在數據類型上添加新函數,而無需重新編譯現有代碼,同時保留靜態類型安全性(例如,無強制轉換)。

解決方案已經提出,但我沒有研究過它們。 在Lambda The Ultimate上進行了很多討論。)

至少在Haskell中我會創建一個抽象數據類型。 這是創建一個不導出構造函數的類型。 該類型的用戶無法在類型上進行模式匹配,並且您必須提供使用該類型的函數。 作為回報,您將獲得一種更容易修改的類型,而無需更改該類型用戶編寫的代碼。

如果新數據意味着沒有新行為,就像在我們被要求將“生日”字段添加到“人”資源的應用程序中那樣我們所要做的就是將其添加到屬於人員資源,然后在功能和OOP世界中很容易解決。 只是不要將“生日”視為代碼的一部分; 它只是數據的一部分。

讓我解釋一下:如果生日是暗示不同的應用行為的東西,例如我們做不同的事情,如果這個人是未成年人,那么在OOP中我們會將生日字段添加到人類,而在FP中我們會添加類似的生日字段到人的數據結構。

如果“birthdate”沒有附加任何行為,那么代碼中應該沒有名為“birthdate”的字段。 諸如字典(地圖)之類的數據結構將保存各種字段。 無論是OOP還是FP,添加新的都不需要更改程序。 通過附加驗證正則表達式或使用類似的驗證小語言在數據中表達驗證行為應該是什么,將類似地添加驗證。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM