Reputation: 143
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
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
Reputation: 531878
Don't use !!
. There are really only two error conditions:
n
is negativen
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
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