purescript/purescript-arrays

The absence of `map` function

ivan-kleshnin opened this issue · 16 comments

No doubt it's intentional. Same stuff for purescript-lists.
My guess is that map is not exposed to not collide with Prelude.map. This has downsides, though.

  1. Educational. I can't say "import Data.Array as Array and use concrete types before you're ready to functors", because the most important function isn't there.

  2. Namespacing. Special rule for map in Array.concatMap _ >>> map _ >>> Array.whatever _ kind of constructions is annoying. You can say it's a matter of habit, but in JS I use qualified imports almost exclusively. So I expected to do the same in PureScript. Maybe it's not a pragmatic approach (because operators) – you tell me.

Btw. can we namespace operators like Array.(..) or something?

  1. Consistency. The presence of generic map makes Array.map unnecessary? Well, there's Control.Bind.bindFlipped and there's Array.concatMap so no. I'm sure there are more functions in Array that have generic counterparts.

map is an exception and PureScripts is a language that doesn't favor exceptions. So a question.

Would you resolve this through simply exposing a map = Functor.map with a monomorphic type signature?

As for namespacing, is this something you can't accomplish with import Data.Array as Array?

Would you resolve this through simply exposing a map = Functor.map with a monomorphic type signature?

I would copy filter: https://github.com/purescript/purescript-arrays/blob/master/src/Data/Array.purs#L576-L583

foreign import filter :: forall a. (a -> Boolean) -> Array a -> Array a
->
foreign import map :: forall a. (a -> b) -> Array a -> Array b

As for namespacing, is this something you can't accomplish with import Data.Array as Array?

Current Array doesn't export map.

I would copy filter: https://github.com/purescript/purescript-arrays/blob/master/src/Data/Array.purs#L576-L583

The difference here is that map is already defined for Arrays in the Prelude: https://github.com/purescript/purescript-prelude/blob/v3.1.0/src/Data/Functor.purs#L42

So doing the foreign import approach would mean having two copies of the same code. This is why I suggest the re-export based approach. Is there a disadvantage to that?

@ivan-kleshnin please let me know if #125 does what you want

If we're going to re-export a version of map for educational purposes and/or for consistency with eg concatMap, there's an argument to be made that we should re-export a monomorphic version like you suggested earlier, i.e.:

map :: forall a b. (a -> b) -> Array a -> Array b
map = Prelude.map
garyb commented

👎 from me, I don't think we should do it at all. Since almost everything is a functor, using the general map should be encouraged rather than making it seem like something for arrays.

If we are though, I'd just do it re-export style like @matthewleon's PR, since that's what we do for a ton of foldable/traversable functions also.

there's an argument to be made that we should re-export a monomorphic version like you suggested earlier

Yes. I also see, however, that there are a whole bunch of polymorphic re-exports from the module already. So this maintains consistency with those.

Not really sure which way I lean on this. Just re-exporting the way I do in #125 has the benefit of being simpler, so I went for that.

👎 from me, I don't think we should do it at all. Since almost everything is a functor, using the general map should be encouraged rather than making it seem like something for arrays.

This makes sense, but also: lots of things are also monads, and I'm aware that Array.concatMap and =<< are equivalent, but despite this I do occasionally use Array.concatMap instead of =<<. It's nice in situations where there aren't very many other contextual clues that you are dealing with an array, I think.

from me, I don't think we should do it at all. Since almost everything is a functor, using the general map should be encouraged rather than making it seem like something for arrays.

Fantasy Land introduces filterable algebra which defines filter as a characteristic function. By the logic in quote, shall PureScript decide to bring that concept: we make a Filterable typeclass, make an Array instance of it, and then... remove Array.filter and List.filter because it will appear in Prelude and "almost any collection is filterable". Tomorrow someone invents a new algebra of sortables and we... remove Array.sort and Array.sortWith?

What I'm saying, is that map is not the last instance where a) we have a generic version of something familiar b) the names clash.

@ivan-kleshnin please let me know if #125 does what you want

@matthewleon yes, initially I hoped for something like:

import Prelude as Prelude

map :: forall a b. (a -> b) -> Array a -> Array b
map = Prelude.map

But now I see, that we'll have to do the same for import Data.Foldable (foldl, foldr, foldMap, fold, intercalate, elem, notElem, find, findMap, any, all) as Exports etc. and that's a lot of work for a controversial benefit, so maybe your version is just right.

It's nice in situations where there aren't very many other contextual clues that you are dealing with an array, I think.

Yes. For example we can have head :: Array a -> Maybe a and we can have Alternative m => head :: Array a -> m a. Using Array.map reinforces the idea that previous function returns an array and not just any functor.

garyb commented

Fantasy Land introduces filterable algebra which defines filter as a characteristic function. By the logic in quote, shall PureScript decide to bring that concept: we make a Filterable typeclass, make an Array instance of it, and then... remove Array.filter and List.filter because it will appear in Prelude and "almost any collection is filterable". Tomorrow someone invents a new algebra of sortables and we... remove Array.sort and Array.sortWith?

I'd be fine with that. 😉 There is purescript-filterable already, but we don't have it as a core library, that's the only reason it's not already the case, as far as I'm concerned.

But also, functor is still significantly more common than things that apply to collections.

the names clash.

Regarding the names clashing, they don't really - if you import the same function that has been exported from multiple places, the compiler should know they're the same thing and not complain about it. Aside from one of them giving a redundant import warning.

Using Array.map reinforces the idea that previous function returns an array and not just any functor.

The type is (a -> b) -> f a -> f b not something like (a -> b) -> f a -> g b, so it doesn't give any indication it would change the structure being mapped over. In fact, that's one of the characteristics that makes it a functor rather than just being "something mappable" or whatever.

I don’t think that’s quite what was meant; I mean that if you see map f in code, but it’s not obvious that this function is being applied to an array (eg perhaps the compiler knows because this information comes from a complex type definition, but you can’t remember all the details), then it can sometimes be helpful to use the hypothetical monomorphic Array.map.

The type is (a -> b) -> f a -> f b not something like (a -> b) -> f a -> g b, so it doesn't give any indication it would change the structure being mapped over. In fact, that's one of the characteristics that makes it a functor rather than just being "something mappable" or whatever.

I meant what @hdgarrood said. It's not about types, it's about the calling side:

someFn >>> Array.map -- if it compiles, someFn returns Array a
someFn >>> map       -- if it compiles, someFn returns Functor a

In cases where someFn is itself dubious (like my example with head above) it's quite useful of a hint to see Array.map instead of map.

wclr commented

@garyb

Since almost everything is a functor, using the general map should be encouraged rather than making it seem like something for arrays.

What about types that are supposed also to have map2, map3,... functions?

Is it encouraged to use them also with general Functor's 'map'?
It seems a little bit inconsistent to use MyType.map2 but not use MyType.map but rather just map or Functor.map no?

We wouldn’t provide map2, map3, etc, because those are covered by Apply/Applicative.

wclr commented

@hdgarrood
So it can be stated that generally using in the code general map, lift2, lift3... is prefered way to creating and using api like Some.map, Some.map2, Some.map3?

You should do whatever feels right to you, but yes, that is the approach we're taking in the core libraries and that is the approach I'd use generally.