sanctuary-js / sanctuary-type-classes

:rainbow: Standard library for Fantasy Land
MIT License
233 stars 21 forks source link

define Array$prototype$bimap for two-element arrays #86

Closed davidchambers closed 6 years ago

davidchambers commented 6 years ago

@masaeedu shared this snippet in the Gitter room:

export const capitalizeKeys = o =>
  pipe([
    keys,
    map(k => ({ [k.toUpperCase()]: o[k] })),
    reduce(concat)({})
  ])(o);

After some discussion we agreed this code could be significantly nicer if S.pairs returned an array of honest-to-goodness pairs. One could then write:

//    capitalizeKeys :: StrMap a -> StrMap a
const capitalizeKeys = S.pipe ([S.pairs, S.map (S.mapLeft (S.toUpper)), S.fromPairs]);

This is not possible because ['foo', true] does not support bimap/mapLeft. It could do, but a problem arises when one considers map. How should S.map (S.toUpper) (['foo', 'bar']) evaluate? If one sees ['foo', 'bar'] as a member of Array String, the result should be ['FOO', 'BAR']. If, on the other hand, one sees ['foo', 'bar'] as a member of Pair String String, the result should be ['foo', 'BAR']. (Treating ['foo', true] as Pair String Boolean while treating ['foo', 'bar'] as Array String is out of the question.)

I'm reluctant to support bimap and mapLeft while leaving map out in the cold. I would rather not treat two-element arrays specially at all, and have functions such as S.pairs use the real Pair a b type soon to be released as sanctuary-pair.

@masaeedu posed an intriguing question:

Is mapRight not a thing?

It certainly could be a thing. It's trivial to derive from bimap, as was the case with mapLeft. We could provide the following four functions:

map      ::   Functor f => (a -> b) -> f a -> f b
bimap    :: Bifunctor f => (a -> b) -> (c -> d) -> f a b -> f c d
mapLeft  :: Bifunctor f => (a -> b) -> f a c -> f b c
mapRight :: Bifunctor f => (b -> c) -> f a b -> f a c

mapRight would be equivalent to map for Either a b and various other types. For Pair a b, though, or Array2 a b as it will be known if sanctuary-js/sanctuary-def#182 is merged, mapRight would behave differently from map:

> S.map (S.toUpper) (['foo', 'bar'])
['FOO', 'BAR']

> S.mapRight (S.toUpper) (['foo', 'bar'])
['foo', 'BAR']

> S.mapRight (S.not) (['foo', true])
['foo', false]

I'd still like to consider having S.pairs return a real pair rather than a two-element array, but improving the usefulness of values such as ['foo', true] may be a good idea regardless. What do you think?

gabejohnson commented 6 years ago

I don't mind defining mapRight as an alias for map or bimap(identity, f), but I don't like the idea of trying to "type coerce" an Array into a pair. I know it's idiomatic to use it that way, but I wouldn't want it in this library as, to my mind, sanctuary-type-classes should have no knowledge of Santuary's type checking capabilities.

Avaq commented 6 years ago

I don't like the idea of trying to "type coerce" an Array into a pair -- @gabejohnson

There's no type coercion happening. I worry that you've misunderstood this:

mapRight would be equivalent to map for Either a b and various other types. For Pair a b, though, [...] mapRight would behave differently from map. -- @davidchambers

I want to make sure that you understand that this happens naturally - not because we are checking any types. What's being proposed are two simple things, of which neither sound wrong to me:

  1. Adding a Bifunctor instance to all Arrays of length 2.
  2. Adding mapRight, derived from bimap.

Together, this would allow me to map over an entire array with map, and adjust just the right value of a binary array using mapRight.

Avaq commented 6 years ago

The problem might be that we are breaking a Fantasy Land law doing this:

If a data type provides a method which could be derived, its behaviour must be equivalent to that of the derivation

And map can be derived from bimap. So having a value ['a', 'b'] with two differing map implementations might be in violation.

Avaq commented 6 years ago

So in other words

Is mapRight not a thing?

It is a thing: map is mapRight for all instances of Bifunctor.

The more I think about it, the less of a good idea this seems.

masaeedu commented 6 years ago

@Avaq [1, 2] has two possible instances of Functor, one derivable from its instance of Bifunctor, and one that is not (this is no different from the two possible applicative instances of an array). Providing a mapRight allows you to unambiguously access the Bifunctor derivable Functor instance's map where there is ambiguity.

masaeedu commented 6 years ago

There are of course other ways to resolve the ambiguity, such as using type-reps (which is how the applicative ambiguity is resolved) or being able to reorder precedence of type classes in a local scope, but these are a little trickier than just adding an alias.

gabejohnson commented 6 years ago

@Avaq I understand what's being proposed. I put "type coerce" in quotes because I didn't have a better term in mind at that moment. I should have said "give Array a Bifunctor instance".

I want to make sure that you understand that this happens naturally - not because we are checking any types. What's being proposed are two simple things, of which neither sound wrong to me:

  1. Adding a Bifunctor instance to all Arrays of length 2.
  2. Adding mapRight, derived from bimap.

I object 1 but not 2. I think it's going to be confusing in the long run to add a Bifunctor instance for arrays of any length and I suspect we'll end up deciding to remove it in the future.

What's stopping us from adding a Bifunctor instance for arrays of length 3?

instance Bifunctor (Array3 a) ...
Avaq commented 6 years ago

@gabejohnson Yeah. I see your point now. It seems we agree.

davidchambers commented 6 years ago

sanctuary-type-classes should have no knowledge of Santuary's type checking capabilities.

I agree. I was thinking that Array$prototype$bimap would do the length check itself and throw if this.length !== 2.

I think it's going to be confusing in the long run to add a Bifunctor instance for arrays of any length and I suspect we'll end up deciding to remove it in the future.

I appreciate your cautiousness. Let's leave this issue open while pushing to release sanctuary-pair. The ergonomics of the real Pair a b type may prove to be excellent, in which case the value of treating two-element arrays as bifunctors would be greatly diminished. The existence of bimap, mapLeft, and map mean many Pair a b transformations can be performed without lambdas, diminishing the importance of an inherent advantage of arrays: the ability to write ([k, v]) => ....

gabejohnson commented 6 years ago

The existence of bimap, mapLeft, and map mean many Pair a b transformations can be performed without lambdas, diminishing the importance of an inherent advantage of arrays: the ability to write ([k, v]) => ...

Additionally, Pair implements the ECMAScript Iterable protocol so

(([x, y]) => x + y)(Pair(1, 2)) // 3

We can have our 🍰 and 🍴 it too πŸ˜„