假设我有一个记录,例如Person
,我希望能够通过多种数据结构查看此人。也许有一个名字的索引,一个人的邮政编码的另一个索引,以及该人当前纬度和经度的另一个索引。也许还有更多的数据结构。所有这些都存在,因为我需要有效地查找一个或多个具有不同标准的人。
如果我只需要读取一个人的属性,this is no problem。但现在假设我需要使用其中一个数据结构查找一个人,然后更新该人的数据。
在OOP语言中,每个数据结构都指向内存中的同一个人。因此,当您更新一个时,您也隐式更新其他数据结构的引用。这几乎是副作用和杂质的定义。我知道这完全违背了Haskell范式,我并不期望Haskell以这种方式工作。
那么,什么是Haskell-ish方法呢?要清楚,问题是这样的:我通过一个数据结构查找一个人,然后将该人(可能还有一些其他任意数据)传递给ArbitraryData -> Person -> Person
类型的函数。如何在所有各种查找结构中传播此更改?
作为Haskell的相对新人,我的第一直觉是每次更新一个人时,都会使用新更新的人重建每个查找结构。但这似乎很多仪式,我有充足的机会搞砸了GHC无法察觉的方式,而且一点也不优雅。 Haskell以其优雅而闻名,我无法想象它缺乏这种常见和基本问题的优雅解决方案。所以我想我错过了什么。
作为参考,这个问题扩展了我在以下问题中讨论的一些问题:
Multiple lookup structures for same data: Memory duplication?
Identity of simulation objects in Haskell
修改
我想到的一个解决方案:不要在主状态中维护每个查找结构的副本。只保留一个存在的所有人的列表,这是我们更新人员时唯一需要更新的事项。每次你需要通过邮政编码进行查询时,将所有人员的列表传递给一个生成有效的by-zip-code数据结构的函数。然后对结果执行查找。
我不知道这是否有效。如果它导致CPU在每次使用时实际重新计算查找结构,则这是不可接受的。但我知道Haskell有时可以避免重新评估相同的表达式。不幸的是,我仍然没有想到何时就是这种情况。所以我不知道这种方法是否可行。
换句话说:我可以编写我的函数,好像他们每次都在计算查询,实际上GHC会在基础数据没有改变的情况下优化它吗?因为那是我上面提到的问题的非常优雅的解决方案。
答案 0 :(得分:5)
自从我回答这个问题以来,Freenode上#haskell的一些人推荐了替代的预制解决方案:
Data.IxSet
。Data.Store
,据说可提供镜片。您可以创建包含查找表的数据结构,以及实际Person
的{{3}}。查找表将为您提供Int
或Int
的列表(而不是Person
或Person
的列表),这是{{1}的索引1}}。例如:
Vector Person
data PersonStuff = PersonStuff {
persons :: Vector Person,
firstNameLookupTable :: LookupTable Name,
...
}
data LookupTable a = LookupTable {
table :: Map a Int,
update :: Person -> Person -> Map a Int -> Map a Int
}
函数被赋予旧update
,更新后的Person
,并且仅在相关详细信息发生更改时才会更新表。通过您将编写的方便Person
函数修改Person
时,这些函数将为您更新所有查找表,并返回包含所有关联数据的新PersonStuff
。这使得纯数据结构具有快速查找功能。
您可以制作PersonStuff
之类的功能,让所有拥有名字的人,为每个人应用updatePeopleWithFirstName :: Name -> (Person -> Person) -> PersonStuff -> PersonStuff
,修改Person -> Person
中的条目,然后使用Vector
用于更新所有lookupTables。
答案 1 :(得分:3)
我可能只是使用新值更新每个查找结构。也许可以将结构分组到记录中并提供全局更新功能。
或许您可以将其中一个搜索条件指定为“主要”,并让其他查找映射中的值指向对象的“主键”,而不是指向对象值本身。但是,这会导致非主键对每次访问进行一次额外查找。
答案 2 :(得分:2)
如果你需要有效地完成它,你将不得不降级到可变数据结构,基本上是IO
monad。
在Oask中的对象之间的这些可更新引用也可以在Haskell中使用。这些是IORef
。还有它们的线程安全版本:MVar
和TVar
- 它们之间的选择取决于你的concurrerency模型。
这种在对象之间具有不同类型引用的数据结构称为Graph,它发生在我正在处理Haskell图数据库项目时。该项目已接近其首次发布。内存数据结构已经实现,持久层也是如此,剩下的就是客户端和服务器。所以请密切关注它。我会在发布时对它进行reddit。源存储库在这里:https://github.com/nikita-volkov/graph-db/,虽然我有一段时间没有推动更新,所以它有点过时了。
答案 3 :(得分:1)
Haskell试图鼓励你思考价值观,而不是实体。通过这种方式,我的意思是纯代码在大多数情况下通过将值从一种转换为另一种来构造事物,而不是修改或更新许多其他类型共享的数据。对象的平等/身份完全由其内容定义,而不是由其位置定义。但让我更具体。
“纯变异”的一般解决方案是创建一个内同态。在您的情况下,如果您有一个Directory
人,您可以使用带有签名的功能阅读一个人的数据
type Name = String
get :: Name -> Directory -> Person
并使用函数
进行修改mod :: Name -> (Person -> Person) -> (Directory -> Directory)
如果您有很多修改功能f
,g
,h
,i
,那么您可以将它们串在一起
mod i . mod h . mod g . mod f
但重要的是要意识到,在该链中创建的每个Directory
都可能独立存在并被更新/读取/修改。这就是不变性的本质 - 数据是持久性的,我们必须在修改时“手动”推送我们的数据。
那么如何将更改传播到其他结构?简而言之......你做不到。如果你正在尝试,那么你就会以非常难以完成的方式对事物进行建模。
Haskell问你“宣传”是什么意思?这些对象基于过去的数据,我们无法改变这一事实。
纯粹的不可变数据肯定存在局限性。有些算法无法转换,通常是通过在唯一名称生成器和有限Map
上重新创建“指针算术”来实现的。如果是这种情况,最好通过ST
或IO
monad开始引入不纯效果,从而可以从STRef
和IORef
容器类型中获得真正的内存突变
答案 4 :(得分:1)
“更新所有索引结构”的方法不一定是不必要的仪式,如果你将“高效查找操作的人集合”的概念建模为一个单一的东西本身,而不是一堆独立的您“手动”尝试彼此保持同步的集合。
假设您有Person
类型。然后,您有一组Person
个对象,您希望按类型Name
和Zip
对其进行索引。您可以使用Map Name Person
和Map Zip Person
之类的内容,但这并不能真正表达您的意思。您没有两组人员,一组由Name
键入,另一组由Zip
键入。您有一个人群,可以通过Name
或Zip
查找,因此您编写的代码和您使用的数据结构应该反映出来。
让我们调用集合类型People
。对于您的索引查找,您最终会得到类似findByName :: People -> Name -> Person
和findByZip :: People -> Zip -> Person
的内容。
您还拥有可以“更新”Person -> Person
条记录的Person
类型的函数。因此,您可以使用findByName
从Person
中提取People
,然后应用更新功能来获取新的Person
。怎么办?您必须构建一个新的People
,并将原来的Person
替换为新的Person
。 “更新”功能无法解决此问题,因为他们只关注Person
值,并且对People
商店一无所知(甚至可能有很多People
商店商店)。所以你需要一个像updatePeople :: Person -> Person -> People -> People
这样的函数,你最终会编写很多像这样的代码:
let p = findByName name people
p' = update p
in updatePeople p p' people
这有点像样板。看起来像updateByName :: Name -> (Person -> Person) -> People -> People
的工作。
有了这个,在OO语言中,您可以编写类似people.findByName(name).changeSomething(args)
的内容,现在可以编写updateByName name (changeSomething args) people
。没那么不同!
请注意,我还没有完全讨论 有关如何实际实现这些数据结构或操作的信息。我纯粹是在思考你拥有的概念以及对它们有意义的操作。这意味着像这样的方案无论如何实现它们;你甚至可以(可能应该?)隐藏模块障碍背后的实现细节。您可以将People
实现为多个集合的记录,将不同的内容映射到您的Person
记录,但是您可以从“外部”将其视为支持多种不同类型的集合的单个集合。查找/更新操作,而不必担心保持多个索引同步。只有在People
类型及其操作的实现中,您必须担心这一点,这使您有机会一次性地解决它,而不是必须在每次操作时正确地执行它。
你可以进一步采取这种做法。通过一些额外的假设(例如,您的Name
,Zip
和任何其他索引都在Person
/ People
的不同字段中使用相同模式实现的知识你可以使用类型类和/或模板Haskell来避免分别实现findByName
,findByZip
,findByFavouriteSpoon
等(虽然有单独的实现让你有更多机会使用不同的索引策略取决于所涉及的类型,并且可以帮助优化更新,以便例如您只需更新可能无效的索引)。您可以使用类型类和类型族来实现findBy
,它使用调用它的任何索引键的类型来确定要使用的索引,无论您是单独的实现还是单个泛型实现(尽管这意味着你不能有多个具有相同类型的索引。)
以下是我应该工作的一个例子,提供基于类型的findBy
和updateBy
操作:
{-# LANGUAGE FlexibleContexts, MultiParamTypeClasses, TypeFamilies #-}
import Data.Map (Map, (!), adjust, delete, insert)
-- sample data declarations
newtype Name = Name String
deriving (Eq, Ord, Show)
newtype Zip = Zip Int
deriving (Eq, Ord, Show)
data Person = Person
{ name :: Name
, zipCode :: Zip
}
-- you probably wouldn't export the constructor here
data People = People
{ byName :: Map Name Person
, byZip :: Map Zip Person
}
-- class for stores that can be indexed by key
class FindBy key store where
type Result key store
findBy :: key -> store -> Result key store
updateBy :: key -> (Result key store -> Result key store) -> store -> store
-- helper functions
-- this stuff would be hidden
updateIndex
:: Ord a
=> (Person -> a) -> Person -> Person -> Map a Person -> Map a Person
updateIndex f p p' = insert (f p') p' . delete (f p)
-- this function has some per-index stuff;
-- note that if you add a new index to People you get a compile error here
-- telling you to account for it
-- also note that we put the *same* person in every map; sharing should mean
-- that we're not duplicating the objects, so no wasted memory
replacePerson :: Person -> Person -> People -> People
replacePerson p p' ps = ps { byName = byName', byZip = byZip' }
where
byName' = updateIndex name p p' $ byName ps
byZip' = updateIndex zipCode p p' $ byZip ps
-- a "default" definition for updateBy in terms of findBy when the store happens
-- to be People and the result happens to be Person
updatePeopleBy
:: (FindBy key People, Result key People ~ Person)
=> key -> (Person -> Person) -> People -> People
updatePeopleBy k f ps =
let p = findBy k ps
in replacePerson p (f p) ps
-- this is basically the "declaration" of all the indexes that can be used
-- externally
instance FindBy Name People where
type Result Name People = Person
findBy n ps = byName ps ! n
updateBy = updatePeopleBy
instance FindBy Zip People where
type Result Zip People = Person
findBy z ps = byZip ps ! z
updateBy = updatePeopleBy
答案 5 :(得分:0)
Jarret,我强烈建议你用Haskell维基上记录的简单形式和Oleg Kiselyov开发的更高级的Zippers来调查generic version。引用Oleg,
Zipper是一种可更新且纯粹的功能游标,可用于数据结构。它允许我们在没有任何突变的情况下替换数据结构深处的项目,例如树或术语。结果将尽可能多地与旧结构共享其组件。旧数据结构仍然可用,如果我们希望稍后“撤消”操作,这将非常有用。
wiki页面提供了一个简单的示例,说明如何更新树的一个节点,而无需重建树的其余部分。
如果您将不同的视图包装在拉链中并使用共享密钥,您应该会看到显着的效率提升。如果您将不同的视图包装在适当的monad(例如State Monad)中,则可以通过一个操作更新位置,并查看所有不同的视图移动以指向“相同”的对象。