elm-community / result-extra

Convenience functions for working with Result.

Home Page:http://package.elm-lang.org/packages/elm-community/result-extra/latest

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

mapBoth vs elim

toastal opened this issue · comments

Your use of mapBoth is misleading. map implies changing values of a functor, not getting values out. I think map both should remain, but it's signature should be: mapBoth : (e -> f) -> (a -> b) -> Result e a -> Result f b and the current mapBoth should be renamed elim.

However, the name mapBoth is somewhat consistent with mapDefault in http://package.elm-lang.org/packages/elm-community/maybe-extra/1.1.0/Maybe-Extra#mapDefault. Note that that function is also about "getting values out", but does have "map" in its name. So if you want a consistent story, Maybe.Extra.mapDefault would also have to change?

I disagree completely. Look up mapBoth on ElmSearchResult.Extra.mapBoth is the only one not actually mapping both. It should combine mapping both the error and the value at once for efficiency's sake. It's both intuitively what makes sense in the name as well as what every other libraries are doing.

Furthermore in the case of mapDefault and why it's not like this is

  • it doesn't have two function arguments
  • it's just a handy composition of conceptually map (a -> b) >> withDefault b returning a b so slamming it together with the name mapDefault feels appropriate and intuitive.

So, maybe there's a more intuitive name than elim for the Result context. Either in Haskell uses either, the These library uses these, and in my Either library I used elim but still aliased either for completeness. Either libraries, which is essentially what the Result type is, also contain a fromLeft and fromRight to eliminate the functor just like mapDefault. But essentially in this case you want to eliminate or destroy the Result via two functions rather than a "default" value and return that singular value—not map it which is changing the value of the functor.

TL;DR: mapBoth in this library is an unintuitive and inconsistent name, and it needs a better name while the real mapBoth as I've pull requested needs to exists because it's intuitive, useful and efficient.

I disagree completely.

You seem to be disagreeing to a lot of things I haven't actually said. 😄

For example, I haven't commented at all about the suitability of the name mapBoth for the function in question here, or its relative merits to the name elim etc.

All I've been saying is that name map... for the function (e -> b) -> (a -> b) -> Result e a -> b is consistent with name map... for the function b -> (a -> b) -> Maybe a -> b.

You seem to also be disagreeing that the functions (e -> b) -> (a -> b) -> Result e a -> b and b -> (a -> b) -> Maybe a -> b correspond to each other (I said it by saying they both are about "getting values out"). Well, on that point, I can kind of "prove" that my perspective is correct. As follows:

  • You mention that for Either the function corresponding to the (e -> b) -> (a -> b) -> Result e a -> b one is typically called either, but could also be called elim.
  • Well, check out the function maybe in Haskell. It is exactly the b -> (a -> b) -> Maybe a -> b one. And yes, it would also be appropriate to call it elim, because it is exactly the eliminator for the Maybe type in the same way that the other function is the eliminator for the Either type, and the one under consideration here is the eliminator for the Result type.

So I don't see how you can maintain that the (e -> b) -> (a -> b) -> Result e a -> b function and the b -> (a -> b) -> Maybe a -> b function do not correspond to each other. They are the exact same thing (the canonical eliminator, in technical terms) for the Result and Maybe types, respectively.

In conclusion, it would make sense that their names follow a common scheme. That could be by calling them both elim, or by calling the former result and the latter maybe, or maybe by other means. But they do correspond to each other, so it makes sense to make decisions about their names together. And that's all I had really be saying.

Following a look into your Either library, an appropriate name for elim could be mapFromBoth or mapBothFrom. By analogy to Maybe's mapDefault as composition of map and withDefault (which could also be called fromJust, in exactly the same way your Either's withDefault is a synonym for fromRight), because essentially Either's (or Result's) elim is the composition of mapBoth (in the bifunctor sense, not the current Result.Extra.mapBoth sense) and fromLeft/fromRight combined into one (since after an appropriate mapBoth call, both Left and Right cases can be eliminated without requiring a default value to make up for a type mismatch).

In Haskell terms, Result would have a Bifunctor instance and have a function bimap :: (a -> b) -> (c -> d) -> p a c -> p b d. The other similar function would be either :: (a -> c) -> (b -> c) -> Either a b -> c (or: maybe :: b -> (a -> b) -> Maybe a -> b). Thus, from my perspective, the names in elm should be:

  • mapBoth :: (a -> b) -> (c -> d) -> Result a c -> Result b d
  • result :: (a -> c) -> (b -> c) -> Result a b -> c
  • and so on.

I don't agree that a *withDefault function is that useful, as you can easily make one using const and identity with the combinator result.

Most of this comes down to semantics, but what these libraries do is set the course for naming in general for all future libraries because the common lexicon we're establishing help people reason about what mapBoth means in the context of Elm (and apparently renaming most things from Haskell so we don't have a common lexicon ala bimap eye roll). I chose the name elim because I wanted to maintain compatibility with the existing Either library that was far from "complete" – and I think the word conveys the meaning pretty well. Regardless, I think it's important that these names are good and intuitive and I'm open to what other might have to say.

I think a strong argument can be made that mapDefault is also a bad name along with mapBoth for Result. Map implies that was are staying inside the same type, mapDefault clearly violates that contract.

@toastal is right. mapBoth currently in the Elm ecosystem obeys the biFunctor api in every regard except for in Result.Extra. We should be working hard to maintain the quality of developer intuition by keeping the types and names of functions as consistent as possible.

https://klaftertief.github.io/elm-search/?q=mapBoth

This is also true of map

https://klaftertief.github.io/elm-search/?q=map

as well as other map variants like mapWith mapFst mapWithId. The last argument is our Functor, and the function returns a Functor of the same type.

Its natural that developers would build this intuition as its both mathematical, and driven home by the sheer bulk of existing functions that currently exist supporting it.

The outliers are small:

mapBoth : (e -> b) -> (a -> b) -> Result e a -> b
mapDefault : b -> (a -> b) -> Maybe a -> b
mapDefault : c -> (b -> c) -> Either x b -> c

These functions, and only these function currently include the word map and violate the intuition. So the naming should change. @agrafix's name suggestions sound good to me, as well as the proposal in the PR.

I think it's uncontroversial that mapBoth : (e -> b) -> (a -> b) -> Result e a -> b is wrong. (I never claimed it was right.)

I also agree with all the talk about setting good naming examples here.

So is there agreement now that mapDefault should be renamed in the same go? And to what?

Here is my spitball of potential names:

resultAs : c -> (b -> c) -> Result x b -> c
extract : c -> (b -> c) -> Result x b -> c
extractAs : c -> (b -> c) -> Result x b -> c
extractWith : c -> (b -> c) -> Result x b -> c
resolveAs : c -> (b -> c) -> Result x b -> c
collapse  : c -> (b -> c) -> Result x b -> c

Since all this is now tied in with the name of Maybe.Extra.mapDefault, it makes sense to look at the discussion that led to that name: elm-community/maybe-extra#7

Haskell's maybe as a potential name was on the table back then.

@Fresheyeball, I'm confused. Are we looking for names for a function of type c -> (b -> c) -> Result x b -> c? I don't think we are, from all the above discussions.

It's important for context to remember that Maybe isn't a bifunctor like Either, Result, Task... you can't map Nothing.

Well, you could also define maybe as type Maybe a = Either () a and then you can "map" () -> c which is very similar to just having a constant c. Maybe it's even a good idea to define maybe like so: maybe : (() -> c) -> (a -> c) -> Maybe a -> c to get a lazy default! Then it would also be exactly like the others. I would say maybe, result , task, either should be the correct names for the combinators mapping "both" sides to a common result.

@jvoigtlaender you are right! I was thinking in terms of the existing mapDefault functions. To keep it consistent may be more steps than originally thought. This library would probably benefit from having 3 functions here:

resultAs : c -> (b -> c) -> Result x b -> c -- analogous to mapDefault in toastal/either
result : (e -> b) -> (a -> b) -> Result e a -> b -- in agreement with @agrafix
mapBoth : (a -> b) -> (c -> d) -> Result a c -> Result b d

About maybe, either, etc.: http://package.elm-lang.org/help/design-guidelines#module-names-should-not-reappear-in-function-names

The "Elm way" seems to be to think of functions being used qualified. So Maybe.map and List.map and Either.mapBoth and Result.mapBoth all seem fine. But Maybe.maybe and Result.result are strange then. The design guidelines seem to call for finding a common name such that Maybe.commonName : b -> (a -> b) -> Maybe a -> b and Result.commonName : (e -> b) -> (a -> b) -> Result e a -> b.

Thats a really good point. So we are in a space where there is not an established comvention, and Haskell csnt bail us out. So we need to pick something new. Any in my spitball look appealing as a commonName for these ideas?

I don't like the map in the name, because for me mapping means keeping the container "intact". Maybe unpack would be a good name?

Or kaboom ;) ... elim and collapse aren't bad, but I do like unpack.

(Sorry for the accidental closing, hit wrong button.)

I like unpack as well.

And from past dicussion on the mailing list, I think I can say it is highly unlikely the name elim would ever be admitted into core. The name eliminate yes, the name elim no. (That discussion was about const and constant specifically, but the argument was made in more general terms.)

So.. unpack yes win. But we need 2 new names one for

: (e -> b) -> (a -> b) -> Result e a -> b

and one for

: c -> (b -> c) -> Result x b -> c

which one is unpack? is one unpack and the other unpackWithDefault?

A function : c -> (b -> c) -> Result x b -> c does not currently exist in this library. Does it really need to exist?

I think if we are going to pick a commonName to use in place of mapDefault in the maybe package, then yes for consistency.

Hmm, I don't think : c -> (b -> c) -> Result x b -> c and : b -> (a -> b) -> Maybe a -> b should have a (completely) common name. Instead, we should have unpack : (x -> c) -> (b -> c) -> Result x b -> c and unpack : b -> (a -> b) -> Maybe a -> b. But yes, : c -> (b -> c) -> Result x b -> c could have some name derived from unpack.

I personally like unpack being the function with function arguments to eliminate rather than just like a fromLeft or fromRight with a default value.

How about:

Result.unpack : (x -> c) -> (y -> c) -> Result x y -> c
Maybe.unpack : (() -> c) -> (y -> c) -> Maybe y -> c

Maybe.unwrap : c -> (y -> c) -> Maybe y -> c
Result.unwrap : c -> (y -> c) -> Result x y -> c

@agrafix I don't like the lazy default personally. Its a cool idea, but not 100% straight forward. Other than that, loooooks good to me.

In the Maybe context, I would prefer to rename Maybe.unpack to Maybe.unwrapLazy. So, in order, the functions above would be named Result.unpack, Maybe.unwrapLazy, Maybe.unwrap, and Result.unwrap.

The idea being that unwrap is something you do on something that has only one possible type inside (Maybe), while unpack is something you do on something that has two possible types inside (Result). And you can also choose to unwrap a thing with two possible types inside, namely by providing a default value.

Plus, the Lazy suffix would be explicit, in line with the new orLazy and orElseLazy functions.

I'm really hesitant towards the way lazy has come to be not have a concrete meaning. In the Html.Lazy it has nothing to do with lazy evaluation—maybe if that were cleaned up, but I might expect this to do a short-circuited memoization instead because of what I learned from Html for lazy to be.

Also I think the concept of unpack should not be limited to 2, but n where n is the maximum number of possibilities.

Also I think the concept of unpack should not be limited to 2, but n where n is the maximum number of possibilities.

That sounds to me like an argument for Maybe.unpack : c -> (y -> c) -> Maybe y -> c. But maybe you mean it differently. In any case, maybe it's a discussion to be continued in maybe-extra?

About lazy, I see where this is coming from. Probably the most accurate way to express things would be to say ...Thunked or instead of ...Lazy.

Maximum number of possibilities that contain values... like if you had an type Eitherish a b c : Left a | Right d | Up c and unpack would look like unwrap : (a -> d) -> (b -> d) -> (c -> d) -> Eitherish a b c -> d.

Or maybe ...Deferred? I don't think I like that that much, but it's an option.

I've made an issue in maybe-extra concerning what to do about mapDefault there now: elm-community/maybe-extra#24.

I guess we can close the discussion here and sort that issue out over there :-)