Skip to content

Commit 40a38d2

Browse files
committed
Restrict over', iover', and set' to traversals
* `over'`, `iover'`, `set'`, and associated operators previously accepted setters. However, it's impossible to actually modify strictly through a setter; a traversal is needed for that. Restrict the types to require `A_Traversal`, and remove the associated (technically correct but deceptive) `Mapping` instances. * Document the strictness behavior of `set'`. Fixes #473
1 parent a3b2d99 commit 40a38d2

File tree

10 files changed

+57
-58
lines changed

10 files changed

+57
-58
lines changed

optics-core/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# optics-core-0.5 (???)
2+
* Restrict `over'`, `iover'`, `set'`, and associated operators to require
3+
traversals rather than just setters. Setters are not capable of actually
4+
making strict modifications, so these operations were just silently lazier
5+
than expected when passed setters.
6+
17
# optics-core-0.4.1 (2022-03-22)
28
* Add support for GHC-9.2
39
* Add `is` ([#410](https://github.com/well-typed/optics/pull/410))

optics-core/src/Optics/Internal/Utils.hs

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,7 @@
33
-- | This module is intended for internal use only, and may change without warning
44
-- in subsequent releases.
55
module Optics.Internal.Utils
6-
( Solo (..)
7-
, wrapSolo'
8-
, getSolo
9-
10-
, Traversed(..)
6+
( Traversed(..)
117
, runTraversed
128

139
, OrT(..)
@@ -21,34 +17,6 @@ module Optics.Internal.Utils
2117
import qualified Data.Semigroup as SG
2218

2319
import Data.Profunctor.Indexed
24-
import Data.Tuple.Solo (Solo (..), getSolo)
25-
26-
-- Needed for strict application of (indexed) setters.
27-
--
28-
-- Credit for this goes to Eric Mertens, see
29-
-- <https://github.com/glguy/irc-core/commit/2d5fc45b05f1>.
30-
31-
instance Mapping (Star Solo) where
32-
roam f (Star k) = Star $ wrapSolo' . f (getSolo . k)
33-
iroam f (Star k) = Star $ wrapSolo' . f (\_ -> getSolo . k)
34-
35-
instance Mapping (IxStar Solo) where
36-
roam f (IxStar k) =
37-
IxStar $ \i -> wrapSolo' . f (getSolo . k i)
38-
iroam f (IxStar k) =
39-
IxStar $ \ij -> wrapSolo' . f (\i -> getSolo . k (ij i))
40-
41-
-- | Mark a value for evaluation to whnf.
42-
--
43-
-- This allows us to, when applying a setter to a structure, evaluate only the
44-
-- parts that we modify. If an optic focuses on multiple targets, Applicative
45-
-- instance of Identity' makes sure that we force evaluation of all of them, but
46-
-- we leave anything else alone.
47-
--
48-
wrapSolo' :: a -> Solo a
49-
wrapSolo' a = Solo $! a
50-
51-
----------------------------------------
5220

5321
-- | Helper for 'Optics.Fold.traverseOf_' and the like for better
5422
-- efficiency than the foldr-based version.

optics-core/src/Optics/IxSetter.hs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ module Optics.IxSetter
5858
) where
5959

6060
import Data.Profunctor.Indexed
61+
import Data.Tuple.Solo (Solo (..), getSolo)
6162

6263
import Optics.Internal.Indexed
6364
import Optics.Internal.Indexed.Classes
@@ -81,11 +82,11 @@ iover o = \f -> runIxFunArrow (getOptic (castOptic @A_Setter o) (IxFunArrow f))
8182

8283
-- | Apply an indexed setter as a modifier, strictly.
8384
iover'
84-
:: (Is k A_Setter, is `HasSingleIndex` i)
85+
:: (Is k A_Traversal, is `HasSingleIndex` i)
8586
=> Optic k is s t a b
8687
-> (i -> a -> b) -> s -> t
8788
iover' o = \f ->
88-
let star = getOptic (castOptic @A_Setter o) $ IxStar (\i -> wrapSolo' . f i)
89+
let star = getOptic (castOptic @A_Traversal o) $ IxStar (\i -> (Solo $!) . f i)
8990
in getSolo . runIxStar star id
9091

9192
{-# INLINE iover' #-}
@@ -105,7 +106,7 @@ iset o = \f -> iover o (\i _ -> f i)
105106

106107
-- | Apply an indexed setter, strictly.
107108
iset'
108-
:: (Is k A_Setter, is `HasSingleIndex` i)
109+
:: (Is k A_Traversal, is `HasSingleIndex` i)
109110
=> Optic k is s t a b
110111
-> (i -> b) -> s -> t
111112
iset' o = \f -> iover' o (\i _ -> f i)

optics-core/src/Optics/IxTraversal.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ import Control.Monad.Trans.State
9797
import Data.Functor.Identity
9898

9999
import Data.Profunctor.Indexed
100+
import Data.Tuple.Solo (Solo (..))
100101

101102
import Optics.Internal.Indexed
102103
import Optics.Internal.Indexed.Classes
@@ -227,7 +228,7 @@ ifailover'
227228
=> Optic k is s t a b
228229
-> (i -> a -> b) -> s -> Maybe t
229230
ifailover' o = \f s ->
230-
let OrT visited t = itraverseOf o (\i -> wrapOrT . wrapSolo' . f i) s
231+
let OrT visited t = itraverseOf o (\i -> wrapOrT . (Solo $!) . f i) s
231232
in if visited
232233
then case t of Solo v -> Just v
233234
else Nothing

optics-core/src/Optics/Operators.hs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import Optics.Getter
2828
import Optics.Optic
2929
import Optics.Review
3030
import Optics.Setter
31+
import Optics.Traversal
3132

3233
-- | Flipped infix version of 'view'.
3334
(^.) :: Is k A_Getter => s -> Optic' k is s a -> a
@@ -65,7 +66,7 @@ infixr 8 #
6566
infixr 4 %~
6667

6768
-- | Infix version of 'over''.
68-
(%!~) :: Is k A_Setter => Optic k is s t a b -> (a -> b) -> s -> t
69+
(%!~) :: Is k A_Traversal => Optic k is s t a b -> (a -> b) -> s -> t
6970
(%!~) = over'
7071
{-# INLINE (%!~) #-}
7172

@@ -79,7 +80,7 @@ infixr 4 %!~
7980
infixr 4 .~
8081

8182
-- | Infix version of 'set''.
82-
(!~) :: Is k A_Setter => Optic k is s t a b -> b -> s -> t
83+
(!~) :: Is k A_Traversal => Optic k is s t a b -> b -> s -> t
8384
(!~) = set'
8485
{-# INLINE (!~) #-}
8586

@@ -103,7 +104,7 @@ infixr 4 !~
103104
infixr 4 ?~
104105

105106
-- | Strict version of ('?~').
106-
(?!~) :: Is k A_Setter => Optic k is s t a (Maybe b) -> b -> s -> t
107+
(?!~) :: Is k A_Traversal => Optic k is s t a (Maybe b) -> b -> s -> t
107108
(?!~) = \o !b -> set' o (Just b)
108109
{-# INLINE (?!~) #-}
109110

optics-core/src/Optics/Setter.hs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ module Optics.Setter
6363
) where
6464

6565
import Data.Profunctor.Indexed
66+
import Data.Tuple.Solo (Solo (..), getSolo)
6667

6768
import Optics.Internal.Optic
6869
import Optics.Internal.Setter
69-
import Optics.Internal.Utils
7070

7171
-- | Type synonym for a type-modifying setter.
7272
type Setter s t a b = Optic A_Setter NoIx s t a b
@@ -102,14 +102,28 @@ over o = \f -> runFunArrow $ getOptic (castOptic @A_Setter o) (FunArrow f)
102102
-- 'over' is used, because the first coordinate of a pair is never forced.
103103
--
104104
over'
105-
:: Is k A_Setter
105+
:: Is k A_Traversal
106106
=> Optic k is s t a b
107107
-> (a -> b) -> s -> t
108+
-- See [Note: Solo wrapping]
108109
over' o = \f ->
109-
let star = getOptic (castOptic @A_Setter o) $ Star (wrapSolo' . f)
110+
let star = getOptic (castOptic @A_Traversal o) $ Star ((Solo $!) . f)
110111
in getSolo . runStar star
111112
{-# INLINE over' #-}
112113

114+
-- Note: Solo wrapping
115+
--
116+
-- We use Solo for strict application of (indexed) setters.
117+
--
118+
-- Credit for this idea goes to Eric Mertens; see
119+
-- <https://github.com/glguy/irc-core/commit/2d5fc45b05f1>.
120+
--
121+
-- Using Solo rather than Identity allows us, when applying a traversal to a
122+
-- structure, to evaluate only the parts that we modify. If an optic focuses on
123+
-- multiple targets, the Applicative instance of Solo (combined with applying
124+
-- the Solo data constructor strictly) makes sure that we force evaluation of
125+
-- all of them, but we leave anything else alone.
126+
113127
-- | Apply a setter.
114128
--
115129
-- @
@@ -128,10 +142,11 @@ set o = over o . const
128142

129143
-- | Apply a setter, strictly.
130144
--
131-
-- TODO DOC: what exactly is the strictness property?
132-
--
145+
-- The new value will be forced if and only if the optic traverses at
146+
-- least one target. If forcing the new value is inexpensive, then it
147+
-- is cheaper to do so manually and use 'set'.
133148
set'
134-
:: Is k A_Setter
149+
:: Is k A_Traversal
135150
=> Optic k is s t a b
136151
-> b -> s -> t
137152
set' o = over' o . const

optics-core/src/Optics/Traversal.hs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ import Data.Bitraversable
105105
import Data.Functor.Identity
106106

107107
import Data.Profunctor.Indexed
108+
import Data.Tuple.Solo (Solo (..))
109+
108110
import Optics.AffineTraversal
109111
import Optics.Fold
110112
import Optics.Internal.Optic
@@ -305,7 +307,7 @@ failover'
305307
=> Optic k is s t a b
306308
-> (a -> b) -> s -> Maybe t
307309
failover' o = \f s ->
308-
let OrT visited t = traverseOf o (wrapOrT . wrapSolo' . f) s
310+
let OrT visited t = traverseOf o (wrapOrT . (Solo $!) . f) s
309311
in if visited
310312
then case t of Solo v -> Just v
311313
else Nothing

optics-extra/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# optics-extra-0.5 (????)
2+
* Restrict `modifying'` and `assign'` to traversals. Setters are not capable of
3+
actually making strict modifications, so these operations were just silently
4+
lazier than expected when passed setters.
5+
16
# optics-extra-0.4.2.1 (2022-05-20)
27
* Fix for previous release when used with `mtl-2.3` and `transformers-0.5`.
38

optics-extra/src/Optics/State.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ modifying o = modify . over o
4343
-- >>> flip evalState ('a','b') $ modifying' _1 (errorWithoutStackTrace "oops")
4444
-- *** Exception: oops
4545
modifying'
46-
:: (Is k A_Setter, MonadState s m)
46+
:: (Is k A_Traversal, MonadState s m)
4747
=> Optic k is s s a b
4848
-> (a -> b)
4949
-> m ()
@@ -75,7 +75,7 @@ assign o = modifying o . const
7575
-- >>> flip evalState ('a','b') $ assign' _1 (errorWithoutStackTrace "oops")
7676
-- *** Exception: oops
7777
assign'
78-
:: (Is k A_Setter, MonadState s m)
78+
:: (Is k A_Traversal, MonadState s m)
7979
=> Optic k is s s a b
8080
-> b
8181
-> m ()

optics/tests/Optics/Tests/Eta.hs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,19 +96,19 @@ eta7lhs = over mapped
9696
eta7rhs f = over mapped f
9797

9898
eta8lhs, eta8rhs
99-
:: Functor f => (a -> b) -> f a -> f b
100-
eta8lhs = over' mapped
101-
eta8rhs f = over' mapped f
99+
:: Traversable f => (a -> b) -> f a -> f b
100+
eta8lhs = over' traversed
101+
eta8rhs f = over' traversed f
102102

103103
eta9lhs, eta9rhs
104104
:: FunctorWithIndex i f => (i -> a -> b) -> f a -> f b
105105
eta9lhs = iover imapped
106106
eta9rhs f = iover imapped f
107107

108108
eta10lhs, eta10rhs
109-
:: FunctorWithIndex i f => (i -> a -> b) -> f a -> f b
110-
eta10lhs = iover' imapped
111-
eta10rhs f = iover' imapped f
109+
:: TraversableWithIndex i f => (i -> a -> b) -> f a -> f b
110+
eta10lhs = iover' itraversed
111+
eta10rhs f = iover' itraversed f
112112

113113
eta11lhs, eta11rhs
114114
:: (FunctorWithIndex i f, FunctorWithIndex j g)
@@ -117,7 +117,7 @@ eta11lhs = iset (imapped <%> imapped)
117117
eta11rhs f = iset (imapped <%> imapped) f
118118

119119
eta12lhs, eta12rhs
120-
:: (FunctorWithIndex i f, FunctorWithIndex j g)
120+
:: (TraversableWithIndex i f, TraversableWithIndex j g)
121121
=> ((i, j) -> b) -> f (g a) -> f (g b)
122-
eta12lhs = iset' (imapped <%> imapped)
123-
eta12rhs f = iset' (imapped <%> imapped) f
122+
eta12lhs = iset' (itraversed <%> itraversed)
123+
eta12rhs f = iset' (itraversed <%> itraversed) f

0 commit comments

Comments
 (0)