Как мне обработать результат Maybe для at в Control.Lens.Indexed без экземпляра Monoid

Я недавно обнаружил пакет Lens на Hackage и сейчас пытаюсь использовать его в небольшом тестовом проекте, который в один прекрасный день может превратиться в сервер MUD / MUSH, если я продолжу над ним работать.

Вот уменьшенная версия моего кода, иллюстрирующая проблему, с которой я столкнулся прямо сейчас, с линзами at, используемыми для доступа к контейнерам Key / Value (Data.Map.Strict в моем случае)

{-# LANGUAGE OverloadedStrings, GeneralizedNewtypeDeriving, TemplateHaskell #-}
module World where
import Control.Applicative ((<$>),(<*>), pure)
import Control.Lens
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as DM
import Data.Maybe
import Data.UUID
import Data.Text (Text)
import qualified Data.Text as T
import System.Random (Random, randomIO)

newtype RoomId = RoomId UUID deriving (Eq, Ord, Show, Read, Random)
newtype PlayerId = PlayerId UUID deriving (Eq, Ord, Show, Read, Random)

data Room =
  Room { _roomId :: RoomId 
       , _roomName :: Text
       , _roomDescription :: Text
       , _roomPlayers :: [PlayerId]
       } deriving (Eq, Ord, Show, Read)

makeLenses ''Room

data Player =
  Player { _playerId :: PlayerId
         , _playerDisplayName :: Text
         , _playerLocation :: RoomId
         } deriving (Eq, Ord, Show, Read)

makeLenses ''Player

data World =
  World { _worldRooms :: Map RoomId Room
        , _worldPlayers :: Map PlayerId Player
        } deriving (Eq, Ord, Show, Read)

makeLenses ''World

mkWorld :: IO World
mkWorld = do
  r1 <- Room <$> randomIO <*> (pure "The Singularity") <*> (pure "You are standing in the only place in the whole world") <*> (pure [])
  p1 <- Player <$> randomIO <*> (pure "testplayer1") <*> (pure $ r1^.roomId)
  let rooms = at (r1^.roomId) ?~ (set roomPlayers [p1^.playerId] r1) $ DM.empty
      players = at (p1^.playerId) ?~ p1 $ DM.empty in do
    return $ World rooms players

viewPlayerLocation :: World -> PlayerId -> RoomId
viewPlayerLocation world playerId=
  view (worldPlayers.at playerId.traverse.playerLocation) world  

Поскольку на комнаты, игроки и подобные объекты ссылаются во всем коде, я сохраняю их в своем типе состояния World как карты идентификаторов (UUID с новым типом) для их объектов данных.

Чтобы получить те, у кого есть линзы, мне нужно как-то обработать Maybe, возвращаемый линзой at (в случае, если ключа нет на карте, это Nothing). В моей последней строке я попытался сделать это с помощью обхода, который выполняет проверку типов, пока конечный результат является экземпляром Monoid, но обычно это не так. Здесь это не потому, что playerLocation возвращает RoomId, у которого нет экземпляра Monoid.

No instance for (Data.Monoid.Monoid RoomId)
  arising from a use of `traverse'
Possible fix:
  add an instance declaration for (Data.Monoid.Monoid RoomId)
In the first argument of `(.)', namely `traverse'
In the second argument of `(.)', namely `traverse . playerLocation'
In the second argument of `(.)', namely
  `at playerId . traverse . playerLocation'

Поскольку Monoid требуется для обхода только потому, что обход обобщается на контейнеры размером больше одного, я теперь задавался вопросом, есть ли лучший способ справиться с этим, который не требует семантически бессмысленных экземпляров Monoid для всех типов, которые могут содержаться в одном из моих объектов, которые я хочу хранить на карте.

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


person Matthias Hörmann    schedule 17.11.2012    source источник
comment
А как насчет использования моноидов First или Last из Data.Monoid?   -  person Nathan Howell    schedule 18.11.2012


Ответы (3)


Если у вас есть Traversal и вы хотите получить Maybe для первого элемента, вы можете просто использовать headOf вместо view, т.е.

viewPlayerLocation :: World -> PlayerId -> Maybe RoomId
viewPlayerLocation world playerId =
  headOf (worldPlayers.at playerId.traverse.playerLocation) world  

Инфиксная версия headOf называется ^?. Вы также можете использовать toListOf для получения списка всех элементов и других функций в зависимости от того, что вы хотите сделать. См. Документацию Control.Lens.Fold.

Быстрая эвристика, в каком модуле искать ваши функции:

  • Getter - это только одно значение, доступное только для чтения.
  • Lens - это представление чтения-записи ровно одного значения
  • Traversal - это представление чтения-записи с нулевым или большим количеством значений.
  • Fold - это доступное только для чтения представление нулевых или более значений.
  • Setter - это доступное только для записи (ну, только для изменения) представление нуля или более значений (на самом деле, возможно, бесчисленное множество значений)
  • Iso - это, в общем, изоморфизм - Lens, который может идти в любом направлении
  • Предположительно вы знаете, когда используете функцию Indexed, поэтому можете посмотреть в соответствующем модуле Indexed

Подумайте о том, что вы пытаетесь сделать, и о том, какой модуль будет самым общим для этого. :-) В этом случае у вас есть Traversal, но вы пытаетесь только просмотреть, а не изменить, поэтому функция, которую вы хотите, находится в .Fold. Если бы у вас также была гарантия, что оно ссылается ровно на одно значение, оно было бы в .Getter.

person shachaf    schedule 18.11.2012
comment
Спасибо, кажется, headOf предоставляет именно то, что я искал. И да, довольно сложно определить, какой модуль может содержать нужную мне функцию. Для этого вам пригодится ваша эвристика. - person Matthias Hörmann; 18.11.2012

Короткий ответ: упаковка линз - это не волшебство.

Не сообщая мне, что это за ошибка или ошибка по умолчанию, вы хотите сделать:

viewPlayerLocation :: World -> PlayerId -> RoomId

Вы знаете две вещи, что

Чтобы получить те, у кого есть линзы, мне нужно обработать Maybe, возвращаемый линзой at

а также

traverse, который выполняет проверку типов до тех пор, пока конечный результат является экземпляром Monoid

С Monoid вы получаете mempty :: Monoid m => m по умолчанию, когда поиск не выполняется.

Что может потерпеть неудачу: PlayerId не может быть в _worldPlayers, а _playerLocation не может быть в _worldRooms.

Итак, что должен делать ваш код, если поиск не работает? Это «невозможно»? Если это так, то используйте fromMaybe (error "impossible") :: Maybe a -> a для сбоя.

Если возможно, что поиск не удастся, то есть ли нормальное значение по умолчанию? Возможно, верните Maybe RoomId и пусть решает звонящий?

person Chris Kuklewicz    schedule 17.11.2012
comment
Да, я должен был упомянуть об этом, я бы хотел, чтобы в этом случае, если возможно, он возвращал значение Maybe RoomId, распространяя значение Maybe наружу. Для сеттера я был бы в порядке, просто ничего не делая, если запись карты с совпадающим идентификатором не существует, но было бы предпочтительнее какой-либо отчет об ошибках. - person Matthias Hörmann; 18.11.2012

Есть ^?!, который освобождает вас от звонков fromMaybe.

person nponeccop    schedule 03.12.2014