Search code examples
haskellmonadspersistent

Haskell's mapM when there are 3 monadic results


Now and again I keep running into Haskell code where I feel like I should be able to use mapM to make operating over the monads cleaner, but if I have two computations of type m (t a), and perhaps there is another operation that uses t, something like a -> t a (typically this is Maybe)... say I have this persistent code:

findDealership :: MonadIO m => Key Vehicle -> SqlPersistT m (Maybe (Entity Dealership))
findDealership vehicleKey = mapM selectDealership (getVehicleDealership vehicleKey)

getVehicleDealership :: MonadIO m => Key Vehicle -> SqlPersistT m (Maybe (Key Dealership))
getVehicleDealership vehicleKey =
  (\x -> x >>= vehicleDealershipId . entityVal) <$> getEntity vehicleKey

selectDealership :: MonadIO m => Key Dealership -> SqlPersistT m (Maybe (Entity Dealership))
selectDealership dealershipKey = selectFirst [DealershipId ==. dealershipKey] []

Where vehicleDealershipId is a field of type Maybe (Key Dealership), the above code will not compile and I'm unable to work out the right combination of >>='s and mapM's.... I feel this problem is a bit like the one mapM is solving, only there is another level of monad in there... you can end up with SqlPersistT m (Maybe (Maybe (Maybe (Key Dealership)))) but obviously I need the inner type to be completely flattened...

Error output for the above code:

    • Couldn't match type ‘Control.Monad.Trans.Reader.ReaderT
                             SqlBackend m0’
                     with ‘Maybe’
      Expected type: SqlPersistT m (Maybe (Entity Dealership))
        Actual type: Control.Monad.Trans.Reader.ReaderT
                       SqlBackend
                       m
                       (Control.Monad.Trans.Reader.ReaderT
                          SqlBackend m0 (Maybe (Entity Dealership)))
    • In the expression:
        mapM selectDealership $ getVehicleDealership vehicleKey
      In an equation for ‘findDealership’:
          findDealership vehicleKey
            = mapM selectDealership $ getVehicleDealership vehicleKey
   |
46 | findDealership vehicleKey = mapM selectDealership $ getVehicleDealership vehicleKey
   |                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

and:

    • Couldn't match type ‘Maybe (Key Dealership)’
                     with ‘Key Dealership’
      Expected type: Control.Monad.Trans.Reader.ReaderT
                       SqlBackend m0 (Key Dealership)
        Actual type: SqlPersistT m0 (Maybe (Key Dealership))
    • In the second argument of ‘($)’, namely
        ‘getVehicleDealership vehicleKey’
      In the expression:
        mapM selectDealership $ getVehicleDealership vehicleKey
      In an equation for ‘findDealership’:
          findDealership vehicleKey
            = mapM selectDealership $ getVehicleDealership vehicleKey
   |
46 | findDealership vehicleKey = mapM selectDealership $ getVehicleDealership vehicleKey
   |                                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Solution

  • What I have come to realise now, is that what I wanted here was MaybeT.

    My example can be re-written using MaybeT like so:

    findDealership :: MonadIO m => Key Vehicle -> SqlPersistT m (Maybe (Entity Dealership))
    findDealership vehicleKey = runMaybeT $ do
      dealershipKey <- MaybeT (getVehicleDealership vehicleKey) 
      selectDealership dealershipKey
    
    getVehicleDealership :: MonadIO m => Key Vehicle -> SqlPersistT m (Maybe (Key Dealership))
    getVehicleDealership vehicleKey = runMaybeT $ do
      vehicleEntity <- MaybeT (getEntity vehicleKey)
      hoistMaybe $ vehicleDealershipId (entityVal vehicleEntity)
    
    selectDealership :: MonadIO m => Key Dealership -> SqlPersistT m (Maybe (Entity Dealership))
    selectDealership dealershipKey = 
      selectFirst [DealershipId ==. dealershipKey] []