2012-11-17 7 views
15

Recentemente ho scoperto il pacchetto di obiettivi su Hackage e ho cercato di utilizzarlo ora in un piccolo progetto di test che potrebbe trasformarsi in un MUD/MUSH server un giorno molto lontano se continuo a lavorarci.Come gestisco il risultato Maybe di in Control.Lens.Indexed senza un'istanza Monoid

Ecco una versione ridotta a icona del mio codice che illustra il problema che sto affrontando in questo momento con la a lenti utilizzate per accedere ai contenitori chiave/valore (Data.Map.Strict nel mio caso)

{-# 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 

Dal camere , i giocatori e gli oggetti simili sono referenziati su tutto il codice che li memorizzo nel mio stato di stato World come mappe di Id (UUID newtyped) ai loro oggetti di dati.

Per recuperare quelli con obiettivi, devo gestire il valore restituito dall'obiettivo (nel caso in cui la chiave non sia nella mappa questo è Nothing) in qualche modo. Nella mia ultima riga ho provato a farlo attraverso la traversa che fa typecheck fintanto che il risultato finale è un'istanza di Monoid ma questo non è generalmente il caso. Proprio qui non è perché playerLocation restituisce un RoomId che non ha un'istanza 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' 

Poiché la Monoide è richiesto da traverse solo perché traslazione generalizza a contenitori di dimensioni maggiori di uno ora mi chiedevo se esiste un modo migliore per gestire questa che non richiede istanze Monoide semanticamente senza senso su tutti i tipi possibilmente contenuto in uno dei miei oggetti che voglio memorizzare nella mappa.

O forse ho frainteso completamente il problema qui e ho bisogno di utilizzare un bit completamente diverso del pacchetto di lenti piuttosto grande?

+0

Che dire dell'utilizzo dei 'mono' o' ultimo' da 'Data.Monoid'? –

risposta

21

Se si dispone di un Traversal e si desidera ottenere un Maybe per il primo elemento, si può semplicemente utilizzare headOf invece di view, vale a dire

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

La versione infisso di headOf si chiama ^?. È anche possibile utilizzare toListOf per ottenere un elenco di tutti gli elementi e altre funzioni a seconda di ciò che si desidera fare. Vedere la documentazione Control.Lens.Fold.

Un'euristica veloce per quale modulo per cercare le funzioni in:

  • Un Getter è una vista in sola lettura di esattamente un valore
  • Un Lens è una vista di lettura e scrittura di esattamente un valore
  • un Traversal è una vista in lettura e scrittura di zero o più valori-
  • un Fold è una vista in sola lettura di zero o più valori-
  • A Setter è una vista di sola scrittura (bene, solo modifica) di zero o più valori (possibilmente innumerevoli valori, infatti)
  • Un Iso è, beh, un isomorfismo - un Lens che può andare in entrambe le direzioni
  • Presumibilmente si sa quando si utilizza una funzione di Indexed, in modo da poter guardare nel corrispondente modulo di Indexed

pensare a quello che si sta cercando di fare e ciò che il modulo più generale di mettere sarebbe in :-) In questo caso hai un Traversal, ma stai solo cercando di visualizzare, non modificare, quindi la funzione desiderata è .Fold. Se anche tu avessi la garanzia che si stesse riferendo esattamente a un valore, sarebbe in .Getter.

+0

Grazie, headOf sembra fornire esattamente quello che stavo cercando. E sì, è un po 'complicato capire quale modulo potrebbe contenere la funzione di cui ho bisogno. La tua euristica ti tornerà utile. –

1

Risposta breve: il pacchetto di obiettivi non è magico.

senza dirmi che cosa l'errore o difetto è, si vuole fare:

viewPlayerLocation :: World -> playerid -> RoomId

Sai due cose, che

Per recuperare quelli con obiettivi ho bisogno di gestire il Forse restituito dall'obiettivo

e

traversata che non TYPECHECK fintanto che il risultato finale è un esempio di Monoide

Con un Monoid si ottiene mempty :: Monoid m => m come predefinita quando la ricerca ha esito negativo.

Cosa può mancare: Il PlayerId non può essere nel _worldPlayers e il _playerLocation non può essere in _worldRooms.

Quindi, cosa dovrebbe fare il codice se una ricerca non riesce? Questo è "impossibile"? In tal caso, utilizzare fromMaybe (error "impossible") :: Maybe a -> a per bloccarsi.

Se è possibile che la ricerca fallisca, esiste un predefinito corretto? Forse restituire Maybe RoomId e consentire al chiamante di decidere?

+0

Sì, avrei dovuto dirlo, mi piacerebbe che restituisse un Forse RoomId se possibile in quel caso, propagando il valore di Forse verso l'esterno. Per un setter starei bene con il non fare nulla nel caso in cui la voce della mappa con l'ID corrispondente non esistesse, ma sarebbe preferibile una sorta di segnalazione degli errori. –

1

C'è ^?! che ti libera dalla chiamata fromMaybe.

Problemi correlati