It’s Ok. And if it’s not, Err will say why.

Better than Nothing

When we learn Result comes with more error than Maybe

Michel Belleville
Wat, the Elm-ist
Published in
4 min readMar 10, 2019

--

me> Wat. I found another type that has a map function: Result ; is it one of those Functors we talked about that time? 🤔

wat> Let’s see:

Result error value
= Ok value
| Err error
Result.map : (a -> b) -> Result error a -> Result error b

wat> Looks like a legit Functor alright (assuming it respects the laws 👮‍♀ but I trust those nice people of the Elm core team know their business there).

me> But… it’s got two type parameters 😧 all the other Functors we saw last time had only one. How does this work?

wat> Let’s inspect the constructors first. 🔎

Result error value
= Ok value
| Err error

me> Ok, so a Result error value has two type parameters (error and value) and two constructors Ok (which takes a value) and Err (which takes an error)… and it looks a bit like Maybe with one more type parameter.

wat> It does, doesn’t it? That’s because it kinda is. Remember we can use Maybe something to model cases when there can either be something (Just something) or not (Nothing), so we can use it as an output value for operations that can fail? (like converting a String into an Int, since all String are not necessarily numbers)

me> That I do. 😄

wat> It’s pretty simple to have a guess as to why the conversion from a String into an Int failed, simply by looking at the String, right?

me> Right.

wat> Now let’s say we want to be able to convert a String to a fancy Email value we just invented:

type alias Email =
{ recipient : String
, domainName : String
, domainExtension : String
}
stringToEmail : String -> Maybe Email
stringToEmail input =
... -- the implementation does not really matter here :)
stringToEmail "foo@bar.com" ==
Just
{ recipient = "foo"
, domainName = "bar"
, domainExtension = "com"
}
stringToEmail "not @an email.com" ==
Nothing
-- that's because there are spaces in the string
stringToEmail "also@notanemail" ==
Nothing
-- that's because there's no domain extension
stringToEmail "still.not.an.email" ==
Nothing
-- that's because there's no @
-- But we can't tell why it failed each time because Nothing
-- carries no information about why it failed

me> I see… there is no way to know what got wrong while trying to convert the email address.

wat> Now let’s do it with Result instead…

stringToEmail : String -> Result String Email
stringToEmail input =
... -- again, the implementation does not really matter there ;)
stringToEmail "foo@bar.com" ==
Ok
{ recipient = "foo"
, domainName = "bar"
, domainExtension = "com"
}
stringToEmail "not @an email.com" ==
Err "there are spaces in the string"
stringToEmail "also@notanemail" ==
Err "there is no domain extension"
stringToEmail "still.not.an.email" ==
Err "there is no @"

me> 😃 Aaaaah, now the cause of the error String can be wrapped in the Err constructor, and we can retrieve it with a case .. of! And when everything is fine, we can use the Ok constructor instead and put the proper value inside!

wat> And the error does not even need be a String ; since it is a type parameter in Result error value, just like value, you can use your very own specialized error type that suits your specific needs.

me> That’s really cool! but… I’m going to need a lot of case .. of again to use these Results content… 😞

wat> Well, do not despair! ♘ Remember, you told me at the beginning of our chat that Result is also a Functor, and like a good little functor, it has a map function!

me> But I still don’t see how it works! 😠 As far as I understand, Functors are for types that have one type parameter, like Maybe something, List something, Array something… right?

wat> Yes… and no. 😉

me> I mean, how can map work with two type parameters?

-- generic form for any Functor:
-- map : (a -> b) -> Functor a -> Functor b
Maybe.map : (a -> b) -> Maybe a -> Maybe b
List .map : (a -> b) -> List a -> List b
Array.map : (a -> b) -> Array a -> Array b

wat> The trick is to include the error parameter in the functor definition. So it’s not just Result that is a functor, it’s really Result error:

Result.map : (a -> b) -> Result error a -> Result error b

me> 🤯 … is that why the value parameter is the second parameter and not the first?

wat> Yes. If not, it wouldn’t be such a good idea to name the function map because people that use map expect the last type parameter will be the one to be changed.

me> And I guess that Result.map only changes the value in an Ok and just passes the Err with their error values untouched, just like Maybe.map returns a Nothing when fed a Nothing?

wat> We’ll make a ducky out of you someday 👨‍🎓

me> Thanks… And what if I want to change not the value, but the error? (if any)

wat> You’ve got a nifty Result.mapError function for that:

Result.mapError : (a -> b) -> Result a value -> Result b value

me> Oh, so it’s just like map… but for errors.

wat> 😁 and just like Maybe, there’s a Result.withDefault to unwrap values (providing a default value in case we had an Err):

Result.withDefault : value -> Result error value -> value

me> I guess I’m going to check the rest of the library out

wat> While you’re at it, maybe you should have a peek at theResult.Extra ; it’s got extra goodies to work with Result that you might like.

me> Cool 😃 off to read…

--

--