Skip to content

Conversation

mbid
Copy link
Contributor

@mbid mbid commented Jun 15, 2017

Add -WithIndex classes

This adds FunctorWithIndex, FoldableWithIndex and
TraversableWithIndex and some combinators. Implementations are mostly
copied from the versions without index with minor adaptions. Similarly
for the documentation.

I think the corresponding haskell package does not state all laws for
these type classes, so there are TODO's in the doc strings of the three
classes.

StateL and StateR in TraversableWithIndex.purs are exact copies of
the versions in Traversable.purs because they are not exported there.
Maybe it would be better to merge TraversableWithIndex.purs into
Traversable.purs and similarly for Foldable. The Foldable and Traverse
instances of Array could then be along the lines of

traverse f = itraverse (const f)`

so that the foreign js imports for the unindexed version could be
removed.

FunctorWithIndex's imap collides with the imap of
purescript-invariant. Haskell calls the latter invmap. I don't know
whether that's a problem.

This does not yet include the -1WithIndex classes, i.e.
generalizations of Foldable1 and Traversable1 in Semigroup/. I
guess it would make sense to provide them here as well.

If this gets merged we should also implement instances in some other packages. purescript-lists and purescript-maps comes to mind. Anywhere else?

Martin Bidlingmaier added 3 commits June 15, 2017 14:59
This adds `FunctorWithIndex`, `FoldableWithIndex` and
`TraversableWithIndex` and some combinators. Implementations are mostly
copied from the versions without index with minor adaptions. Similarly
for the documentation.

I think the corresponding haskell package does not state all laws for
these type classes, so there are TODO's in the doc strings of the three
classes.

`StateL` and `StateR` in TraversableWithIndex.purs are exact copies of
the versions in Traversable.purs because they are not exported there.
Maybe it would be better to merge TraversableWithIndex.purs into
Traversable.purs and similarly for Foldable. The Foldable and Traverse
instances of Array could then be along the lines of
```purescript
traverse f = itraverse (const f)`
```
so that the foreign js imports for the unindexed version could be
removed.

`FunctorWithIndex`'s `imap` collides with the `imap` of
purescript-invariant. Haskell calls the latter `invmap`. I don't know
whether that's a problem.

This does not yet include the `-1WithIndex` classes, i.e.
generalizations of `Foldable1` and `Traversable1` in `Semigroup/`. I
guess it would make sense to provide them here as well.
-- | - `imap (const f) = map f`
-- | though?
-- |
-- | TODO: `imap` collides with `Invariant`s `imap`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should either use WithIndex suffixes on all of these functions, or prefix with ix as an alternative to i prefixes?

I wouldn't be too worried about overlapping with Invariant's map either though, there's always qualification. And Invariant is one of the less frequently used classes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer ix- over -WithIndex. Consistency with the haskell functions (so i-) is also nice I guess. Your call.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer mapWithIndex since we use that convention elsewhere.

@paf31
Copy link
Contributor

paf31 commented Jun 18, 2017

Thanks!

I think we can remove the FFI requirement here by defining instances in terms of regular Foldable and Traversable and Array.mapWithIndex, right? (at the expense of a little performance admittedly)

What do you think?

@paf31
Copy link
Contributor

paf31 commented Jun 18, 2017

For the StateX thing, I think we could make an internal module.

@mbid
Copy link
Contributor Author

mbid commented Jun 19, 2017

Yeah, I agree that the duplication of the FFI implementations is not nice. I guess we can do two things:

  • What you said: Use imap, i.e. itraverse f = sequence <<< imap f. The FoldableWithIndex functions can be implemented either using Const and this itraverse, or imap Tuple followed by the folds without index. Unfortunately, we'd need to implement Const or Tuple here, because both packages already depend on foldable-traversable.
  • Implement the variants without index in terms of the indexed variants, e.g. foldMap = ifoldMap <<< const. This would require defining the -WithIndex classes in the same file together with the variants without index (e.g. FoldableWithIndex in Foldable.purs), or moving the FFI implementations to a separate internal module. Maybe foldMap would be a bit slower because of the additional argument passed, but ifoldMap would only iterate the array once instead of twice, and use no additional memory (similarly for the other functions).

