Is there a better way to bind Aff + Either?

Hi,

First of all I’m super noob with PureScript (fp in general).

My terrible looking function

I was hacking away an Expressjs app without worrying to much on how it looked like as long as I could get going, this is the function:

createListing :: Handler
createListing = do
  env <- liftEffect $ getConfig
  body <- getBody >>= runExcept >>> pure
  maybeToken :: Maybe Admin.DecodedIdToken <- getUserData "idToken"
  case body, env, maybeToken of
    Left e, _, _ -> do
      logShow e
      setStatus 400
      sendJson { error: "the body is malformed" }
    Right _, Left e, _ -> do
      logShow e
      setStatus 500
      sendJson { error: "something happend 1" }
    Right _, Right _, Nothing -> do
      logShow "no token"
      setStatus 500
      sendJson { error: "something happend 2" }
    Right (a :: CreateListingForm), Right config, Just token -> do
      logShow a
      eitherSummoner <- liftAff $ RS.summonersByName config a.summonerName
      case eitherSummoner of
        Left err -> do
          log $ "1 GET /api response failed to decode: " <> show err
          setStatus 500
          sendJson { error: "something happend 3" }
        Right summoner -> do
          result <- liftAff $ getLeagueEntriesForSummoner config summoner.id
          case result of
            Right leagueEntries ->
              do
                leagueEntries
                # find
                    ( case _ of
                        LOL entry ->
                          entry.queueType == "RANKED_SOLO_5x5"
                        _ -> false
                    )
                # case _ of
                    Just (LOL leagueEntry) -> do
                      isRegistered <- liftAff $ summonerExists summoner
                      logShow isRegistered
                      if isRegistered then do
                        setStatus 403
                        sendJson { error: "summoner already has an entry" }
                      else do
                        id <- liftEffect UUID.genUUID
                        let
                          entry =
                            { id: id
                            , userUUID: token.uid
                            , summonerName: summoner.name
                            , queueType: a.queueType
                            , tier: leagueEntry.tier
                            , rank: leagueEntry.rank
                            -- , lanes: []
                            -- , note: ""
                            , profileIconId: summoner.profileIconId
                            , wins: leagueEntry.wins
                            , losses: leagueEntry.losses
                            }
                        resp <- liftAff $ try $ writeEntry entry
                        case resp of
                          Left e -> do
                            logShow e
                            setStatus 500
                            sendJson { error: "something happend 5" }
                          Right _ -> do
                            -- log $ "GET /api response: " <> stringify response.body
                            sendJson entry
                    _ -> do
                      -- TODO: this is just an unranked player or not ranked in that particular thing
                      log $ "no stats for queue type" <> show leagueEntries
                      setStatus 500
                      sendJson { error: "something happend 4" }
            Left err -> do
              log $ "2 GET /api response failed to decode: " <> show err
              logShow summoner
              setStatus 500
              sendJson { error: "something happend 6" }

But the nesting is terrible looking, and the match expression is quite repetitive.

I tried to use Aff Either and stay in the Aff as long as possible before coming up to the Handler again.

After A LOT of refactoring (for the purpose of :sparkles:learning :sparkles:) I came up with this:

Refactor N1


maybeToken
  # note NoToken
  >>=
    ( \token ->
        env
          # bimap (EnvError) (\env' -> { token, env' })
    )
  >>=
    ( \prev ->
        body
          # bimap MalformedBody (\body' -> merge prev { body' })
    )
  # pure
  >>= case _ of
    Right prev@{ env', body' } ->
      RS.summonersByName env' body'.summonerName
        >>=
          bimap
            SummonersByNameError
            (\summoner -> merge prev { summoner })
            >>> pure
    Left x -> pure $ Left x
  >>= case _ of
    Right prev@{ env', summoner } ->
      getLeagueEntriesForSummoner env' summoner.id
        >>=
          bimap
            GetLeagueEntriesForSummonerError
            (\leagueEntries -> merge prev { leagueEntries })
            >>> pure
    Left x -> pure $ Left x
  >>= case _ of
    Right prev@{ leagueEntries } ->
      leagueEntries
        # find
            ( case _ of
                LOL entry ->
                  entry.queueType == "RANKED_SOLO_5x5"
                _ -> false
            )
        # case _ of
            Just (LOL leagueEntry) ->
              Right $ merge prev { leagueEntry }
            _ -> Left Unranked
        # pure
    Left x -> pure $ Left x
  >>= case _ of
    Right prev@{ token, body', leagueEntry, summoner } ->
      summonerExists summoner
        >>= \isRegistered -> do
          id <- liftEffect UUID.genUUID
          pure $
            if isRegistered then Left SummonerExists
            else
              { entry:
                  { id: id
                  , userUUID: token.uid
                  , summonerName: summoner.name
                  , queueType: body'.queueType
                  , tier: leagueEntry.tier
                  , rank: leagueEntry.rank
                  , profileIconId: summoner.profileIconId
                  , wins: leagueEntry.wins
                  , losses: leagueEntry.losses
                  }
              }
                # merge prev
                # Right
    Left x -> pure $ Left x
  >>= case _ of
    Right prev@{ entry } ->
      do
        try $ writeEntry entry
        >>=
          case _ of
            Left e -> Left (ErrorWriting (show e))
            Right _ -> Right prev
            >>> pure
    Left x -> pure $ Left x
  # liftAff
  >>= case _ of
    Left err@NoToken -> do
      logShow err
      setStatus 401
      sendJson { error: "the token is malformed" }
    Left err@(EnvError _) -> do
      logShow err
      setStatus 500
      sendJson { error: "something happend 1" }
    Left err@(MalformedBody _) -> do
      logShow err
      setStatus 400
      sendJson { error: "the body is malformed" }
    Left err@(SummonersByNameError _) -> do
      logShow err
      setStatus 500
      sendJson { error: "something happend 3" }
    Left err@(GetLeagueEntriesForSummonerError _) -> do
      logShow err
      setStatus 500
      sendJson { error: "something happend 6" }
    Left err@(Unranked) -> do
      -- TODO: this is just an unranked player or not ranked in that particular thing
      logShow err
      setStatus 500
      sendJson { error: "something happend 4" }
    Left err@(SummonerExists) -> do
      logShow err
      setStatus 500
      sendJson { error: "summoner already has an entry" }
    Left err@(ErrorWriting _) -> do
      logShow err
      setStatus 500
      sendJson { error: "something happend 6" }
    Right { entry } -> do
      sendJson entry

It is very intimidating, and the left side is very repetitive

I struggled with Aff(Either a b) having to case _ of in each step, looking for something like bindBind for a -> Aff(Either b c) -> Aff(Either b a) -> Aff(Either b c) but couldn’t figure it out. So I jumped to ExceptT as >>= seems to be closer to what I was looking for.

I refactored inline to ExceptT and then decided to try to use where to code split functions and finally arrived at this:

createListing :: Handler
createListing =
  do
    env <- liftEffect $ getConfig
    (body :: Either MultipleErrors CreateListingForm) <- getBody >>= runExcept >>> pure
    (token :: Maybe Admin.DecodedIdToken) <- getUserData "idToken"

    tokenOrErr token
      >>= envOrErr env
      >>= bodyOrErr body
      >>= summonerOrErr
      >>= leagueEntriesOrErr
      >>= findRankedSoloOrErr
      >>= entryOrErr
      >>= writeEntryOrErr
      # runExceptT
      # liftAff
      >>= case _ of
        Left (code /\ err) -> do
          setStatus code
          sendJson { error: err }
        Right { entry } -> do
          sendJson entry
  where
  tokenOrErr :: Maybe Admin.DecodedIdToken -> ExceptT CreateListingError Aff { token' :: Admin.DecodedIdToken }
  tokenOrErr = note (401 /\ "the token is malformed") >>> map (\token' -> { token' }) >>> except

  envOrErr
    :: (Either JsonDecodeError Config)
    -> { token' :: Admin.DecodedIdToken
       }
    -> ExceptT
         CreateListingError
         Aff
         { token' :: Admin.DecodedIdToken
         , env' :: Config
         }
  envOrErr env prev = env
    # bimap (const (500 /\ "something happend 1")) (\env' -> merge prev { env' })
    # except

  bodyOrErr
    :: (Either MultipleErrors CreateListingForm)
    -> { token' :: Admin.DecodedIdToken
       , env' :: Config
       }
    -> ExceptT
         CreateListingError
         Aff
         { token' :: Admin.DecodedIdToken
         , env' :: Config
         , body' :: CreateListingForm
         }
  bodyOrErr body prev = body
    # bimap (const (400 /\ "the body is malformed")) (\body' -> (merge prev { body' }))
    # except

  summonerOrErr
    :: { token' :: Admin.DecodedIdToken
       , env' :: Config
       , body' :: CreateListingForm
       }
    -> ExceptT
         CreateListingError
         Aff
         { token' :: Admin.DecodedIdToken
         , env' :: Config
         , body' :: CreateListingForm
         , summoner :: SummonerDTO
         }
  summonerOrErr prev =
    RS.summonersByName prev.env' prev.body'.summonerName
      # lift
      >>=
        bimap
          (const (500 /\ "something happend 3"))
          (\summoner -> merge prev { summoner })
          >>> except

  leagueEntriesOrErr
    :: { token' :: Admin.DecodedIdToken
       , env' :: Config
       , body' :: CreateListingForm
       , summoner :: SummonerDTO
       }
    -> ExceptT
         CreateListingError
         Aff
         { token' :: Admin.DecodedIdToken
         , env' :: Config
         , body' :: CreateListingForm
         , summoner :: SummonerDTO
         , leagueEntries :: Array LeagueEntryDTO
         }
  leagueEntriesOrErr prev =
    getLeagueEntriesForSummoner prev.env' prev.summoner.id
      # lift
      >>=
        bimap
          (const $ 500 /\ "something happend 6")
          (\leagueEntries -> merge prev { leagueEntries })
          >>> except

  findRankedSoloOrErr
    :: { token' :: Admin.DecodedIdToken
       , env' :: Config
       , body' :: CreateListingForm
       , summoner :: SummonerDTO
       , leagueEntries :: Array LeagueEntryDTO
       }
    -> ExceptT
         CreateListingError
         Aff
         { token' :: Admin.DecodedIdToken
         , env' :: Config
         , body' :: CreateListingForm
         , summoner :: SummonerDTO
         , leagueEntries :: Array LeagueEntryDTO
         , leagueEntry :: LeagueEntryDTOLOL
         }
  findRankedSoloOrErr prev =
    prev.leagueEntries
      # find
          ( case _ of
              LOL entry ->
                entry.queueType == "RANKED_SOLO_5x5"
              _ -> false
          )
      # case _ of
          Just (LOL leagueEntry) ->
            Right $ merge prev { leagueEntry }
          _ -> Left (500 /\ "player is unranked")
      # except

  entryOrErr
    :: { token' :: Admin.DecodedIdToken
       , body' :: CreateListingForm
       , summoner :: SummonerDTO
       , leagueEntry :: LeagueEntryDTOLOL
       , leagueEntries :: Array LeagueEntryDTO
       , env' :: Config
       }
    -> ExceptT
         CreateListingError
         Aff
         { token' :: Admin.DecodedIdToken
         , env' :: Config
         , body' :: CreateListingForm
         , summoner :: SummonerDTO
         , leagueEntries :: Array LeagueEntryDTO
         , leagueEntry :: LeagueEntryDTOLOL
         , entry :: Entry
         }
  entryOrErr prev =
    summonerExists prev.summoner
      # lift
      >>= \isRegistered -> do
        id <- liftEffect UUID.genUUID
        except $
          if isRegistered then Left (500 /\ "summoner already has an entry")
          else
            { entry:
                { id: id
                , userUUID: prev.token'.uid
                , summonerName: prev.summoner.name
                , queueType: prev.body'.queueType
                , tier: prev.leagueEntry.tier
                , rank: prev.leagueEntry.rank
                , profileIconId: prev.summoner.profileIconId
                , wins: prev.leagueEntry.wins
                , losses: prev.leagueEntry.losses
                }
            }
              # merge prev
              # Right

  writeEntryOrErr
    :: forall r
     . { entry :: Entry
       | r
       }
    -> ExceptT
         CreateListingError
         Aff
         { entry :: Entry
         | r
         }
  writeEntryOrErr prev =
    prev.entry
      # writeEntry
      # try
      # lift
      >>=
        case _ of
          Left _ -> Left (500 /\ "something happend 6")
          Right _ -> Right prev
          >>> except

I struggled a lot to make the main part concise

createListing :: Handler
createListing =
  do
    env <- liftEffect $ getConfig
    (body :: Either MultipleErrors CreateListingForm) <- getBody >>= runExcept >>> pure
    (token :: Maybe Admin.DecodedIdToken) <- getUserData "idToken"

    tokenOrErr token
      >>= envOrErr env
      >>= bodyOrErr body
      >>= summonerOrErr
      >>= leagueEntriesOrErr
      >>= findRankedSoloOrErr
      >>= entryOrErr
      >>= writeEntryOrErr
      # runExceptT
      # liftAff
      >>= case _ of
        Left (code /\ err) -> do
          setStatus code
          sendJson { error: err }
        Right { entry } -> do
          sendJson entry

Probably can be a lot better.

One of the remaining issues I have with the code is the size and repetition of these signatures:

  entryOrErr
    :: { token' :: Admin.DecodedIdToken
       , body' :: CreateListingForm
       , summoner :: SummonerDTO
       , leagueEntry :: LeagueEntryDTOLOL
       , leagueEntries :: Array LeagueEntryDTO
       , env' :: Config
       }
    -> ExceptT
         CreateListingError
         Aff
         { token' :: Admin.DecodedIdToken
         , env' :: Config
         , body' :: CreateListingForm
         , summoner :: SummonerDTO
         , leagueEntries :: Array LeagueEntryDTO
         , leagueEntry :: LeagueEntryDTOLOL
         , entry :: Entry
         }

wish I could do something like:

  writeEntryOrErr
    :: forall r
     . { entry :: Entry
       | r
       }
    -> ExceptT
         CreateListingError
         Aff
         { entry :: Entry
         | r
         }

And restructure a row type for only the values I need at each step, but I use Record(merge) to return more data in all steps but the last one, so the types get a bit complicated, I have to use Union and Nub in the signature so merge doesn’t return an error. (However, if I just write in-line functions the type inference works perfectly).

Other Solutions: Writer / Reader

Another Solution I could think of is to use a Writer/Reader instead of passing a new record all the time, but that would make all the functions “fragile” I think, if they are not placed exactly right, types cannot say “you’re missing a value” and just go left.

What I can say about the last code

  • types are long
  • types are repetitive
  • overall code is much more than the original messy version.

Is there a way to improve this code? I bet there is a myriad of things to do “better” but as I said, I’m not that savvy with PS/FP.

Thank you for your help :yellow_heart:

1 Like

Perhaps you could elaborate on what you’re trying to achieve with this refactoring exercise, so people can better suggest technical approaches to that.

1 Like

Sorry for not being clear :blossom:

TL;DR

  1. Was my use of ExeptT Aff right (could I have used it better)?
  2. Was my decision of ExceptT Aff good (could I use a better tool for flat chained async-throwing-effects)?
  3. Is there a better way to accumulate results (merge previous results with current results in a step and pass them to the next step)?
  4. Is there a better way to type the steps of the final refactor (they increase the final code to 3 times the original)?
  5. Overall, is there a better way to refactor the first version?

More context:

This is a request handler for an HTTP request that calls third party APIs, merges some results and writes to a database. My goal is to return the data that was written in the database or map any error.

The first version is very nested, so i tried to flatten the flow.

However, while doing so the code got significantly longer. Mostly due to the long/repetitive function signatures.

In case I’m still being vague, maybe a more concrete question would be: what are some structures that help write nice looking chained async-effect computations that might throw errors?

I’m open to any suggestion, I went from nested Maybe/Either matching to a flat ExceptT Aff, but maybe theres a better way to do these kind of things?

I’m also thinking, maybe I shouldn’t use a record to accumulate results, maybe State or Writer/Reader is better? But i haven’t used them before so I’m not sure of the result.

Or maybe my use of a record to accumulate results between steps was poor and there’s a better way to use it?

Ok it seems I get a bit better do-notation on and came up with this version

createListing :: Handler
createListing =
  do
    env <- liftEffect $ getConfig
    (body :: Either MultipleErrors CreateListingForm) <- getBody >>= runExcept >>> pure
    (token :: Maybe Admin.DecodedIdToken) <- getUserData "idToken"

    createListing' env body token
      # runExceptT
      # liftAff
      >>= case _ of
        Left (code /\ err /\ toLog) -> do
          traverse_ log toLog
          setStatus code
          sendJson { error: err }
        Right entry -> do
          sendJson entry
  where
  createListing'
    :: Either JsonDecodeError Config
    -> Either MultipleErrors CreateListingForm
    -> Maybe Admin.DecodedIdToken
    -> ExceptT CreateListingError Aff Entry
  createListing' env body token = do
    token' <- token # note (401 /\ "the token is malformed" /\ Nothing) # except

    env' <- env # lmap (\err -> (500 /\ "something happend 1" /\ Just (show err))) # except

    body' <- body # lmap (const (400 /\ "the body is malformed" /\ Nothing)) # except

    summoner <- ExceptT (RS.summonersByName env' body'.summonerName)
      # withExceptT (\err -> (500 /\ "something happend 3" /\ Just (show err)))

    leagueEntries <- ExceptT (getLeagueEntriesForSummoner env' summoner.id)
      # withExceptT (\err -> (500 /\ "something happend 4" /\ Just (show err)))

    leagueEntry <- leagueEntries
      # find
          ( case _ of
              LOL entry -> entry.queueType == "RANKED_SOLO_5x5"
              _ -> false
          )
      # case _ of
          Just (LOL leagueEntry) -> Right leagueEntry
          _ -> Left (500 /\ "player is unranked" /\ Nothing)
      # except

    entry <- lift (summonerExists summoner)
      >>= \isRegistered -> do
        id <- liftEffect UUID.genUUID
        except $
          if isRegistered then Left (500 /\ "summoner already has an entry" /\ Nothing)
          else
            Right
              { id: id
              , userUUID: token'.uid
              , summonerName: summoner.name
              , queueType: body'.queueType
              , tier: leagueEntry.tier
              , rank: leagueEntry.rank
              , profileIconId: summoner.profileIconId
              , wins: leagueEntry.wins
              , losses: leagueEntry.losses
              }

    entry
      # writeEntry >>> try >>> ExceptT
      # withExceptT (\err -> (500 /\ "something happend 5" /\ Just (show err)))
      # void
    pure $ entry

I think it’s quite ok, could something be improved?

Not knowing any specifics of your system, it’s hard to give sweeping generalizations. Your questions boil down to whom needs to read/receive exceptions, and IMO it’s not obvious that a user of an API should receive internal errors such as due to token decoding in the form of HTTP error codes.
If indeed your user needs to receive all of that, then your code above looks just fine.

Alternatives include :

  • one big sum type with all the error conditions (rather than separate JsonDecodeError, CreateListingError etc.). At the end of the day these are generated sequentially, so you only need to crash with the first one that occurs.
  • re. usage of records. : since I write primarily Haskell, to me a record is a “closed” product of fields. You mostly generate record values and pass them around as a whole. In purescript we can decorate records with additional fields, which is an interesting solution, but the downside is that only record values at the end of the chain are meaningful.
2 Likes

Yeah, this does seems like a good place to use ExceptT! Regarding the type signatures, you can use type synonyms to cut down on the repetition:

https://github.com/purescript/documentation/blob/master/language/Types.md#type-synonyms

type Data =
  { token' :: Admin.DecodedIdToken
  , body' :: CreateListingForm
  , summoner :: SummonerDTO
  , leagueEntry :: LeagueEntryDTOLOL
  , leagueEntries :: Array LeagueEntryDTO
  , env' :: Config
  }

entryOrErr :: Data -> ExceptT CreateListingError Aff Data

You can also use the + operator to split up the rows into reusable parts that you can combine later:

https://pursuit.purescript.org/packages/purescript-typelevel-prelude/6.0.0/docs/Type.Row#t:type%20(+)

Then you could have something like

type Data1 r = ( token' :: Admin.DecodedIdToken | r )
type Data2 r = Data1 + ( env' :: Config | r )
type Data3 r = Data2 + ( body' :: CreateListingForm | r )
…

envOrErr :: Either JsonDecodeError Config -> Record (Data1 + ()) -> ExceptT CreateListingError Aff (Record (Data2 + ()))
3 Likes

WOOOOW that’s amazing, I didn’t know about (+)!

I decided to go with do notation which seem quite nice once I started with an ExceptT block instead of an Aff block, so I ditched the Record but I can deff see the use of (+), I use TypeScript a lot, so it is like intersection types (&).

I also discovered I actually didn’t need to type all those functions in the where block! at least the compiler/IDE only warns for root-level functions without signatures (or if it can’t deduct types), so I could actually cut a lot of lines.

Thank you so much for your help :hugs:

1 Like

Oh, thanks a lot for the suggestions!

Errors like JsonDecodeError are just logged, not returned to the client, I just give a 500 + something happened + code.

However, indeed, I’m also debating which errors should be facing the user, like body malformed as a user error and which is a server error (like decoding the token). I think token malformed is indeed a bad error in my case because the authentication and verification of the token happen before the request even hits the handler, so if there’s an error with the token at this point, the error should be an internal server error (meaning something in my server is not correct, i is not the user’s fault).

I’m still not sure how to properly log things because I haven’t found any logger that returns things like StackTrace to know where the error happened, so I’m just “logging” away. Still, I wish to find a more robust logging solution that produces good logs to debug the app in production when an actual incident happens.

Thanks a lot for your kind suggestions and for taking the time to read my mess :hugs:

As a solo-developer learning PS, I’m looking for some peer review of my use of the language/syntax; thanks to Pursuit, I could refactor the original code to a more concise version with better constructors and lifts, my original refactor was a mess and the one following it was full of magical stuff making it hard to read, I produced a final one that I think is quite much better.

(it also does a few more things in both logic and logging)

type CreateListingError = T3 Int String (Maybe String)

mkInternalServerError0 :: String -> CreateListingError
mkInternalServerError0 place = 500 /\ "Internal Server Error" /\ Just ("Error " <> place)

mkInternalServerError1 :: forall e. Show e => String -> e -> CreateListingError
mkInternalServerError1 place e = mkInternalServerError0 (place <> ": " <> show e)

mkBadRequest :: CreateListingError
mkBadRequest = 400 /\ "Bad Request" /\ Nothing

mkConflict :: String -> CreateListingError
mkConflict place = 409 /\ ("Conflict - " <> place) /\ Nothing

mkDataNotFound :: String -> CreateListingError
mkDataNotFound place = 404 /\ ("Data not found - " <> place) /\ Nothing

createListing :: Handler
createListing =
  do
    env <- getConfig
    body <- getBody
    token <- getUserData "idToken"

    createListing' env body token
      # runExceptT
      # liftAff
      >>= case _ of
        Left (code /\ err /\ toLog) -> do
          log' toLog
          setStatus code
          sendJson err
        Right entry -> do
          sendJson entry
  where
  log' :: forall m. MonadEffect m => Maybe String -> m Unit
  log' = traverse_ (\e -> log ("Error in createListing: " <> e))

  createListing'
    :: Either JsonDecodeError Config
    -> ExceptT (NonEmptyList ForeignError) Identity CreateListingForm
    -> Maybe Admin.DecodedIdToken
    -> ExceptT CreateListingError Aff Entry
  createListing' env body token = do
    token' <- exceptNoteM token $ mkInternalServerError0 "reading token"

    env' <- env # lmap (mkInternalServerError1 "reading env") # except

    body' <- body # mapExceptT
      ( unwrap
          >>> lmap (const mkBadRequest)
          >>> pure
      )

    isRegistered <- userHasRegitry' token'
      # withExceptT (mkInternalServerError1 "userHasRegitry")

    when isRegistered (throwError (mkConflict "User already has an entry"))

    summoner <- summonersByName' env' body'.summonerName
      # withExceptT
          ( case _ of
              SummonerNotFound -> mkDataNotFound "Summoner not found"
              e -> mkInternalServerError1 "summonersByName" e
          )

    isSummonerInBoard <- summonerExists' summoner
      # withExceptT (mkInternalServerError1 "summonerExists")

    when isSummonerInBoard (throwError (mkConflict "Summoner already has an entry"))

    leagueEntries <- getLeagueEntriesForSummoner' env' summoner.id
      # withExceptT (mkInternalServerError1 "getLeagueEntriesForSummoner")

    leagueEntry <- leagueEntries
      # find
          ( case _ of
              LOL entry -> entry.queueType == "RANKED_SOLO_5x5"
              _ -> false
          )
      # case _ of
          Just (LOL leagueEntry) -> pure leagueEntry
          _ -> throwError (mkConflict "Player is unranked")

    id <- genUUID

    let
      entry =
        { id: id
        , userUUID: token'.uid
        , summonerName: summoner.name
        , queueType: body'.queueType
        , tier: leagueEntry.tier
        , rank: leagueEntry.rank
        , profileIconId: summoner.profileIconId
        , wins: leagueEntry.wins
        , losses: leagueEntry.losses
        }

    writeEntry' entry
      # withExceptT (mkInternalServerError1 "writeEntry")
      # void
    pure $ entry

  userHasRegitry' = userHasRegitry >>> try >>> ExceptT
  getLeagueEntriesForSummoner' = getLeagueEntriesForSummoner ..> ExceptT
  summonersByName' = RS.summonersByName ..> ExceptT
  summonerExists' = summonerExists >>> try >>> ExceptT
  genUUID = liftEffect UUID.genUUID
  writeEntry' = writeEntry >>> try >>> ExceptT