This is very thorough, and I'm sure it's helpful to a lot of folks. So thank you to the author. For me, it's probably a syntactical issue, but I find things such as the below too hard to comprehend.
What did we get just by doing this? Let's see:
λ> :type EitherIO
EitherIO :: IO (Either e a) -> EitherIO e a
λ> :type runEitherIO
runEitherIO :: EitherIO e a -> IO (Either e a)
So already we have a way to go between our own type and the combination we used previously! That's gotta be useful somehow.
OK, looks like a pair of parentheses moved. I have seriously no idea why that is interesting or useful. Conceptually what's going on is probably not complicated, but that syntax makes it inscrutable to me. (I could be wrong and the problem could be at a deeper level of understanding.)
They're different representations of the same thing.
This is useful because we can do easy error handling in the EitherIO monad but we can't run it. We can run the IO monad but handling errors in it sucks ass.
So we handle errors in EitherIO, and make it runnable (ie. convert to the IO monad) by calling runEitherIO.
It's like calling a function that takes a pair when some other API gives you a two element list. The same kind of data, just munging the shape so things work. In pseudocode:
// EitherIO and runEitherIO in this analogy.
fun toList((a, b)) -> [a, b]
fun toPair([a, b]) -> (a, b)
// Gotta convert to a "runnable" pair before calling the function.
var latLngList = [blah, blah]
var resultPair = api1ThatRequiresPair(toPair(latLngList))
// Convert representations to do operations with a different API.
api2ThatRequiresList(toList(resultPair))
data EitherIO e a = EitherIO {
runEitherIO :: IO (Either e a)
}
But, for the sake of clarity, it can be written as
data EitherIO e a = MakeEitherIO {
runEitherIO :: IO (Either e a)
}
The author pointed out two functions that this EitherIO declaration gave us. One is a constructor, MakeEitherIO, with type
IO (Either e a) -> EitherIO e a
In other words, MakeEitherIO takes a value of type IO (Either e a) and returns a value of type EitherIO e a (you could think of this as "wrapping" the original IO value).
The second function, runEitherIO, is an accessor function for EitherIO's named record field "runEitherIO". It has type
EitherIO e a -> IO (Either e a)
That is, runEitherIO takes a value of type EitherIO and returns the internal value of type IO (Either e a) (you could think of this as "unwrapping" the IO value).
What did we get just by doing this? Let's see:
So already we have a way to go between our own type and the combination we used previously! That's gotta be useful somehow.OK, looks like a pair of parentheses moved. I have seriously no idea why that is interesting or useful. Conceptually what's going on is probably not complicated, but that syntax makes it inscrutable to me. (I could be wrong and the problem could be at a deeper level of understanding.)