I'd prefer the second version slightly, but let me know which one you'd find better.

@mbid
Copy link
Contributor Author

mbid commented Jun 21, 2017

I've just done some benchmarks for the second approach. Looks like this would make foldMap around 25% slower than it currently is.

EDIT: Added benchmarks for the other implementations as well.

  • the Traverse instances are slow anyway, and implementing itraverse in terms of traverse instead of natively (or vice-versa) does not change much, relatively.
  • Implementing foldr with ifoldr does not change worsen performance by much. On the other hand, ifoldr via foldr is only half as fast (I guess because the array is iterated twice instead of only once).
  • implementing foldMap in terms of ifoldMap makes it ~25% slower, ifoldMap via foldMap makes it ~18% slower.

So I guess we can definitely drop the foreign implementation of itraverse. I don't know what to do for foldMap. I don't know what to do with regard to foldMap. It's not big foreign implementation. We definitely need to keep the foreign implementation of imap (no other way to implement here).

Martin Bidlingmaier added 5 commits June 23, 2017 11:45
Also fixes the analogous bug for ifoldMapDefaultL
The modified `Foldable` and `Traversable` laws that must hold for their
-WithIndex versions are missing in the docs. But because the actual
unmodified laws for `Foldable` and `Traversable` themselves are not here
either I guess it shouldn't be a problem.
@mbid
Copy link
Contributor Author

mbid commented Jun 23, 2017

Ok, so I've removed the foreign implementations where possible. This means that ifoldr and ifoldl are only 1/2 - 1/3 as fast as they could to be for trivial iteration (e.g. just adding integers), but that's good for me. Given that the versions without indices are used far more often, I don't think it would've been a good tradeoff to take a performance hit for foldr instead of ifoldr.

Regarding the documentation, I've documented some laws that enforce that map f = imap (const f) and analogous equations for Foldable and Traversable.
Given that the complete Foldable and Traversable laws are not in the docs either, I found it pointless to document the modified versions for the -WithIndex versions. We should maybe improve this in a separate issue.

I guess I'll add some tests now. Can we break up the single test file into one for each class? As far as I can tell, there is no interdependence.

Martin Bidlingmaier added 2 commits June 23, 2017 18:18
No tests for

- ifoldM
- itraverse_
- ifor_
- iall
- iany
- all instances except for Array

These are not tested for Foldable either.
@mbid
Copy link
Contributor Author

mbid commented Jun 23, 2017

Ok so I've just gone ahead and added some tests. As far as I can tell, this should be OK to merge now unless we want to change the prefix of the indexed functions from i- to something else.

-- | map f = imap (const f)
-- | ```
class Functor f <= FunctorWithIndex i f | f -> i where
imap :: forall a b. (i -> a -> b) -> f a -> f b
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is called mapWithIndex elsewhere actually.

@paf31
Copy link
Contributor

paf31 commented Jun 27, 2017

@garyb Any more thoughts on this? Other than naming, I think this looks great.

@mbid
Copy link
Contributor Author

mbid commented Jun 27, 2017

Are you suggesting renaming only imap to mapWithIndex, or also ifoldMap to foldMapWithIndex and so on? I'd find it a little inconsistent if we only did the first thing.

@paf31
Copy link
Contributor

paf31 commented Jun 27, 2017

I'm suggesting renaming them all.

@mbid
Copy link
Contributor Author

mbid commented Jul 9, 2017

Ok, done.

bifoldr :: forall a b c. (a -> c -> c) -> (b -> c -> c) -> c -> p a b -> c
bifoldl :: forall a b c. (c -> a -> c) -> (c -> b -> c) -> c -> p a b -> c
bifoldMap :: forall m a b. Monoid m => (a -> m) -> (b -> m) -> p a b -> m
bfoldrWithIndex :: forall a b c. (a -> c -> c) -> (b -> c -> c) -> c -> p a b -> c
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused why these changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uh yeah, and rightly so.

@paf31 paf31 merged commit c4875cc into purescript:master Jul 10, 2017
@paf31
Copy link
Contributor

paf31 commented Jul 10, 2017

Great work, thank you!

I'll make a minor release for this since nothing seems to be breaking here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants