Sebastian Sipos
Sebastian Sipos

Reputation: 143

How to sequence conditions when using either

So I'm building this CLI todos app to learn some Haskell and I am trying to streamline error handling (I feel that there's too much code using my current approach).

And so I've written this function to update a task at a specific position by applying a function over it:

  updateTodoAt :: Int -> (Todo -> Either Error Todo) -> [Todo] -> Either Error [Todo]
  updateTodoAt position fn todos
                  | n < 0 = Left "Must be a strict positive, bruh!"
                  | n >= length todos = Left "Out of bounds?"
                  | otherwise = fn (todos!!n)
                                >>= (\todo -> Right(take n todos ++ [todo] ++ drop (n+1) todos))

This works fine, however I feel that I'm missing something and there should be a way to write this using some *> or >> or >>=, but I can't seem to be able find the combination. I've just started grasping how to chain these together... :) The reasoning for trying to accomplish this is because this function is being called within another that does it's own pre-validations and the code already is messy.

I've tried various permutations in the lines of the next block (it's just an example), but from the first moment I felt this approach can't work because this isn't the way to "cast" from Maybe to Either.

  updateTodoAt position fn todos = fuu n < 0 "Must be a strict positive, bruh!"
                    *> fuu (n >= length todos) "Out of bounds?"
                    *> fn (todos!!n)
                    >>= (\todo -> Right(take n todos ++ [todo] ++ drop (n+1) todos)) 
                  where n = position - 1

  fuu :: Bool -> Error -> Maybe Error
  fuu True e = Just e
  fuu False _ = Nothing

My intuition tells me that there should be a function that can be chained like this.

Fyi: type Error = Text, I'm using Protolude and OverloadedStrings

The code is up at https://github.com/ssipos90/todos-hs but it's a bit 'dated. I've started working on parsing the "database" file using megaparsec, but I think error handling is more important.

Upvotes: 2

Views: 367

Answers (3)

K. A. Buhr
K. A. Buhr

Reputation: 51029

The definition of fuu that you're looking for is:

fuu :: Bool -> Error -> Either Error ()
fuu True e = Left e
fuu False _ = Right ()

Here's why... The operator (*>) has type:

(*>) :: (Applicative f) => f a -> f b -> f b

In this context, f is specialized to Either Error, so it's actually:

(*>) :: Either Error a -> Either Error b -> Either Error b

If you want to write fuu p "xxx" *> other_action, the type of the result will be the type of other_action (namely, Either Error b for some b). You need to define fuu so that the supplied fuu p "xxx" has type Either Error a for some type a, where it doesn't matter what type a is:

fuu :: Bool -> Error -> Either Error ???
fuu True err = Left err
fuu False _ = Right ???

If you find yourself in a situation where you need to supply a ??? where the value and the type don't matter, the value/type () is always a good bet:

fuu :: Bool -> Error -> Either Error ()
fuu True err = Left err
fuu False _ = Right ()

Note that it's common to write:

when (n < 0) (Left "Must be a strict positive, bruh!")

using when from Control.Monad, instead of bothering to define fuu.

Also, you may find your function looks a little more natural when rewritten in do-notation:

import Control.Monad

updateTodoAt' :: Int -> (Todo -> Either Error Todo) 
     -> [Todo] -> Either Error [Todo]
updateTodoAt' position fn todos = do
  when (n < 0) $ Left "Must be a strict positive, bruh!"
  when (n > length todos) $ Left "Out of bounds?"
  todo <- fn (todos !! n)
  return $ take n todos ++ [todo] ++ drop (n+1) todos
  where n = position - 1

Upvotes: 4

chepner
chepner

Reputation: 531878

Don't use !!. There are really only two error conditions:

  1. n is negative
  2. The input list is empty (which implies the non-negative n is too large)

Given a non-negative n and an non-empty list, you'll either apply the function to the first list element, or recurse.

updateTodoAt n _ xs
    | n < 0 = Left "negative index"
    | null xs = Left "index too large"
updateTodoAt 0 fn (x:xs) = (: xs) <$> (fn x)
updateTodoAt n fn (x:xs) = (x :) <$> updateTodoAt (n-1) fn xs

Upvotes: 0

Lee
Lee

Reputation: 144196

You only need to use fmap here:

updateTodoAt :: Int -> (Todo -> Either Error Todo) -> [Todo] -> Either Error [Todo]
updateTodoAt n fn todos
  | n < 0 = Left "Must be a strict positive, bruh!"
  | n >= length todos = Left "Out of bounds?"
  | otherwise = let (l, (todo:r)) = splitAt n todos
        in fmap (\todo -> l ++ [todo] ++ r) (fn todo)

Upvotes: 0

Related Questions