Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add strict over variants #1017

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open

Add strict over variants #1017

wants to merge 2 commits into from

Conversation

treeowl
Copy link
Contributor

@treeowl treeowl commented Dec 21, 2022

Add over' and iover', which force the result(s) of applying the passed function.

Closes #1016

@treeowl
Copy link
Contributor Author

treeowl commented Dec 23, 2022

@ekmett and @RyanGlScott , what do you think? Two questions in my mind:

  1. Most things around are marked INLINE. Should these be too?
  2. Should there be operator forms? %~! and %@~!, perhaps? Or would the bangs go in the middle somewhere?

@treeowl
Copy link
Contributor Author

treeowl commented Dec 23, 2022

Another question: as @phadej suggested, I wrote these to take specific traversals, rather than ATraversal and AnIndexedTraversal. I don't really understand the conventions around which style to use where.

@ekmett
Copy link
Owner

ekmett commented Dec 23, 2022

Combinators can generally ask for any functor that they need, so that's fine.

The main uses for ATraversal/AnIndexedTraversal is to allow users to pass around lists of traversals and the like, and to reconstitute them into real Traversals / IndexedTraversals through cloneTraversal.

@ekmett
Copy link
Owner

ekmett commented Dec 23, 2022

These should be marked INLINE, and the current morphology would likely lead to %!~ and %!@~ probably, (and %!= and %!@= respectively for state). The nice thing about the state one is you can use the usual strict function over a monad encoding I think using <$!>.

-- fields.
--
-- @
-- over traverse (const ⊥) [1,2] = [⊥, ⊥]
Copy link
Collaborator

Choose a reason for hiding this comment

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

These can be turned into proper doctests. length $ ... and matching like

...
...Exception...
...

for an error. It's not the prettiest, but it's an honest test.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To my mind doctests wouldn't do as good a job of documenting, but I can write them anyway. How do I run those locally?

Copy link
Collaborator

Choose a reason for hiding this comment

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

cabal build all --enable-tests
cabal-docspec

Copy link
Collaborator

@phadej phadej Dec 24, 2022

Choose a reason for hiding this comment

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

To my mind doctests wouldn't do as good a job of documenting, but I can write them anyway.

There is barely any non-doctest examples in lens. They may do less good job as documenting, but at least they stay up to date. There are hundreds of examples in lens package. No-one would ever go through them manually and verify they still type-check/work/etc.

Copy link
Contributor Author

@treeowl treeowl Dec 26, 2022

Choose a reason for hiding this comment

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

I'm getting errors from cabal-docspec about mismatched GHC and plan versions. Unfortunately, cabal-docspec doesn't seem to have much in the way of documentation, so I can't figure out where to go from there.

src/Control/Lens/Traversal.hs Show resolved Hide resolved
@treeowl
Copy link
Contributor Author

treeowl commented Dec 23, 2022

These should be marked INLINE, and the current morphology would likely lead to %!~ and %!@~ probably, (and %!= and %!@= respectively for state). The nice thing about the state one is you can use the usual strict function over a monad encoding I think using <$!>.

Are you suggesting traversing in the ambient monad? Or just pattern matching out of Solo into it?

@ekmett
Copy link
Owner

ekmett commented Dec 24, 2022

It's a state monad variant, so in theory the <$!> trick should be usable in the ambient monad. Not wedded to doing it that way, was just indicating it's probably doable and quite clean.

@@ -342,6 +342,10 @@ cloneIndexedSetter l pafb = taintedDot (runIdentity #. l (Indexed $ \i -> Identi
-- >>> over _1 show (10,20)
-- ("10",20)
--
-- Like 'fmap', @over@ is normally lazy in the result(s) of calling the
Copy link
Collaborator

@phadej phadej Dec 24, 2022

Choose a reason for hiding this comment

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

I'm also on the edge with this "warning".

Shouldn't the strict version also have a similar warning that it actually forces all the computations up front. I.e. over' traverse f aList is most likely a wrong thing to do. (And the examples used are indeed contrived).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, they're contrived. Should I give a datatype definition and makeLenses and all that jazz? A more realistic case might be a record with a lazy field. If you use over several times, you'll build a thunk chain in there. over' traverse isn't too far from what, say, Data.Map.Strict.map does, though it's probably less efficient.

Copy link
Collaborator

@phadej phadej Dec 24, 2022

Choose a reason for hiding this comment

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

That's a problem (to give an non contrived examples). I think in majority of relevant cases you'd rather make the field strict. In fact, there are people which would prefer Data.Map.Strict to be a separate datatype. (The type system doesn't help to keep the Map thunk free.)

i.e. while over' is useful, I don't think it should be commonly used.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The only case I can think out of my head is Seq. Where over' traverse doesn't leave thunks, but AFAIK ruins asymptotics.

Copy link
Contributor Author

@treeowl treeowl Dec 26, 2022

Choose a reason for hiding this comment

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

@phadej over' could quite reasonably be used with ix for Array, SmallArray (if that has an instance), Vector, Seq, Map, or IntMap. I imagine some folks just use at instead, but that's semantically overkill (and potentially expensive) for just forcing.

@phadej
Copy link
Collaborator

phadej commented Dec 24, 2022

This "solves" #944

@treeowl
Copy link
Contributor Author

treeowl commented Dec 26, 2022

It occurs to me that there's another approach to this issue, and it's not entirely clear whether one or the other is better. First, an exceedingly simple monad transformer that probably belongs in transformers:

newtype SoloT m a = SoloT
  { runSoloT :: m (Solo a) }

-- Note: in the below instances, clever implementations of <$,
-- <*, *>, or >> will almost certainly be wrong; the defaults are
-- correct.
instance Monad m => Functor (SoloT m) where
  fmap = liftM
instance Monad m => Applicative (SoloT m) where
  pure = SoloT . pure . Solo
  liftA2 = liftM2
  (<*>) = ap
instance Monad m => Monad (SoloT m) where
  m >>= f = SoloT $ runSoloT m >>= \(Solo a) -> runSoloT (f a)
instance MonadTrans SoloT where
  lift = SoloT . fmap Solo

Now, we can strictify optics in a monadic context:

strictly :: Monad m => LensLike (SoloT m) s t a b -> LensLike m s t a b
strictly l f s = runSoloT (l (\a -> SoloT $ f a >>= \ !b -> pure $ Solo b) s) >>= \(Solo t) -> pure t

In particular, we can write

overly' :: LensLike (SoloT Identity) s t a b -> (a -> b) -> s -> t
overly' l = over (strictly l)

I still think a direct implementation of over' is better for its exact purpose, but maybe strictly is also a good thing to have on hand?

@treeowl
Copy link
Contributor Author

treeowl commented Dec 26, 2022

strictly seems stricter, actually, which may be a bad thing. Let's stick with over' stuff for now, and think through the rest later.

@treeowl
Copy link
Contributor Author

treeowl commented Dec 26, 2022

Sorry, no it's not. This stuff is confusing! Let's consider both.

@treeowl
Copy link
Contributor Author

treeowl commented Dec 26, 2022

Ah, now I understand why that felt off. The monad transformer is the wrong thing, but there's a very similar (slightly lazier) applicative transformer that I believe does exactly what I mean.

type BoxT :: (Type -> Type) -> Type -> Type
newtype BoxT m a = BoxT
  { runBoxT :: m (Solo a) }
  deriving (Functor)

instance Apply m => Apply (BoxT m) where
  liftF2 f (BoxT m) (BoxT n) = BoxT (liftF2 (liftA2 f) m n) 
  {-# INLINE liftF2 #-}
instance Applicative m => Applicative (BoxT m) where
  pure = BoxT . pure . Solo
  {-# INLINE pure #-} 
  BoxT m <*> BoxT n = BoxT (liftA2 (<*>) m n)
  {-# INLINE (<*>) #-}
#if MIN_VERSION_base(4,10,0)
  liftA2 f (BoxT m) (BoxT n) = BoxT (liftA2 (liftA2 f) m n)
  {-# INLINE liftA2 #-}
#endif

Now, we can be exceedingly general:

strictly :: (Functor m, Profunctor p, Profunctor q) => Optical p q (BoxT m) s t a b -> Optical p q m s t a b
strictly l f = rmap (fmap getSolo .# runBoxT) $ l (rmap (\mb -> BoxT ((Solo $!) <$> mb)) f)
{-# INLINE strictly #-}

-- 'Setter'.
newtype BoxT m a = BoxT
{ runBoxT :: m (Solo a) }
deriving (Functor, Foldable, Traversable)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Where should this type be defined and exported?

Copy link
Collaborator

Choose a reason for hiding this comment

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

So you don't want to wait until SoloT is added somewhere, as you proposed in private mail to me and Ross (maintainer of transformers)?

I understand that you have time to work on stuff during holidays, but I honestly don't, so will politely ignore you until the next year.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

SoloT turned out to be the wrong sort of transformer for this purpose. I made a mistake and changed course.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Please tell that to others in that email message too. I wasn't aware that I don't need to think about that anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@treeowl treeowl force-pushed the strict-over branch 3 times, most recently from 5083a08 to b1be2ff Compare December 27, 2022 00:37
@treeowl
Copy link
Contributor Author

treeowl commented Dec 27, 2022

I don't know how much to overgeneralize things. We could, for example, write

over' :: (Profunctor p, Profunctor q) => Optical p q Solo s t a b -> p a b -> q s t
over' l f = rmap getSolo $ l (rmap (Solo $!) f)

Is that more useful or more confusing?

Copy link
Collaborator

@phadej phadej left a comment

Choose a reason for hiding this comment

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

This pull request has overgrown what I feel comfortable to comment about. I'd ask @ekmett to review this.

@phadej
Copy link
Collaborator

phadej commented Dec 27, 2022

I don't know how much to overgeneralize things. We could, for example, write

over' :: (Profunctor p, Profunctor q) => Optical p q Solo s t a b -> p a b -> q s t
over' l f = rmap getSolo $ l (rmap (Solo $!) f)

Is that more useful or more confusing?

Will over' traverse (+1) [1,2,3] still work?

@treeowl
Copy link
Contributor Author

treeowl commented Dec 27, 2022

I don't know how much to overgeneralize things. We could, for example, write

over' :: (Profunctor p, Profunctor q) => Optical p q Solo s t a b -> p a b -> q s t
over' l f = rmap getSolo $ l (rmap (Solo $!) f)

Is that more useful or more confusing?

Will over' traverse (+1) [1,2,3] still work?

Yes. p and q both specialize to ->, so rmap becomes (.).

@phadej
Copy link
Collaborator

phadej commented Dec 27, 2022

So can over be of Optical p q Identity s t a b -> p a b -> q s t form? I'd say it's both or neither.

@treeowl
Copy link
Contributor Author

treeowl commented Dec 27, 2022

In that scheme, over would become

over :: (Profunctor p, Profunctor q) => Optical p q Identity s t a b -> p a b -> q s t
over l f = runIdentity #. l (Identity #. f)

@treeowl
Copy link
Contributor Author

treeowl commented Dec 28, 2022

It occurs to me that when used in a non-trivial functor, the traversal produced by strictly will always leave thunks in it. Only forcing those thunks from the outside will actually force the targets. This can be avoided if the ambient functor is either a Traversable or a Monad:

strictlyT :: (Traversable f, Profunctor p, Profunctor q) => Optical p q (BoxT f) s t a b -> Optical p q f s t a b
strictlyT l f = rmap (getSolo . sequenceA .# runBoxT) $ l (rmap (BoxT #. fmap (Solo $!)) f)

strictlyM :: (Monad f, Profunctor p, Profunctor q) => Optical p q (BoxT f) s t a b -> Optical p q f s t a b
strictlyM l f = rmap ((>>= \(Solo r) -> pure r) .# runBoxT) $ l (rmap (BoxT #. fmap (Solo $!)) f)

Is either of these worth having?

* Add `strictly` to turn a lazy (standard) traversal into a strict one
  that forces targets before installing them.
* Add `over'`, `iover'`, `modifying'`, `imodifying'`, and corresponding
  operators `%!~`, `%!@~`, `%!=`, and `%!@=`.
* Adjust documentation.

Closes ekmett#1016
@ekmett
Copy link
Owner

ekmett commented Dec 28, 2022

i think they are worth having, i hate the names, but not the functionality.

@treeowl
Copy link
Contributor Author

treeowl commented Dec 28, 2022

@ekmett should the monadic version use the SoloT monad transformer instead of the BoxT applicative transformer? My guess is yes, but I probably need to play with some benchmarks.

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.

Add a strict over?
3 participants