這是因爲創建一個Endo
一個整潔的例子,即功能a -> a
喜歡你[Film] -> [Film]
是一個無狀態的語言來處理「國家」的好方法。讓我們在潛水。
所以我們的目標是創建一個像becomeFan "Joseph" "7 1/2" :: [Film] -> [Film]
一個功能是「電影數據庫更新功能」。要執行此更新,您需要修改電影數據庫以更新電影"7 1/2"
的粉絲列表,使其包含"Joseph"
。我們假設每個用戶的名字是全局唯一的,並且多次寫入這個函數。
現在我們假設,如果影片不在我們的數據庫中,那麼becomeFan
不會做任何事情,並且數據庫不包含重複項。
首先我們有直接的遞歸版本。
becomeFan _ _ [] = [] -- empty film database
becomeFan name film ([email protected](title, cast, year, fans) : fs)
| film == title = (title, cast, year, name:fans) : fs
| otherwise = f : becomeFan name film fs
這將只是重複倒在數據庫影片列表和使我們更新且僅當弗利姆標題匹配我們正在試圖修改的一個。請注意0-語法,它允許我們將電影作爲「整體」進行檢查,並仍然對其進行解構。
雖然這種方法面臨的挑戰無數,但它非常複雜!我們有許多基本假設與我們實施becomeFan
的方式相關,這些假設可能會與我們編寫的其他函數一起被剝離。幸運的是,Haskell很好,很好解決這樣的問題。
第一步是引入一些更強大的數據類型。
我們要做的是消除一些類型的同義詞,並介紹一些更強大的容器類型,尤其是Set
其行爲類似於數學集合和Map
它就像一本字典或哈希。
import qualified Data.Set as Set
import qualified Data.Map as Map
我們還對Film
使用「記錄」類型。記錄同構於(「功能等同於」)元組,但具有對文檔有用的命名字段,並讓我們使用更少的類型同義詞。
type Name = String
type Year = Int
data Film = Film { title :: Title, cast :: Set Name, year :: Year, fans :: Set Name)
通過使用Map Title Film
來代表我們的數據庫中,我們也得到保證電影的獨特性(一個Map
使得Title
鍵零個或一個Film
S-我們不能有多個匹配)。這裏的缺點是我們可能會在Database
密鑰中同步Title
,Title
中的Film
類型本身。
type Database = Map Title Film
那麼如何在這個新系統中重寫becomeFan
?
becomeFan name title =
Map.alter update title where
update Nothing = Nothing -- that title was not in our database
update (Just f) = Just (f { fans = Set.insert name (fans f) })
現在我們扶着Map.alter :: (Maybe v -> Maybe v) -> k -> Map k v -> Map k v
和Set.insert :: a -> Set a -> Set a
大多是做我們的繁重和維護各種唯一性約束。請注意,Map.alter
的第一個參數是一個函數Maybe v -> Maybe v
,它允許我們處理缺失的影片(如果輸入是Nothing
)並決定從數據庫中刪除膠片(如果我們返回Nothing
)。
這也是值得注意的是,我們的內部函數update :: Maybe Film -> Maybe Film
可以更容易寫成fmap (\f = f { fans = Set.insert name (fans f) })
解除「純粹」的更新步驟上放入Maybe
,因爲它是一個Functor
。
我們可以做得更好嗎?當然,但這裏讓人感到困惑。在大多數情況下,以前的答案可能是你最好的選擇。但是,讓我們開玩笑吧。
我們可以使用Control.Lens的鏡片使我們更容易訪問到Map
,Set
和Film
。
要做到這一點,我們將導入模塊
import Control.Lens
並重寫Film
類型,以便庫可以自動生成使用微距鏡頭。
data Film = Film { _title :: Title, _cast :: Set Name, _year :: Year, _fans :: Set Name }
$(makeLenses ''Film)
我們所要做的就是在前面加上一個下劃線到每個記錄字段名稱,Control.Lens.makeLenses
會自動生成根據原先的名字我們的鏡頭。因此,在該行之後,我們有像title :: Lens' Film Title
這樣的功能,這正是我們想要的功能。
然後我們可以使用Map
的At
實例來創建我們的改造功能,之前相當多的,但寫成鏡頭操作的字符串
becomeFan name film = over (at film) (fmap . over fans . Set.insert $ name)
其中over (at film)
推廣並取代Map.alter
和(fmap . over fans . Set.insert $ name)
替代了我們update
我們之前定義的內部函數。
我們甚至可以構建一個強大的二傳手鏡頭直接着眼於有一定Film
粉絲列表中的某個風扇的存在。
isFan :: Name -> String -> Setter' Database Bool
isFan name film = at film . mapped . fans . contains name
這些方法在第一相當討厭的,並有很奇怪的(但完全可檢查的)類型,但他們變得非常漂亮,一旦你習慣了在抽象的那個水平工作。它「像英語一樣讀取」,感覺就像XPath的好處。
becomeFan name film = isFan name film .~ True
,並用這種結構,我們甚至可以立即升級的全過程爲State
單子。
flip execState initialDB $ do
isFan "Joseph" "7 1/2" .= True
isFan "Steve" "Citizen Kane" .= True
-- oh, wait, nevermind
isFan "Joseph" "7 1/2" .= False
雖然,我們可以使用任何becomeFan
定義做同樣的Control.Monad.State.withState
。
你嘗試寫這樣的功能?它看起來像什麼?發生了什麼? – Floris 2013-04-04 13:42:05
如果是這樣'becomeFan ::標題 - >風扇 - >數據庫 - > Database'或'becomeFan ::範 - >標題 - >數據庫 - > Database'?定義所有這些類型的同義詞之後,它是一個恥辱不使用它們...... – dave4420 2013-04-04 14:27:32
您的權利,這將是一個更好的類型要使用的功能,使其更清晰 – user2240649 2013-04-04 14:30:21