Skip to content

Commit

Permalink
Restrict over', iover', and set' to traversals
Browse files Browse the repository at this point in the history
* `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 well-typed#473
  • Loading branch information
treeowl committed Dec 28, 2022
1 parent a3b2d99 commit 7a7585d
Show file tree
Hide file tree
Showing 9 changed files with 49 additions and 49 deletions.
6 changes: 6 additions & 0 deletions optics-core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# optics-core-0.5 (???)
* Restrict `over'`, `iover'`, `set'`, and associated operators to require
traversals rather than just setters. Setters are not capable of actually
making strict modifications, so these operations were just silently lazier
than expected when passed setters.

# optics-core-0.4.1 (2022-03-22)
* Add support for GHC-9.2
* Add `is` ([#410](https://github.com/well-typed/optics/pull/410))
Expand Down
34 changes: 1 addition & 33 deletions optics-core/src/Optics/Internal/Utils.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@
-- | This module is intended for internal use only, and may change without warning
-- in subsequent releases.
module Optics.Internal.Utils
( Solo (..)
, wrapSolo'
, getSolo

, Traversed(..)
( Traversed(..)
, runTraversed

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

import Data.Profunctor.Indexed
import Data.Tuple.Solo (Solo (..), getSolo)

-- Needed for strict application of (indexed) setters.
--
-- Credit for this goes to Eric Mertens, see
-- <https://github.com/glguy/irc-core/commit/2d5fc45b05f1>.

instance Mapping (Star Solo) where
roam f (Star k) = Star $ wrapSolo' . f (getSolo . k)
iroam f (Star k) = Star $ wrapSolo' . f (\_ -> getSolo . k)

instance Mapping (IxStar Solo) where
roam f (IxStar k) =
IxStar $ \i -> wrapSolo' . f (getSolo . k i)
iroam f (IxStar k) =
IxStar $ \ij -> wrapSolo' . f (\i -> getSolo . k (ij i))

-- | Mark a value for evaluation to whnf.
--
-- This allows us to, when applying a setter to a structure, evaluate only the
-- parts that we modify. If an optic focuses on multiple targets, Applicative
-- instance of Identity' makes sure that we force evaluation of all of them, but
-- we leave anything else alone.
--
wrapSolo' :: a -> Solo a
wrapSolo' a = Solo $! a

----------------------------------------

-- | Helper for 'Optics.Fold.traverseOf_' and the like for better
-- efficiency than the foldr-based version.
Expand Down
7 changes: 4 additions & 3 deletions optics-core/src/Optics/IxSetter.hs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ module Optics.IxSetter
) where

import Data.Profunctor.Indexed
import Data.Tuple.Solo (Solo (..), getSolo)

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

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

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

-- | Apply an indexed setter, strictly.
iset'
:: (Is k A_Setter, is `HasSingleIndex` i)
:: (Is k A_Traversal, is `HasSingleIndex` i)
=> Optic k is s t a b
-> (i -> b) -> s -> t
iset' o = \f -> iover' o (\i _ -> f i)
Expand Down
3 changes: 2 additions & 1 deletion optics-core/src/Optics/IxTraversal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ import Control.Monad.Trans.State
import Data.Functor.Identity

import Data.Profunctor.Indexed
import Data.Tuple.Solo (Solo (..))

import Optics.Internal.Indexed
import Optics.Internal.Indexed.Classes
Expand Down Expand Up @@ -227,7 +228,7 @@ ifailover'
=> Optic k is s t a b
-> (i -> a -> b) -> s -> Maybe t
ifailover' o = \f s ->
let OrT visited t = itraverseOf o (\i -> wrapOrT . wrapSolo' . f i) s
let OrT visited t = itraverseOf o (\i -> wrapOrT . (Solo $!) . f i) s
in if visited
then case t of Solo v -> Just v
else Nothing
Expand Down
7 changes: 4 additions & 3 deletions optics-core/src/Optics/Operators.hs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import Optics.Getter
import Optics.Optic
import Optics.Review
import Optics.Setter
import Optics.Traversal

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

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

Expand All @@ -79,7 +80,7 @@ infixr 4 %!~
infixr 4 .~

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

Expand All @@ -103,7 +104,7 @@ infixr 4 !~
infixr 4 ?~

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

Expand Down
27 changes: 21 additions & 6 deletions optics-core/src/Optics/Setter.hs
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ module Optics.Setter
) where

import Data.Profunctor.Indexed
import Data.Tuple.Solo (Solo (..), getSolo)

import Optics.Internal.Optic
import Optics.Internal.Setter
import Optics.Internal.Utils

-- | Type synonym for a type-modifying setter.
type Setter s t a b = Optic A_Setter NoIx s t a b
Expand Down Expand Up @@ -102,14 +102,28 @@ over o = \f -> runFunArrow $ getOptic (castOptic @A_Setter o) (FunArrow f)
-- 'over' is used, because the first coordinate of a pair is never forced.
--
over'
:: Is k A_Setter
:: Is k A_Traversal
=> Optic k is s t a b
-> (a -> b) -> s -> t
-- See [Note: Solo wrapping]
over' o = \f ->
let star = getOptic (castOptic @A_Setter o) $ Star (wrapSolo' . f)
let star = getOptic (castOptic @A_Traversal o) $ Star ((Solo $!) . f)
in getSolo . runStar star
{-# INLINE over' #-}

-- Note: Solo wrapping
--
-- We use Solo for strict application of (indexed) setters.
--
-- Credit for idea this goes to Eric Mertens, see
-- <https://github.com/glguy/irc-core/commit/2d5fc45b05f1>.
--
-- Using Solo rather than Identity allows us, when applying a traversal to a
-- structure, to evaluate only the parts that we modify. If an optic focuses on
-- multiple targets, the Applicative instance of Solo (combined with applying
-- the Solo data constructor strictly) makes sure that we force evaluation of
-- all of them, but we leave anything else alone.

-- | Apply a setter.
--
-- @
Expand All @@ -128,10 +142,11 @@ set o = over o . const

-- | Apply a setter, strictly.
--
-- TODO DOC: what exactly is the strictness property?
--
-- The new value will be forced if and only if the optic traverses at
-- least one target. If forcing the new value is inexpensive, then it
-- is cheaper to do so manually and use 'set'.
set'
:: Is k A_Setter
:: Is k A_Traversal
=> Optic k is s t a b
-> b -> s -> t
set' o = over' o . const
Expand Down
4 changes: 3 additions & 1 deletion optics-core/src/Optics/Traversal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ import Data.Bitraversable
import Data.Functor.Identity

import Data.Profunctor.Indexed
import Data.Tuple.Solo (Solo (..))

import Optics.AffineTraversal
import Optics.Fold
import Optics.Internal.Optic
Expand Down Expand Up @@ -305,7 +307,7 @@ failover'
=> Optic k is s t a b
-> (a -> b) -> s -> Maybe t
failover' o = \f s ->
let OrT visited t = traverseOf o (wrapOrT . wrapSolo' . f) s
let OrT visited t = traverseOf o (wrapOrT . (Solo $!) . f) s
in if visited
then case t of Solo v -> Just v
else Nothing
Expand Down
6 changes: 6 additions & 0 deletions optics-extra/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# optics-extra-0.5 (????)

* Restrict `modifying'` and `assign'` to traversals. Setters are not capable of
actually making strict modifications, so these operations were just silently
lazier than expected when passed setters.

# optics-extra-0.4.2.1 (2022-05-20)
* Fix for previous release when used with `mtl-2.3` and `transformers-0.5`.

Expand Down
4 changes: 2 additions & 2 deletions optics-extra/src/Optics/State.hs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ modifying o = modify . over o
-- >>> flip evalState ('a','b') $ modifying' _1 (errorWithoutStackTrace "oops")
-- *** Exception: oops
modifying'
:: (Is k A_Setter, MonadState s m)
:: (Is k A_Traversal, MonadState s m)
=> Optic k is s s a b
-> (a -> b)
-> m ()
Expand Down Expand Up @@ -75,7 +75,7 @@ assign o = modifying o . const
-- >>> flip evalState ('a','b') $ assign' _1 (errorWithoutStackTrace "oops")
-- *** Exception: oops
assign'
:: (Is k A_Setter, MonadState s m)
:: (Is k A_Traversal, MonadState s m)
=> Optic k is s s a b
-> b
-> m ()
Expand Down

0 comments on commit 7a7585d

Please sign in to comment.