3 ms·
> (Yes, I know about map_err.) I am curious to hear more about why you prefer what you wrote to map_err, if you're willing to share. I have my own guess, but I
by steveklabnik 4y ago
> (Yes, I know about map_err.)
I am curious to hear more about why you prefer what you wrote to map_err, if you're willing to share. I have my own guess, but I'm always interested in things like this.
(For those that don't know, you could write the above code as something like:
let aVar = SomeFunction().map_err(|err| OtherError::from(someContext, err))?;
or if it becomes too long,
let aVar = SomeFunction()
.map_err(|err| OtherError::from(someContext, err))?;
and in some cases you don't even need the |err| but since we're taking two arguments here you can't 'curry' it in a sense.)
- klodolph 4y agoThe difference with .map_err() is that you are no longer in the control flow of the parent function, so you can't decide to ignore the error inside the .map_err(), and you can't decide to return/break there either. You end up combining .map_err() with ?. The "match" syntax is very simple, clear, and flexible. I don't have to remember the different Result methods: or(), map_err(), map_or_else(), and_then(), etc. There are just too many methods to remember, and match does everything. (Edit: and it's easier to borrow or move something from the enclosing scope inside 'match'.) If I later decide that the Err() branch inside the match needs to do something other than propagate an error, I can just change the code there, without having to change the .map_err() to something else. I like match because it's simple, easy to read, easy to write, and flexible. In Haskell, I’m more comfortable using functions like Rust's map_err because I can, e.g., put it on the outside of a "do" block, and in Haskell, it’s not like you’re going to "break" or "return" anyway (not in the imperative sense). In other words, in Haskell, something like mapError has the same power as "case" does (and perhaps more, since there are multiple ways to propagate errors in Haskell, and mapError will handle all of them, as long as you’re in an error-handling monad).
- steveklabnik 4y agoCool thank you! That is actually not the thing that I would have guessed, which is why I love asking these questions rather than assuming. > you are no longer in the control flow of the parent function, Yeah, I run into this myself from time to time. I still end up defaulting to map_err when possible and then doing this when I have to, personally, but I can see the logic here. > I don't have to remember the different Result methods: I 1000% understand this. I think where I'm personally at is because I do know those methods, I end up doing the opposite: it takes me a second to go "oh, this is just map_err," which is the same problem but in reverse, and someone's gonna have to do it one way or the other.
- anyfoo 4y ago> and in some cases you don't even need the |err| but since we're taking two arguments here you can't 'curry' it in a sense. This is extremely pedantic, but without knowing Go, it looks like it's the other way around: Because you cannot curry the function "from"[1], you cannot do the partial application of "from" to "err" (which after currying would not really be a "partial application" but just application of the outermost function). Hence usage of what looks like a lambda. [1] I guess currying is just not a thing in Go, because this looks like a straightforward case from a distance?
- steveklabnik 4y agoYeah to be honest I tossed the 's around curry because I didn't want to think about it too hard. You're right, and here's the more lengthy version for the curious. Just to be clear, I was speaking about Rust here, not Go. Rust doesn't really support "currying" exactly. What's happening here is this: The signature of map_err is pub fn map_err<F, O>(self, op: O) -> Result<T, F> where O: FnOnce(E) -> F, A method that takes a closure that implements FnOnce(E) -> F. I shouldn't have named it "from" because From::from in Rust has a different signature than we needed here, I'll call the version in this code "from_context" instead for the rest of this post: fn from(T) -> Self fn from_context<C, E>(context: C, error: E) -> Self (where C and E aren't exactly this but whatever it doesn't matter from what I'm trying to say here) We have an arity mismatch between these two. FnOnce takes one argument: the error. But in our version, we also want to tack on some context. If we didn't need two arguments, then we could simply pass the function as an argument, because function types in Rust also implement the closure traits in Rust. .map_err(OtherError::from)? Is that currying, exactly? It feels like it but I'd argue it's not. It's just a type implementing a trait, just like anything else. If Rust let you do .map_err(OtherError::from_context(someContext))?; I would argue that's currying support. You can do some sorts of currying in Rust, manually, but it's really awkward and 99.99% of the time you're not doing that.
- anyfoo 4y agoGood explanation! Wish I knew Rust (which I do often), now, instead of just Haskell, heheh. For anyone curious what "currying" actually is (I must say that I, for quite some time, made the mistake of thinking that taking a function that takes n arguments and getting a function that takes n-1 arguments (with one fixed) is "currying", which is not technically true, it's just a good thing you can do after currying): Currying is the act of taking a function that takes n arguments (or an n-tuple), and returning a new function with only one argument. But that function effectively wraps another function, and this repeats n times. So, you take e.g. this function: f(x,y,z) and currying makes this function: g(z) You see it only has one argument. But when you call that function with its only argument, the result is actually another function: h(y) There's our second argument. Call that and you get: i(x) And now you can call that with the first argument, and get the result. So after currying a function, you can just apply the last argument, and effectively get back a function with the last argument "fixed". Without that, you effectively have to use a lambda to fix the argument. (Or a combinator that does it for you.) Though you will need some sort of lambda or combinator anyway, if it's not the last argument you want to fix, but some other one. Then you have to create functions with stuff flipped around... Functions in Haskell usually take care that the last argument is the one where you'd most often want to fix it to a value.