Haskell中的复杂数据结构

时间:2014-08-06 12:23:27

标签: haskell

阅读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中构建这样的数据结构呢?

5 个答案:

答案 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

现在,这可以概括。我们可以支持LocationItem,而不是ab。我们获得:

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的构造函数。 removeRightremoveLeft将确保如果您取出左值,则也会删除正确的值。请注意,在我们的情况下,使用其中一个就足够了,因为insert会对称地存储这两个值。

这要求您拥有OrdLocation的有效Item个实例,这些实例应基于其唯一属性(在本例中为其名称)。如果您恰好有OrdEq实例,且不使用该名称,请使用带有相应实例的newtype包装。

答案 1 :(得分:2)

  

有许多位置,每个位置只能容纳一个项目。每个项目都可以定位在一个位置。位置和项目都有一个名称和一些额外的信息(我把它们留在这里)。

因此,项目只能属于一个位置,而一个位置只能包含一个项目。所以基本上,你有一对一的关系。

因此,如果某个地理位置有五个属性,而一个项目有三个属性,则您的合并位置项目有八个项目。

但我怀疑你真正想要的数据结构并不像你描述的那样微不足道......随着你的更多输入我可能能够提供更好的答案。

答案 2 :(得分:2)

我说你的LocItems方法是正确的。如果我理解你,你想要:

  1. 能够在给定位置获取物品(如果有的话)
  2. 获取物品的位置。
  3. 位置只有一个项目,项目只有一个位置。
  4. 您的要求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"