阅读http://learnyouahaskell.com/之后,我仍然不明白,Haskell中是如何构建复杂的数据结构的。
一个例子:
我有很多地方,每个地点都可以容纳一个项目。每个项目都可以定位在一个位置。位置和项目都有一个名称和一些额外的信息(我把它们留在这里)。每个位置和每个项目的名称都是唯一的。
此问题出现在优化问题的上下文中,应该构建一个短传输列表。这意味着,对象只需读取一次,然后就不会被操纵。存在更多类型,它们都是通过面向对象设计中的实例字段链接在一起的。
我的第一个想法(根据我对面向对象设计的了解)将是:
data Location = Location String Item
data Item = Item String Location
由于没有引用,只有Haskell中的值,我希望这是一个无休止的递归。
data Location = Location String Item
data Item = Item String
通过这种方法,当我只有物品时,我无法获得该位置。
我的第二个想法是:
data Location = Location String
data Item = Item String
type LocItems = [(Location, Item)]
嗯,这种方式我必须给出方法,它需要一个位置的项目,两个参数:位置和地图。当它变得越来越复杂时,我最终会得到许多地图。 反对这种方法的第二个原因:类型系统不限制仅链接到另一个的位置和项目。地图(或更好的关联列表)可以将一个位置映射到多个项目。
我不明白如何在Haskell中构建这样复杂的结构。
那么如何在Haskell中构建这样的数据结构呢?
答案 0 :(得分:8)
由于没有引用,只有Haskell中的值,我希望这是一个无休止的递归。
尽管如此,还是可能的:
data Location = Location String Item
data Item = Item String Location
locationName (Location s _) = s
getItem (Location _ i) = i
itemName (Item s _) = s
getLocation (Item _ l) = l
getItemNameAtLocation :: Location -> String
getItemNameAtLocation = itemName . getItem
getLocationNameOfItem :: Item -> String
getLocationNameOfItem = locationName . getLocation
mkItemLocation :: ItemName -> LocationName -> (Item, Location)
mkItemLocation i l = let it = Item i $ Location l $ it in (it, getLocation it)
main = do
let it = Item "Toothbrush" $ Location "Bathroom" $ it
loc1 = getLocation it
loc2 = Location "Quantum bathroom" $ it
print $ getLocationNameOfItem it
print $ getItemNameAtLocation loc1
print $ getItemNameAtLocation loc2
print $ locationName loc2
但是,这并没有强制执行您的规则,因为现在有两个位置声称拥有牙刷。如果您没有导出构造函数,您仍然可以强制执行此操作:
module ItemLocation (mkItemLocation, Item, Location,
getLocation, locationName,
getItem, itemName) where
-- see above for Item, Location and others
type ItemName = String
type LocationName = String
mkItemLocation :: ItemName -> LocationName -> (Item, Location)
mkItemLocation i l = let it = Item i $ Location l $ it in (it, getLocation it)
main = do
let (it, loc) = mkItemLocation "Toothbrush" "Bathroom"
print $ getLocationNameOfItem it
print $ getItemNameAtLocation loc
但是,没有任何东西妨碍您使用mkItemLocation "Toothbrush" "Another quantum room"
。但是在这一点上,你还没有说出如何识别单个物品或位置(可能是通过名称)。
请注意,您可能希望使用data Location = Location String (Maybe Item)
。话虽如此,但您并不清楚如何操纵某个位置或项目,以及这些操作应该如何反映您所在位置的其他位置。根据您实际想要做的事情,您最终可能会将State
与两个Map
一起使用。
好的,上面只是向您展示了如何处理递归数据类型。一个人如何真正解决你的问题?让我们尝试构建一个接口:
data Magic
-- | initial empty magic
empty :: Magic
-- | turns the magic type into a list of (Location, Item)
-- every Location and Item is unique
assoc :: Magic -> [(Location, Item)]
-- | adds the given Location and Item and puts them into relation
-- If either Location or Item already exist, they're going to be
-- removed (together with their counterpart) beforehand
insert :: Location -> Item -> Magic -> Magic
现在,这可以概括。我们可以支持Location
和Item
,而不是a
和b
。我们获得:
module DualMap (DualMap, empty, assocLeft,
assocRight, flipMap, insert,
removeLeft, removeRight) where
import Data.Map (Map)
import qualified Data.Map as M
data DualMap a b = DualMap (Map a b) (Map b a) deriving (Eq, Show)
empty :: DualMap a b
empty = DualMap (M.empty) (M.empty)
flipMap :: DualMap a b -> DualMap b a
flipMap (DualMap ls rs) = DualMap rs ls
assocLeft :: DualMap a b -> [(a, b)]
assocLeft (DualMap ls _) = M.toList ls
assocRight :: DualMap a b -> [(b, a)]
assocRight = assocLeft . flipMap
insert :: (Ord a, Ord b) => a -> b -> DualMap a b -> DualMap a b
insert loc item m = DualMap (M.insert loc item ls) (M.insert item loc is)
where (DualMap ls is) = removeLeft loc m
removeLeft :: (Ord a, Ord b) => a -> DualMap a b -> DualMap a b
removeLeft l m@(DualMap ls rs) =
case M.lookup l ls of
Just r -> DualMap (M.delete l ls) (M.delete r rs)
Nothing -> m
removeRight :: (Ord a, Ord b) => b -> DualMap a b -> DualMap a b
removeRight r m@(DualMap ls rs) =
case M.lookup r rs of
Just l -> DualMap (M.delete l ls) (M.delete r rs)
Nothing -> m
请注意,您不应导出DataMap
的构造函数。 removeRight
和removeLeft
将确保如果您取出左值,则也会删除正确的值。请注意,在我们的情况下,使用其中一个就足够了,因为insert
会对称地存储这两个值。
这要求您拥有Ord
和Location
的有效Item
个实例,这些实例应基于其唯一属性(在本例中为其名称)。如果您恰好有Ord
或Eq
实例,且不使用该名称,请使用带有相应实例的newtype
包装。
答案 1 :(得分:2)
有许多位置,每个位置只能容纳一个项目。每个项目都可以定位在一个位置。位置和项目都有一个名称和一些额外的信息(我把它们留在这里)。
因此,项目只能属于一个位置,而一个位置只能包含一个项目。所以基本上,你有一对一的关系。
因此,如果某个地理位置有五个属性,而一个项目有三个属性,则您的合并位置项目有八个项目。
但我怀疑你真正想要的数据结构并不像你描述的那样微不足道......随着你的更多输入我可能能够提供更好的答案。
答案 2 :(得分:2)
我说你的LocItems
方法是正确的。如果我理解你,你想要:
您的要求1是否映射到以下类型签名:
Location -> Maybe Item
这是一个功能或地图。或者是一个由Map构成的函数,如下所示:
type ItemLocations = Map Location Item
lookupItem :: ItemsByLocation -> Location -> Maybe Item
拥有额外的参数并不是一个真正的问题,有几种方法可以让它消失。例如,如果您在名为itemsByLocation
的地图中有项目位置,那么部分应用lookupItem
会为您提供所需的功能。
let lookupItemInMyMap = lookupItem itemsByLocation
对于第二个要求,从项目中获取位置,我可能会使用另一个地图。地图强制执行您的第三个要求。
虽然缺少一件事:身份。在OOP中,对象具有身份(他们的内存地址),但Haskell值不是。您需要通过Item标识符而不是Item本身将Map从Item键入Location。所以你的第二张地图有以下类型:
type ItemID = String
type LocationsByItem = Map ItemID Location
答案 3 :(得分:1)
在Haskell中,所有对象都是不可变的,因此您无法区分对象的引用和对象的副本(并且不需要)。
在(命令式)OO中,您需要引用,因为您想要共享可变的内容。
您需要忘记这种共享“优化”。
有很多方法可以在Haskell中进行模拟,但主要是不需要的,并且有一个非常明确的不可变解决方案。
例如,“一个位置包含一个项目” - 由Data.Map.Map Location Item
建模。
但是,如果您的位置稍后拥有另一个项目,那么您需要另一个地图。
答案 4 :(得分:1)
主要是什么?也许是一对位置和物品。
type Location = String
type Item = String
type Place = (Location, Item)
myLocation :: Place -> Location
myLocation = fst
myItem :: Place -> Item
myItem = snd
并使用它:
> myItem ("MyLoc", "MyItem")
"MyItem"