packages feed

proarrow-0.1.0.0: test/Examples/Vitrea.hs

{-# LANGUAGE AllowAmbiguousTypes #-}

-- | The examples that ship with the @vitrea@ library (Mario Román and Bartosz Milewski,
-- /Profunctor optics: a categorical update/), ported to proarrow's optics: lenses and a prism over
-- records, a type-changing lens, an algebraic lens and a kaleidoscope over (a slice of) the iris
-- data set, monadic lenses as ordinary lenses in a Kleisli category, and a traversal composed with
-- a prism and a lens.
module Examples.Vitrea (test) where

import Data.Char (toUpper)
import Data.Function (on)
import Data.List (minimumBy)
import Data.Maybe (fromMaybe)
import Data.Type.Nat (Nat4)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.Falsify (testProperty)
import Prelude hiding (Applicative (..), Functor (..), id, map, (.))

import Proarrow.Category.Instance.Kleisli (KLEISLI (..), Kleisli (..), arr)
import Proarrow.Category.Monoidal.Applicative (Applicative (..))
import Proarrow.Core (Promonad (..))
import Proarrow.Functor (Functor (..))
import Proarrow.Optic (convert)
import Proarrow.Optic.Action (ClassifyingLens, classifyingLens, (.?))
import Proarrow.Optic.Kaleidoscope (Kaleidoscope', cotraverseOf, kaleidoscopeOf)
import Proarrow.Optic.PowerGrate (PowerGrate', powerGrate, powerGrateOf)
import Proarrow.Optics
  ( Lens
  , Lens'
  , MonoidalLens
  , Prism'
  , Traversal
  , foldMapOf
  , lens
  , monLens
  , over
  , prism
  , review
  , set
  , traversed
  , view
  , (%)
  , (^?)
  )
import Proarrow.Profunctor.Instance.Costar (unCostar, pattern Costar)
import Proarrow.Profunctor.Instance.Star (Star, unStar, pattern Star)
import Proarrow.Profunctor.Representable (RepCostar (..))
import Props.Optic.Hask (assertEq)

-- * Example 1: lenses and prisms

data Address = Address
  { street' :: String
  , city' :: String
  , country' :: String
  }
  deriving (Show, Eq)

data Person = Person
  { name' :: String
  , home' :: Address
  }
  deriving (Show, Eq)

sherlock :: Person
sherlock =
  Person
    { name' = "Sherlock Holmes"
    , home' = Address{street' = "221b Baker Street", city' = "London", country' = "UK"}
    }

home :: Lens' Person Address
home = lens home' (\(p, a) -> p{home' = a})

street :: Lens' Address String
street = lens street' (\(a, s) -> a{street' = s})

city :: Lens' Address String
city = lens city' (\(a, c) -> a{city' = c})

-- | Parse an address from @"street, city, country"@, or fail with the original string.
asAddress :: Prism' String Address
asAddress = prism buildAddress matchAddress
  where
    buildAddress (Address s c r) = s ++ ", " ++ c ++ ", " ++ r
    matchAddress a = case splitOn ", " a of
      [s, c, r] -> Right (Address s c r)
      _ -> Left a

splitOn :: String -> String -> [String]
splitOn sep = go
  where
    n = length sep
    go s = case breakOn s of
      (h, Nothing) -> [h]
      (h, Just rest) -> h : go rest
    breakOn [] = ([], Nothing)
    breakOn s@(c : cs)
      | take n s == sep = ([], Just (drop n s))
      | otherwise = let (h, r) = breakOn cs in (c : h, r)

place :: String
place = "221b Baker St, London, UK"

-- * Example 1.2: a type-changing lens (the clock is an 'Int' so the tests stay pure)

data Timestamped a = Timestamped
  { created' :: Int
  , modified' :: Int
  , contents' :: a
  }
  deriving (Show, Eq)

