我想写的功能是让用户输入他们的名字和他们想成为粉丝的电影。
这是我正在使用的当前代码:
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"]
提前感谢所有提供的答案!!
答案 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
键中的Database
与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
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