----------------------------------------------------------------------------- -- | -- Module : Miso.Lens -- Copyright : (C) 2016-2025 David M. Johnson -- License : BSD3-style (see the file LICENSE) -- Maintainer : David M. Johnson <code@dmj.io> -- Stability : experimental -- Portability : non-portable -- -- This modules exposes a very simple 'Lens' formulation that is compatible with other lens libraries. -- -- For state management of miso applications, this module should meet all of your needs. It also -- ensures a smaller payload size during compilation. -- -- @ -- data Lens record field -- = Lens -- { _get :: record -> field -- , _set :: record -> field -> record -- } -- @ -- -- The goal is to provide users with an out-of-the box lens experience without the large -- dependency footprint and cognitive load. This module also aims to preserve semantics of -- existing lens combinators using a simple formulation (not the Van Laarhoven). It must be imported -- separately (@import Miso.Lens@) and can be used with the @Effect@ Monad inside of a miso -- application (as described below). -- -- This module is at fixity and interface parity with @lens@ and @microlens@ and can therefore -- be used interchangeably with them. Simply replace the @Miso.Lens@ import with @Control.Lens@. -- For convenience we re-export the 'Lens'' synonym to ease the transition into @lens@ or -- @microlens@. -- -- For the curious reader, if you'd like more information on @lens@ and the Van Laarhoven -- formulation, we recommend the @lens@ library <https://hackage.haskell.org/package/lens>. -- -- > -- Person type -- > data Person = Person -- > { _name :: String -- > , _address :: Address -- > , _age :: Int -- > } deriving (Show, Eq, Generic) -- -- > -- Address type -- > newtype Address -- > = Address -- > { _zipCode :: Zip -- > } deriving (Show, Eq) -- -- > -- | Zip code type synonym -- > type Zip = String -- -- > -- | Name Lens -- > name :: Lens Person String -- > name = lens _name $ \record x -> record { _name = x } -- -- > -- | Address Lens -- > address :: Lens Person Address -- > address = lens _address $ \record x -> record { _address = x } -- -- > -- | Zip Code Lens -- > zipCode :: Lens Address Zip -- > zipCode = lens _zipCode $ \record x -> record { _zipCode = x } -- -- > -- | Lens Composition example -- > personZip :: Lens Person Zip -- > personZip = zipCode . address -- -- > -- | Person example -- > person :: Person -- > person = Person "john" (Address "90210") 33 -- -- > main :: IO () -- > main = print $ john '&' address '.~' Address "10012" -- -- > Person -- > { _name = "john" -- > , _age = 33 -- > , _address = Address {_zipCode = "10012"} -- > } -- -- Example usage with miso's @Effect@ @Monad@ -- -- > newtype Model = Model { _value :: Int } -- -- > value :: Lens Model Int -- > value = lens _value $ \model v -> model { _value = v } -- -- > data Action = AddOne | SubtractOne -- -- > updateModel :: Action -> Effect Model Action () -- > updateModel AddOne = value += 1 -- > updateModel SubtractOne = value -= 1 -- ---------------------------------------------------------------------------- module Miso.Lens ( -- ** Types Lens (..) , Lens' -- ** Smart constructor , lens -- ** Utils , (<&>) -- ** Re-exports , (&) -- ** Combinators , (.~) , (?~) , set , (%~) , over , (^.) , (+~) , (*~) , (//~) , (-~) , (%=) , modifying , (+=) , (*=) , (//=) , (-=) , (.=) , (<~) , (<%=) , (<.=) , (<?=) , (<<.=) , (<<%=) , assign , use , (?=) , (<>~) ) where ---------------------------------------------------------------------------- import Control.Monad.State (MonadState, modify, get) import Control.Category (Category (..)) import Control.Arrow ((<<<)) import Data.Function ((&)) ---------------------------------------------------------------------------- -- | A @Lens@ is a generalized getter and setter. -- -- Lenses allow both the retrieval of values from fields in a record and the -- assignment of values to fields in a record. The power of a @Lens@ comes -- from its ability to be composed with other lenses. -- -- In the context of building applications with miso, the @model@ is -- often a deeply nested product type. This makes it highly conducive -- to @Lens@ operations (as defined below). -- data Lens record field = Lens { _get :: record -> field -- ^ Retrieves a field from a record , _set :: record -> field -> record -- ^ Sets a field on a record } ---------------------------------------------------------------------------- -- | Type synonym re-export for @lens@ / @microlens@ compatability. -- Note: use this if you plan on migrating to lens or microlens eventually. -- Just use @Lens@ otherwise (as examples show). type Lens' record field = Lens record field ---------------------------------------------------------------------------- -- | Lens are Categories, and can therefore be composed. instance Category Lens where id = Lens (\x -> x) (\x _ -> x) Lens g1 s1 . Lens g2 s2 = Lens { _get = g1 <<< g2 , _set = \r f -> s2 r (s1 (g2 r) f) } ---------------------------------------------------------------------------- -- | Set a field on a record -- -- > newtype Person = Person { _name :: String } -- -- > name :: Lens Person String -- > name = lens _name $ \person n -> person { _name = n } -- -- > setName :: Person -> String -> Person -- > setName person newName = person & name .~ newName -- infixr 4 .~ (.~) :: Lens record field -> field -> record -> record (.~) _lens = flip (_set _lens) ---------------------------------------------------------------------------- -- | Synonym for @(.~)@ -- set :: Lens record field -> field -> record -> record set = (.~) ---------------------------------------------------------------------------- -- | Set an options field on a record -- -- > newtype Person = Person { _name :: Maybe String } -- -- > name :: Lens Person (Maybe String) -- > name = lens _name $ \person n -> person { _name = n } -- -- > setName :: Person -> String -> Person -- > setName person newName = person & name ?~ newName -- infixr 4 ?~ (?~) :: Lens record (Maybe field) -> field -> record -> record (?~) _lens f r = r & _lens .~ Just f ---------------------------------------------------------------------------- -- | Modify a field on a record -- -- > \x -> record & field %~ f x -- infixr 4 %~ (%~) :: Lens record field -> (field -> field) -> record -> record (%~) _lens f record = _set _lens record $ f (record ^. _lens) ---------------------------------------------------------------------------- -- | Synonym for (%~) over :: Lens record field -> (field -> field) -> record -> record over = (%~) ---------------------------------------------------------------------------- -- | Read a field from a record using a @Lens@ -- -- > newtype Person = Person { _name :: String } -- > deriving (Show, Eq) -- -- > name :: Lens Person String -- > name = lens _name $ \person n -> person { _name = n } -- -- > getName :: Person -> String -- > getName = person ^. name -- infixl 8 ^. (^.) :: record -> Lens record field -> field (^.) = flip _get ---------------------------------------------------------------------------- -- | Increment a @Num@eric field on a record using a @Lens@ -- -- > newtype Person = Person { _age :: Int } -- -- > age :: Lens Person Int -- > age = lens _age $ \person a -> person { _age = a } -- -- > birthday :: Person -> Person -- > birthday person = person & age +~ 1 -- infixr 4 +~ (+~) :: Num field => Lens record field -> field -> record -> record (+~) _lens x record = record & _lens %~ (+x) ---------------------------------------------------------------------------- -- | Multiply a @Num@eric field on a record using a @Lens@ -- -- > newtype Circle = Circle { _radius :: Int } -- -- > radius :: Lens Circle Int -- > radius = lens _radius $ \circle r -> circle { _radius = r } -- -- > expand :: Circle -> Circle -- > expand circle = circle & radius *~ 10 -- infixr 4 *~ (*~) :: Num field => Lens record field -> field -> record -> record (*~) _lens x record = record & _lens %~ (*x) ---------------------------------------------------------------------------- -- | Divide a @Fractional@ field on a record using a @Lens@ -- -- > newtype Circle = Circle { _radius :: Int } -- -- > radius :: Lens Circle Int -- > radius = lens _radius $ \circle r -> circle { _radius = r } -- -- > expand :: Circle -> Circle -- > expand circle = circle & radius *~ 10 -- infixr 4 //~ (//~) :: Fractional field => Lens record field -> field -> record -> record (//~) _lens x record = record & _lens %~ (/x) ---------------------------------------------------------------------------- -- | Increment a @Num@eric field on a record using a @Lens@ -- -- > newtype Person = Person { _age :: Int } -- -- > age :: Lens Person Int -- > age = lens _age $ \person a -> person { _age = a } -- -- > timeTravel :: Person -> Person -- > timeTravel person = person & age -~ 1 -- infixr 4 -~ (-~) :: Num field => Lens record field -> field -> record -> record (-~) _lens x record = record & _lens %~ subtract x ---------------------------------------------------------------------------- -- | Monoidally append a field in a record using a @Lens@ -- -- > newtype List = List { _values :: [Int] } -- -- > values :: Lens List [Int] -- > values = lens _values $ \l vs -> l { _values = vs } -- -- > addElement :: List -> List -- > addElement list = list & values <>~ [2] -- -- > addElement (List []) -- > -- List [2] -- infixr 4 <>~ (<>~) :: Monoid field => Lens record field -> field -> record -> record (<>~) _lens x record = record & _lens %~ (<> x) ---------------------------------------------------------------------------- -- | Execute a monadic action in @MonadState@ that returns a field. Sets the -- return value equal to the field in the record. -- -- > newtype List = List { _values :: [Int] } -- -- > values :: Lens List [Int] -- > values = lens _values $ \l vs -> l { _values = vs } -- > -- > addElement :: List -> List -- > addElement list = list & values <>~ [2] -- infixr 2 <~ (<~) :: MonadState record m => Lens record field -> m field -> m () l <~ mb = do b <- mb l .= b ---------------------------------------------------------------------------- -- | Modify a record in @MonadState@ monad at a field using a @Lens@ -- -- > newtype Model = Model { _value :: Int } -- -- > data Action = AddOne | SubtractOne -- -- > value :: Lens Model Int -- > value = lens _value $ \p x -> p { _value = x } -- -- > update :: Action -> Effect Model Action () -- > update AddOne = do -- > value %= (+1) -- infix 4 %= (%=) :: MonadState record m => Lens record field -> (field -> field) -> m () (%=) _lens f = modify (\r -> r & _lens %~ f) ---------------------------------------------------------------------------- -- | Synonym for (%=) modifying :: MonadState record m => Lens record field -> (field -> field) -> m () modifying = (%=) ---------------------------------------------------------------------------- -- | Modify the field of a record in @MonadState@ using a @Lens@, then -- return the newly modified field from the updated record. -- -- > import Miso.String (ms) -- -- > newtype Model = Model { _value :: Int } -- > deriving (Show) -- -- > data Action = AddOne -- -- > value :: Lens Model Int -- > value = lens _value $ \p x -> p { _value = x } -- -- > update :: Action -> Effect Model Action () -- > update AddOne = do -- > result <- value <%= (+1) -- > io $ consoleLog (ms result) -- infix 4 <%= (<%=) :: MonadState record m => Lens record field -> (field -> field) -> m field l <%= f = do l %= f use l ---------------------------------------------------------------------------- -- | Assign the field of a record in @MonadState@ to a value using a @Lens@ -- Return the value after assignment. -- -- > import Miso.String (ms) -- -- > newtype Model = Model { _value :: Int } -- -- > data Action = Assign Int -- -- > value :: Lens Model Int -- > value = lens _value $ \p x -> p { _value = x } -- -- > update :: Action -> Effect Model Action () -- > update (Assign x) = do -- > result <- value <.= x -- > io $ consoleLog (ms result) -- x -- infix 4 <.= (<.=) :: MonadState record m => Lens record field -> field -> m field l <.= b = do l .= b return b ---------------------------------------------------------------------------- -- | Assign the field of a record in a @MonadState@ to a value (wrapped in a 'Just') -- using a @Lens@. Return the value after assignment. -- -- > import Miso.String (ms) -- -- > newtype Model = Model { _value :: Maybe Int } -- -- > data Action = SetValue Int -- -- > value :: Lens Model (Maybe Int) -- > value = lens _value $ \p x -> p { _value = x } -- -- > update :: Action -> Effect Model Action () -- > update (SetValue x) = do -- > result <- value <?= x -- > io $ consoleLog (ms result) -- Just 1 -- infix 4 <?= (<?=) :: MonadState record m => Lens record (Maybe field) -> field -> m field l <?= b = do l .= Just b return b ---------------------------------------------------------------------------- -- | Assign the field of a record in a @MonadState@ to a value using a @Lens@. -- Returns the /previous/ value, before assignment. -- -- > import Miso.String (ms) -- -- > newtype Model = Model { _value :: Int } -- > deriving (Show, Eq) -- -- > data Action = Assign Int -- > deriving (Show, Eq) -- -- > value :: Lens Model Int -- > value = lens _value $ \p x -> p { _value = x } -- -- > update :: Action -> Effect Model Action () -- > update (Assign x) = do -- > value .= x -- > previousValue <- value <<.= 1 -- > io $ consoleLog $ ms (show previousValue) -- prints value at x -- infix 4 <<.= (<<.=) :: MonadState record m => Lens record field -> field -> m field l <<.= b = do old <- use l l .= b return old ---------------------------------------------------------------------------- -- | Modifies the field of a record in @MonadState@ using a @Lens@. -- Returns the /previous/ value, before modification. -- -- > import Miso.String (ms) -- -- > newtype Model = Model { _value :: Int } -- > deriving (Show, Eq) -- -- > data Action = Modify (Int -> Int) -- -- > value :: Lens Model Int -- > value = lens _value $ \p x -> p { _value = x } -- -- > update :: Action -> Effect Model Action () -- > update (Modify f) = do -- > value .= 2 -- > result <- value <<%= f -- > io $ consoleLog (ms (show result)) -- prints previous value of 2 -- infix 4 <<%= (<<%=) :: MonadState record m => Lens record field -> (field -> field) -> m field l <<%= f = do old <- use l l %= f return old ---------------------------------------------------------------------------- -- | Sets the value of a field in a record using @MonadState@ and a @Lens@ -- -- > import Miso.String (ms) -- -- > newtype Model = Model { _value :: Int } -- > deriving (Show, Eq) -- -- > data Action = SetValue Int -- -- > value :: Lens Model Int -- > value = lens _value $ \p x -> p { _value = x } -- -- > update' :: Action -> Effect Model Action () -- > update' (SetValue v) = value .= v -- infix 4 .= (.=) :: MonadState record m => Lens record field -> field -> m () (.=) _lens f = modify (\r -> r & _lens .~ f) ---------------------------------------------------------------------------- -- | Synonym for (.=) assign :: MonadState record m => Lens record field -> field -> m () assign = (.=) ---------------------------------------------------------------------------- -- | Retrieves the value of a field in a record using a @Lens@ inside @MonadState@ -- -- > import Miso.String (ms) -- -- > newtype Model = Model { _value :: Int } -- > deriving (Show, Eq) -- -- > data Action = SetValue Int -- -- > value :: Lens Model Int -- > value = lens _value $ \p x -> p { _value = x } -- -- > update :: Action -> Effect Model Action () -- > update (SetValue x) = do -- > value .= x -- > result <- use value -- > io $ consoleLog (ms (show result)) -- prints the value of 'x' -- use :: MonadState record m => Lens record field -> m field use _lens = (^. _lens) <$> get ---------------------------------------------------------------------------- -- | Sets the value of a field in a record using a @Lens@ inside a @MonadState@ -- The value is wrapped in a @Just@ before being assigned. -- -- > newtype Model = Model { _value :: Maybe Int } -- > deriving (Show, Eq) -- -- > data Action = AssignValue Int -- -- > value :: Lens Model (Maybe Int) -- > value = lens _value $ \p x -> p { _value = x } -- -- > update :: Action -> Effect Model Action () -- > update (AssignValue x) = value ?= x -- infix 4 ?= (?=) :: MonadState record m => Lens record (Maybe field) -> field -> m () (?=) _lens value = _lens .= Just value ---------------------------------------------------------------------------- -- | Increments the value of a @Num@eric field of a record using a @Lens@ -- inside a @State@ Monad. -- -- > newtype Model = Model { _value :: Int } -- > deriving (Show, Eq) -- -- > data Action = IncrementBy Int -- -- > value :: Lens Model Int -- > value = lens _value $ \p x -> p { _value = x } -- -- > update :: Action -> Effect Model Action () -- > update (IncrementBy x) = value += x -- infix 4 += (+=) :: (MonadState record m, Num field) => Lens record field -> field -> m () (+=) _lens f = modify (\r -> r & _lens +~ f) ---------------------------------------------------------------------------- -- | Multiplies the value of a @Num@eric field of a record using a @Lens@ -- inside a @State@ Monad. -- -- > newtype Model = Model { _value :: Int } -- > deriving (Show, Eq) -- -- > data Action = MultiplyBy Int -- -- > value :: Lens Model Int -- > value = lens _value $ \p x -> p { _value = x } -- -- > update :: Action -> Effect Model Action () -- > update (MultiplyBy x) = value *= x -- infix 4 *= (*=) :: (MonadState record m, Num field) => Lens record field -> field -> m () (*=) _lens f = modify (\r -> r & _lens *~ f) ---------------------------------------------------------------------------- -- | Divides the value of a @Fractional@ field of a record using a @Lens@ -- inside a @State@ Monad. -- -- > newtype Model = Model { _value :: Double } -- > deriving (Show, Eq) -- -- > data Action = DivideBy Double -- -- > value :: Lens Model Double -- > value = lens _value $ \p x -> p { _value = x } -- -- > update :: Action -> Effect Model Action () -- > update (DivideBy x) = value //= x -- infix 4 //= (//=) :: (MonadState record m, Fractional field) => Lens record field -> field -> m () (//=) _lens f = modify (\r -> r & _lens %~ (/ f)) ---------------------------------------------------------------------------- -- | Subtracts the value of a @Num@eric field of a record using a @Lens@ -- inside of a @State@ Monad. -- -- > newtype Model = Model { _value :: Double } -- > deriving (Show, Eq) -- -- > data Action = SubtractBy Double -- -- > value :: Lens Model Double -- > value = lens _value $ \p x -> p { _value = x } -- -- > update :: Action -> Effect Model Action () -- > update (SubtractBy x) = value -= x -- infix 4 -= (-=) :: (MonadState record m, Num field) => Lens record field -> field -> m () (-=) _lens f = modify (\r -> r & _lens -~ f) --------------------------------------------------------------------------------- -- | Smart constructor @lens@ function. Used to easily construct a @Lens@ -- -- > name :: Lens Person String -- > name = lens _name $ \p n -> p { _name = n } -- lens :: (record -> field) -> (record -> field -> record) -> Lens record field lens = Lens ---------------------------------------------------------------------------- -- | Functor utility, a flipped infix @fmap@. infixl 1 <&> (<&>) :: Functor f => f a -> (a -> b) -> f b f <&> x = x <$> f ----------------------------------------------------------------------------