Как мне обработать результат Maybe в at в Control.Lens.Indexed без экземпляра Monoid
Недавно я обнаружил пакет линз на 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
Поскольку на комнаты, проигрыватели и подобные объекты ссылаются по всему коду, я сохраняю их в своем типе состояния мира как карты идентификаторов (новых типов UUID) для их объектов данных.
Чтобы получить те, у кого есть линзы, мне нужно как-то обработать значение Maybe, возвращаемое объективом at (если ключа нет на карте, это Nothing). В моей последней строке я попытался сделать это с помощью traverse, который выполняет проверку типов, пока конечный результат является экземпляром 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 требуется для traverse только потому, что traverse обобщается на контейнеры размеров больше единицы, я теперь задавался вопросом, есть ли лучший способ справиться с этим, который не требует семантически бессмысленных экземпляров Monoid для всех типов, возможно содержащихся в одном из моих объектов, которые я хочу хранить на карте.
Или, может быть, я неправильно понял проблему здесь, и мне нужно использовать совсем другой кусочек довольно большого пакета объективов?
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
,
Краткий ответ: упаковка объектива не волшебная.
Не сообщая мне, что это за ошибка или по умолчанию, вы хотите сделать:
viewPlayerLocation:: World -> PlayerId -> RoomId
Вы знаете две вещи, которые
Чтобы получить те, у кого есть линзы, мне нужно разобраться с вернувшимся объективом.
а также
traverse, который выполняет проверку типов до тех пор, пока конечный результат является экземпляром Monoid
С Monoid
ты получаешь mempty :: Monoid m => m
по умолчанию, когда поиск не удается.
Что может потерпеть неудачу: PlayerId
не может быть в _worldPlayers
и _playerLocation
не может быть в _worldRooms
,
Итак, что должен делать ваш код в случае сбоя поиска? Это "невозможно"? Если так, то используйте fromMaybe (error "impossible") :: Maybe a -> a
врезаться
Если поиск может потерпеть неудачу, то есть ли вменяемый дефолт? Возможно вернуть Maybe RoomId
и пусть звонящий решит?