Комплексные структуры данных в 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?

Ответ 1

Поскольку ссылок нет, только значения в 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 [email protected](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 [email protected](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 с соответствующим экземпляром.

Ответ 2

имеют много местоположений, каждое место может содержать ровно один элемент. Каждый элемент может располагаться в одном месте. У местоположений и предметов есть как имя, так и дополнительная информация (я оставляю их здесь).

Таким образом, элементы могут принадлежать только одному местоположению, а местоположение содержит ровно один элемент. поэтому в основном у вас есть отношения "один к одному".

Итак, если в местоположении есть пять атрибутов, а элементы имеют три, ваш объединенный элемент местоположения имеет восемь элементов.

Но я подозреваю, что структура данных, которую вы действительно хотите, не такая тривиальная, как то, что вы описали... с большим количеством данных от вас я мог бы дать лучший ответ.

Ответ 3

Я бы сказал, что ваш подход LocItems находится по правильной линии. Если я понимаю вас правильно, вы хотите:

  • чтобы иметь возможность получить элемент в определенном месте (если есть)
  • чтобы получить местоположение элемента.
  • Только один элемент в местоположении и одно местоположение для элемента.

Ну, ваше требование 1 соответствует следующей тиковой сигнатуре:

Location -> Maybe Item

Так что функция или карта. Или функция, сделанная из Карты, например:

type ItemLocations = Map Location Item
lookupItem :: ItemsByLocation -> Location -> Maybe Item

Наличие дополнительного параметра на самом деле не проблема, и есть несколько способов заставить его уйти. Например, если у вас есть позиции позиций на карте под названием itemsByLocation, то при частичном применении lookupItem вы получите нужную функцию.

let lookupItemInMyMap = lookupItem itemsByLocation

Для вашего второго требования, получая местоположение от предмета, я бы, вероятно, использовал другую карту. Карты обеспечивают третье требование.

Там одна вещь пропала, хотя: личность. В ООП объекты имеют идентификатор (адрес памяти), но значения Haskell этого не делают. Вам нужно будет привязать карту от элемента к местоположению с помощью идентификатора элемента, а не самого элемента. Таким образом, ваша вторая карта имеет следующий тип:

type ItemID = String
type LocationsByItem = Map ItemID Location

Ответ 4

В Haskell все объекты неизменяемы, поэтому вы не можете отличить ссылку на объект от копии объекта (и нет необходимости).

В (императивном) OO вам нужны ссылки, потому что вы хотите поделиться чем-то изменяемым.

Вам нужно отучить эту "оптимизацию" обмена.

Есть способы имитировать в Haskell, но в основном это не нужно, и есть совершенно ясное неизменное решение.

Например, "местоположение содержит один элемент", который моделируется Data.Map.Map Location Item. Но если ваше местоположение позже содержит другой элемент, вам понадобится другая карта.

Ответ 5

В чем главное? Возможно, пара местоположений и элементов.

type Location = String
type Item = String
type Place = (Location, Item)

myLocation :: Place -> Location
myLocation = fst

myItem :: Place -> Item
myItem = snd

И используйте его:

> myItem ("MyLoc", "MyItem")
"MyItem"