Haskell将元素添加到现有列表中

时间:2013-04-04 13:40:25

标签: database list haskell

我想写的功能是让用户输入他们的名字和他们想成为粉丝的电影。

这是我正在使用的当前代码:

type Title = String
type Actor = String
type Cast = [Actor]
type Year = Int
type Fan = String
type Fans = [Fan]
type Period = (Year, Year)
type Film = (Title, Cast, Year, Fans)
type Database = [Film]

testDatabase :: Database
testDatabase = [("Casino Royale", ["Daniel Craig", "Eva Green", "Judi Dench"], 2006, ["Garry", "Dave", "Zoe", "Kevin", "Emma"]),
    ("Cowboys & Aliens", ["Harrison Ford", "Daniel Craig", "Olivia Wilde"], 2011, ["Bill", "Jo", "Garry", "Kevin", "Olga", "Liz"]),     
        ("Catch Me If You Can", ["Leonardo DiCaprio", "Tom Hanks"], 2002, ["Zoe", "Heidi", "Jo", "Emma", "Liz", "Sam", "Olga", "Kevin", "Tim"]),           
            ("Mamma Mia!", ["Meryl Streep", "Pierce Brosnan"], 2008, ["Kevin", "Jo", "Liz", "Amy", "Sam", "Zoe"])]

将此作为类型:

becomeFan :: String -> String -> [Film] -> [Film]

我是否能够执行此任务,例如,如果用户“Bob”想成为电影“妈妈咪呀!”的粉丝。然后数据库将更新粉丝列表:

["Kevin", "Jo", "Liz", "Amy", "Sam", "Zoe"]

为:

["Bob", "Kevin", "Jo", "Liz", "Amy", "Sam", "Zoe"]

提前感谢所有提供的答案!!

3 个答案:

答案 0 :(得分:6)

这是一个很好的例子,因为创建Endo,即像a -> a这样的函数[Film] -> [Film]是处理"状态"的好方法。用无国籍的语言。让我们潜入。


因此,我们的目标是创建一个像becomeFan "Joseph" "7 1/2" :: [Film] -> [Film]这样的函数,这是一个"电影数据库更新功能"。要执行此更新,您需要修改电影数据库以更新电影"7 1/2"的粉丝列表以包含"Joseph"。我们假设每个用户的名字都是全球唯一的,并且多次写这个函数。

现在假设,如果影片不在我们的数据库中,那么becomeFan不会做任何事情,并且数据库不包含重复项。

首先我们有直接递归版本。

becomeFan _ _ [] = [] -- empty film database
becomeFan name film (f@(title, cast, year, fans) : fs)
  | film == title = (title, cast, year, name:fans) : fs
  | otherwise     = f : becomeFan name film fs

如果flim标题与我们正在尝试编辑的标题相匹配,那么只会迭代数据库中的电影列表并进行更新。请注意@ - 语法,它允许我们制作我们正在检查的电影"作为一个整体"并且仍在解构它。

这种方法面临的挑战无数,但它非常复杂!我们有一些基本的假设与我们实现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秒 - 我们不能有多场比赛)。这里的缺点是我们可能会将Title键中的DatabaseTitle类型中的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 vSet.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中的镜头来更轻松地访问MapSetFilm

为此,我们将导入模块

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 monad。

flip execState initialDB $ do
  isFan "Joseph" "7 1/2"        .= True
  isFan "Steve"  "Citizen Kane" .= True
  -- oh, wait, nevermind
  isFan "Joseph" "7 1/2"        .= False

尽管如此,我们可以使用Control.Monad.State.withState的任何定义becomeFan执行相同操作。

答案 1 :(得分:1)

由于我没有真正展示任何具体内容,我只会提供一些一般提示..

add :: String -> String -> Database -> Database
add name film db = (film, cast, year, name : fans) : filteredDb
  where (_, cast, year, fans) = get the movie
        filtered_db = remove the entry about ${film} from the db

希望这足以让你入门,如果你有疑问,请告诉我。

答案 2 :(得分:1)

这是另一种解决方法。

becomeFan :: Title -> Fan -> Database -> Database
becomeFan title newFan = map f where
    f film @ (title', cast, year, fans)
        | title == title' = --TODO
        | otherwise       = --TODO