contents :: Lens (Timestamped a) (Timestamped b) a b
contents = lens contents' (\(x, b) -> x{contents' = b})

-- * Example 2: an algebraic lens and a kaleidoscope over the iris data set

data Species = Setosa | Versicolor | Virginica deriving (Show, Eq)

data Measurements = Measurements
  { sepalLe :: Float
  , sepalWi :: Float
  , petalLe :: Float
  , petalWi :: Float
  }
  deriving (Show, Eq)

data Flower = Flower
  { measurements :: Measurements
  , species :: Species
  }
  deriving (Show, Eq)

-- | Classify a new set of measurements by the species of its nearest neighbour in a list of flowers:
-- an algebraic lens for the list monad, whose @put@ sees the whole list rather than one flower.
measure :: ClassifyingLens (Star []) Flower Flower Measurements Measurements
measure = classifyingLens measurements (uncurry learn)
  where
    distance :: Measurements -> Measurements -> Float
    distance (Measurements a b c d) (Measurements x y z w) = sqrt (sum (map (** 2) ([a - x, b - y, c - z, d - w] :: [Float])))
    learn l m = Flower m (species (minimumBy (compare `on` (distance m . measurements)) l))

-- | The four measurements as a power grate, so that any way of aggregating a list of
-- 'Float's aggregates a list of 'Measurements' field by field.
aggregate :: PowerGrate' Measurements Float
aggregate =
  powerGrate @Nat4
    (\(Measurements a b c d) -> (a, (b, (c, (d, ())))))
    (\(a, (b, (c, (d, ())))) -> Measurements a b c d)

-- | Distribute a list aggregator through the kaleidoscope, at the carrier @Costar []@.
aggregateWith :: ([Float] -> Float) -> [Measurements] -> Measurements
aggregateWith = unCostar . powerGrateOf aggregate . Costar

