0

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

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

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]

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

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

至:

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

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

4

3 回答 3

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

这只会遍历数据库中的电影列表,并在电影标题与我们正在尝试编辑的标题匹配时进行更新。请注意@- 语法,它允许我们将正在检查的电影“作为一个整体”进行检查,并且仍然解构它。

然而,这种方法的挑战是无数的——它只是非常复杂!我们有许多与我们实现的方式相关的基本假设,这些假设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)

通过使用 aMap Title Film来表示我们的数据库,我们还可以保证电影的唯一性(aMap将键Title设为零或一Film,我们不能有多个匹配项)。这里的缺点是我们可能会使TitleinDatabase键与TitleinFilm类型本身不同步。

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) })将“纯”更新步骤提升MaybeFunctor.


我们能做得更好吗?当然可以,但这里会让人感到困惑。在大多数情况下,上一个答案可能是您最好的选择。但是,让我们继续前进吧。

我们可以使用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

通过这种结构,我们甚至可以立即将整个过程升级到Statemonad。

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

于 2013-04-04T17:34:20.007 回答
1

这是另一种解决方法。

becomeFan :: Title -> Fan -> Database -> Database
becomeFan title newFan = map f where
    f film @ (title', cast, year, fans)
        | title == title' = --TODO
        | otherwise       = --TODO
于 2013-04-04T14:38:43.643 回答
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

希望这足以让您入门,如果您有任何问题,请告诉我。

于 2013-04-04T14:13:31.283 回答