-- | Classify the /aggregate/ of a list of flowers: the classifying lens composed with the kaleidoscope
-- is a kaleidoscope again (a product by a monoid is applicative), run at the aggregating
-- carrier @Costar []@ (vitrea's @iris & measure . aggregate >- mean@).
classifyAggregate :: ([Float] -> Float) -> [Flower] -> Flower
classifyAggregate = unCostar . kaleidoscopeOf measureAggregate . Costar

-- | The same composite, stored at its named flavor: Román's kaleidoscope.
measureAggregate :: Kaleidoscope' Flower Float
measureAggregate = convert (measure % aggregate)

-- | The kaleidoscope is also a cotraversal: pass a finite 'Cotraversable' carrier through it,
-- here @Maybe a -> b@, i.e. @RepCostar (Star Maybe)@.
classifyOptional :: (Maybe Float -> Float) -> Maybe Flower -> Flower
classifyOptional f = unRepCostar (cotraverseOf measureAggregate (RepCostar @_ @(Star Maybe) f))

mean :: [Float] -> Float
mean l = sum l / fromIntegral (length l)

setosa1 :: Flower
setosa1 = Flower (Measurements 5.1 3.5 1.4 0.2) Setosa

iris :: [Flower]
iris =
  [ setosa1
  , Flower (Measurements 4.9 3.0 1.4 0.2) Setosa
  , Flower (Measurements 4.7 3.2 1.3 0.2) Setosa
  , Flower (Measurements 5.4 3.9 1.7 0.4) Setosa
  , Flower (Measurements 4.4 2.9 1.4 0.2) Setosa
  , Flower (Measurements 7.0 3.2 4.7 1.4) Versicolor
  , Flower (Measurements 6.4 3.2 4.5 1.5) Versicolor
  , Flower (Measurements 5.5 2.3 4.0 1.3) Versicolor
  , Flower (Measurements 4.9 2.4 3.3 1.0) Versicolor
  , Flower (Measurements 5.0 2.0 3.5 1.0) Versicolor
  , Flower (Measurements 6.3 3.3 6.0 2.5) Virginica
  , Flower (Measurements 5.8 2.7 5.1 1.9) Virginica
  , Flower (Measurements 7.1 3.0 5.9 2.1) Virginica
  , Flower (Measurements 4.9 2.5 4.5 1.7) Virginica
  , Flower (Measurements 7.7 3.8 6.7 2.2) Virginica
  ]

-- * Example 3: monadic lenses are lenses in a Kleisli category

-- | A counter monad standing in for the clock: 'tick' returns the current time and advances it.
newtype Clock a = Clock {runClock :: Int -> (a, Int)}

instance Functor Clock where
  map f (Clock g) = Clock (\t -> let (a, t') = g t in (f a, t'))
instance Applicative Clock where
  pure a () = Clock (a (),)
  liftA2 h (Clock f, Clock g) = Clock (\t -> let (a, t') = f t; (b, t'') = g t' in (h (a, b), t''))
instance Promonad (Star Clock) where
  id = Star \a -> Clock (a,)
  Star f . Star g = Star \a -> Clock (\t -> let (b, t') = runClock (g a) t in let (c, t'') = runClock (f b) t' in (c, t''))

tick :: Clock Int
tick = Clock (\t -> (t, t + 1))

-- | Run a Kleisli arrow of @m@ on a plain value.
runK :: forall m a b. Kleisli (KL a :: KLEISLI (Star m)) (KL b) -> a -> m b
runK k = unStar (unKleisli k)

-- | A monadic lens over 'Clock': viewing is pure, updating also stamps the time.
--
-- This is a 'MonoidalLens', not a 'Lens'. The Kleisli category of a monad has coproducts but not
-- products (@'fst' . (f '&&&' g)@ would run both effects where the law allows only @f@\'s), so
-- the monadic lenses of the paper live over the Kleisli /tensor/, with a comonoidal residual.
-- Here the residual is the whole record.
stamp :: forall a b. MonoidalLens (KL (Timestamped a) :: KLEISLI (Star Clock)) (KL (Timestamped b)) (KL a) (KL b)
stamp =
  monLens @(KL (Timestamped a))
    (arr (\x -> (x, contents' x)))
    (Kleisli (Star (\(x, b) -> map (\t -> x{contents' = b, modified' = t}) tick)))

-- | A writer-like monad, for a lens that logs its updates.
newtype Log a = Log {runLog :: ([String], a)}

instance Functor Log where
  map f (Log (w, a)) = Log (w, f a)
instance Applicative Log where
  pure a () = Log ([], a ())
  liftA2 f (Log (w, a), Log (w', b)) = Log (w ++ w', f (a, b))
instance Promonad (Star Log) where
  id = Star (Log . ([],))
  Star f . Star g = Star \a -> let Log (w, b) = g a in let Log (w', c) = f b in Log (w ++ w', c)

newtype Box a = Box {openBox :: a} deriving (Show, Eq)

box :: forall a b. (Show b) => MonoidalLens (KL (Box a) :: KLEISLI (Star Log)) (KL (Box b)) (KL a) (KL b)
box =
  monLens @(KL (Box a))
    (arr (\x -> (x, openBox x)))
    (Kleisli (Star (\(_, b) -> Log (["[box]: contents changed to " ++ show b ++ "."], Box b))))

-- * Example 4: traversals

each :: Traversal [a] [b] a b
each = traversed @(Star [])

uppercase :: String -> String
uppercase = map toUpper

places :: [String]
places =
  [ "43 Adlington Rd, Wilmslow, United Kingdom"
  , "26 Westcott Rd, Princeton, USA"
  , "St James's Square, London, United Kingdom"
  ]

test :: TestTree
test =
  testGroup
    "Vitrea examples"
    [ testProperty "view a composed lens" $ assertEq (view (home % street) sherlock) "221b Baker Street"
    , testProperty "set a composed lens" $
        assertEq (set (home % street) "221b Baker St" sherlock) sherlock{home' = (home' sherlock){street' = "221b Baker St"}}
    , testProperty "over a composed lens" $ assertEq (view (home % city) (over (home % city) uppercase sherlock)) "LONDON"
    , testProperty "preview a prism" $ assertEq (place ^? asAddress) (Just (Address "221b Baker St" "London" "UK"))
    , testProperty "preview a prism that fails" $ assertEq ("nowhere" ^? asAddress) Nothing
    , testProperty "review a prism" $ assertEq (review asAddress (Address "221b Baker St" "London" "UK")) place
    , testProperty "over a prism composed with a lens" $
        assertEq (over (asAddress % city) uppercase place) "221b Baker St, LONDON, UK"
    , testProperty "type-changing lens" $
        assertEq (over contents length (Timestamped 0 1 "What is the answer?")) (Timestamped 0 1 19)
    , testProperty "algebraic lens classifies by nearest neighbour (setosa)" $
        assertEq (species ((measure .? Measurements 4.8 3.1 1.5 0.1) iris)) Setosa
    , testProperty "algebraic lens classifies by nearest neighbour (virginica)" $
        assertEq (species ((measure .? Measurements 7.2 3.1 6.1 2.3) iris)) Virginica
    , testProperty "algebraic lens keeps the measurements it classified" $
        assertEq (measurements ((measure .? Measurements 6.1 2.9 4.2 1.3) iris)) (Measurements 6.1 2.9 4.2 1.3)
    , testProperty "kaleidoscope aggregates field by field" $
        let ms = map measurements iris
        in assertEq
             (aggregateWith mean ms)
             (Measurements (mean (map sepalLe ms)) (mean (map sepalWi ms)) (mean (map petalLe ms)) (mean (map petalWi ms)))
    , testProperty "kaleidoscope with maximum" $
        let ms = map measurements iris
        in assertEq (aggregateWith maximum ms) (Measurements 7.7 3.9 6.7 2.5)
    , testProperty "classifying lens composed with kaleidoscope classifies the mean" $
        assertEq (classifyAggregate mean iris) ((measure .? aggregateWith mean (map measurements iris)) iris)
    , testProperty "the composite is a cotraversal: classify an optional flower" $
        assertEq (species (classifyOptional (fromMaybe 0) (Just setosa1))) Setosa
    , testProperty "algebraic lens is a lens: view" $ assertEq (view measure setosa1) (Measurements 5.1 3.5 1.4 0.2)
    , testProperty "algebraic lens is a lens: over" $
        assertEq (species (over measure id setosa1)) Setosa
    , testProperty "monadic lens: view is pure" $
        assertEq (runClock (runK (view stamp) (Timestamped 0 0 "What is the answer?")) 7) ("What is the answer?", 7)
    , testProperty "monadic lens: update stamps the time" $
        assertEq
          (runClock (runK (over stamp (arr (const "42"))) (Timestamped 0 0 "What is the answer?")) 7)
          (Timestamped 0 7 "42", 8)
    , testProperty "monadic lens: update logs" $
        assertEq (runLog (runK (over box (arr (+ 1))) (Box (41 :: Int)))) (["[box]: contents changed to 42."], Box 42)
    , testProperty "traversal over a list" $ assertEq (over each length ["a", "bb", "ccc"]) [1, 2, 3 :: Int]
    , testProperty "traversal composed with prism and lens" $
        assertEq
          (over (each % asAddress % city) uppercase places)
          [ "43 Adlington Rd, WILMSLOW, United Kingdom"
          , "26 Westcott Rd, PRINCETON, USA"
          , "St James's Square, LONDON, United Kingdom"
          ]
    , testProperty "fold through traversal, prism and lens" $
        assertEq
          (foldMapOf (each % asAddress % street) (: []) places)
          ["43 Adlington Rd", "26 Westcott Rd", "St James's Square"]
    ]