packages feed

large-records-0.1.0.0: test/Test/Record/Sanity/Lens/Micro.hs

{-# LANGUAGE ConstraintKinds       #-}
{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE ImpredicativeTypes    #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE QuasiQuotes           #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TemplateHaskell       #-}
{-# LANGUAGE TypeFamilies          #-}
{-# LANGUAGE UndecidableInstances  #-}

{-# OPTIONS_GHC -Wno-missing-signatures #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ViewPatterns #-}

module Test.Record.Sanity.Lens.Micro (tests) where

import Data.Kind
import Data.Maybe (fromJust)
import Data.SOP
import Lens.Micro (Lens', (^.), (&), (%~))
import Test.Tasty
import Test.Tasty.HUnit

import Data.Record.Generic
import Data.Record.Generic.Lens.Micro
import Data.Record.Generic.SOP
import Data.Record.Generic.Transform
import Data.Record.QQ.CodeGen
import Data.Record.TH

import qualified Data.Record.Generic.Rep as Rep

{-------------------------------------------------------------------------------
  Simple example (no type families)
-------------------------------------------------------------------------------}

largeRecord defaultPureScript [d|
      data Simple (f :: Type -> Type) = MkSimple {
            s1 :: f Int
          , s2 :: f Bool
          , s3 :: f Char
          }
        deriving (Show, Eq)
    |]

simpleExample :: Simple I
simpleExample = [lr| MkSimple {
      s1 = I 5
    , s2 = I True
    , s3 = I 'a'
    } |]

simpleExampleLenses :: Simple (RegularRecordLens Simple f)
simpleExampleLenses = lensesForRegularRecord (Proxy @DefaultInterpretation)

[lr| MkSimple {
      s1 = RegularRecordLens xs1
    , s2 = RegularRecordLens xs2
    , s3 = RegularRecordLens xs3
    } |] = simpleExampleLenses

{-------------------------------------------------------------------------------
  Simplified version of beam's 'Columnar' type'
-------------------------------------------------------------------------------}

data Lenses (tbl :: (Type -> Type) -> Type) (f :: Type -> Type) (x :: Type)

data WrapLens a b = WrapLens (Lens' a b)

type family Columnar f x :: Type where
  Columnar I              x = x
  Columnar (Lenses tbl f) x = WrapLens (tbl f) (Columnar f x)
  Columnar f              x = f x

{-------------------------------------------------------------------------------
  Example with type families, but still regular

  See /next/ example for usage of, and motivation for, 'Lenses'.
-------------------------------------------------------------------------------}

data BeamInterpretation (f :: Type -> Type)

type instance Interpreted (BeamInterpretation f) (Uninterpreted x) = Columnar f x

instance StandardInterpretation BeamInterpretation (RegularRecordLens tbl f)
instance StandardInterpretation BeamInterpretation I

largeRecord defaultPureScript [d|
      data Regular (f :: Type -> Type) = MkRegular {
            r1 :: Columnar f Int
          , r2 :: Columnar f Bool
          , r3 :: Columnar f Char
          }
        deriving (Show, Eq)
    |]

regularExample :: Regular I
regularExample = [lr| MkRegular {
      r1 = 5
    , r2 = True
    , r3 = 'a'
    } |]

regularLenses :: Regular (RegularRecordLens Regular I)
regularLenses = lensesForRegularRecord (Proxy @BeamInterpretation)

[lr| MkRegular {
      r1 = RegularRecordLens xr1
    , r2 = RegularRecordLens xr2
    , r3 = RegularRecordLens xr3
    } |] = regularLenses

{-------------------------------------------------------------------------------
  Beam-like example

  The lenses we generate above have @I x@ as their argument, rather than @x@. In
  beam, the lenses have @Columnar f x@ as their target, which is just @x@ in the
  case that @f == I@. If we want to replicate this, we cannot use
  'lensesForRegularRecord', which gives us 'RegularRecordLens', and must instead
  use the lower-level function 'lensesForHKRecord'. This example is still
  simplified from the beam example because we don't support any form of mixins;
  we insist every field is regular, which allows us to avoid introducing a
  separate type class. See the @beam-large-package@ for full beam integration.
-------------------------------------------------------------------------------}

beamLikeLenses :: forall tbl.
     ( Generic (tbl (Lenses tbl I))
     , Generic (tbl Uninterpreted)
     , Generic (tbl I)
     , HasNormalForm (BeamInterpretation (Lenses tbl I)) (tbl (Lenses tbl I)) (tbl Uninterpreted)
     , HasNormalForm (BeamInterpretation I) (tbl I) (tbl Uninterpreted)
     , Constraints (tbl Uninterpreted) (IsRegularField Uninterpreted)
     )
  => tbl (Lenses tbl I)
beamLikeLenses =
    to . denormalize1 (Proxy @BeamInterpretation) $
      Rep.cmap
        (Proxy @(IsRegularField Uninterpreted))
        aux
        (lensesForHKRecord (Proxy @BeamInterpretation))
  where
    aux :: forall x.
         IsRegularField Uninterpreted x
      => HKRecordLens BeamInterpretation I tbl x
      -> Interpret (BeamInterpretation (Lenses tbl I)) x
    aux (HKRecordLens l) =
        case isRegularField (Proxy @(Uninterpreted x)) of
          RegularField -> Interpret $ WrapLens $
              l
            . standardInterpretationLens (Proxy @BeamInterpretation)
            . unI'

    unI' :: Lens' (I x) x
    unI' f (I x) = I <$> f x

regularBeamLikeLenses :: Regular (Lenses Regular I)
regularBeamLikeLenses = beamLikeLenses

[lr| MkRegular {
      r1 = WrapLens br1
    , r2 = WrapLens br2
    , r3 = WrapLens br3
    } |] = regularBeamLikeLenses

{-------------------------------------------------------------------------------
  Irregular example
-------------------------------------------------------------------------------}

largeRecord defaultPureScript [d|
      data Irregular (f :: Type -> Type) = MkIrregular {
            i1 :: f Int
          , i2 :: f Bool
          , i3 :: Char -- No @f@!
          }
        deriving (Show, Eq)
    |]

irregularExample :: Irregular I
irregularExample = [lr| MkIrregular {
      i1 = I 5
    , i2 = I True
    , i3 = 'a'
    } |]

-- We cannot define this now:
--
-- > irregularLenses :: Irregular (RegularRecordLens Irregular I)
-- > irregularLenses = lensesForRegularRecord (Proxy @DefaultInterpretation)
--
-- It will complain that @Char@ is not equal to
--
-- > Interpreted (DefaultInterpretation (RegularRecordLens Irregular I)) Char
--
-- We can use 'repLenses' to nonetheless get lenses for all fields in
-- 'Irregular', and then translate to an NP so that we can pattern match on it
-- in a type-safe way. Of course, the translation to SOP incurs O(N^2)
-- compile-time cost so this is not a proper solution.
--
-- NOTE: There is not much point using 'repLenses'' here; that is primarily
-- useful only if there is some post-processing step (like done by
-- 'lensesForRegularRecord').
irregularLenses :: NP (Field (SimpleRecordLens (Irregular f))) (MetadataOf (Irregular f))
irregularLenses = fromJust $ toSOP rep
  where
    rep :: Rep (SimpleRecordLens (Irregular f)) (Irregular f)
    rep = lensesForSimpleRecord

(    Field (SimpleRecordLens xi1)
  :* Field (SimpleRecordLens xi2)
  :* Field (SimpleRecordLens xi3)
  :* Nil ) = irregularLenses

{-------------------------------------------------------------------------------
  Tests proper
-------------------------------------------------------------------------------}

tests :: TestTree
tests = testGroup "Test.Record.Sanity.Lens.Micro" [
      testCase "simple_get"    test_simple_get
    , testCase "simple_set"    test_simple_set
    , testCase "regular_get"   test_regular_get
    , testCase "regular_set"   test_regular_set
    , testCase "beamlike_get"  test_beamlike_get
    , testCase "beamlike_set"  test_beamlike_set
    , testCase "irregular_get" test_irregular_get
    , testCase "irregular_set" test_irregular_set
    ]

test_simple_get :: Assertion
test_simple_get =
    assertEqual "" (I True)
      (simpleExample ^. xs2)

test_simple_set :: Assertion
test_simple_set =
    assertEqual "" expected $
      simpleExample & xs1 %~ mapII negate & xs3 %~ mapII succ
  where
    expected :: Simple I
    expected = [lr| MkSimple {
          s1 = I (-5)
        , s2 = I True
        , s3 = I 'b'
        } |]

test_regular_get :: Assertion
test_regular_get =
    assertEqual "" (I True)
      (regularExample ^. xr2)

test_regular_set :: Assertion
test_regular_set =
    assertEqual "" expected $
      regularExample & xr1 %~ mapII negate & xr3 %~ mapII succ
  where
    expected :: Regular I
    expected = [lr| MkRegular {
          r1 = (-5)
        , r2 = True
        , r3 = 'b'
        } |]

test_beamlike_get :: Assertion
test_beamlike_get =
    assertEqual "" True
      (regularExample ^. br2)

test_beamlike_set :: Assertion
test_beamlike_set =
    assertEqual "" expected $
      regularExample & br1 %~ negate & br3 %~ succ
  where
    expected :: Regular I
    expected = [lr| MkRegular {
          r1 = (-5)
        , r2 = True
        , r3 = 'b'
        } |]

test_irregular_get :: Assertion
test_irregular_get =
    assertEqual "" (I True)
      (irregularExample ^. xi2)

test_irregular_set :: Assertion
test_irregular_set =
    assertEqual "" expected $
      irregularExample & xi1 %~ mapII negate & xi3 %~ succ
  where
    expected :: Irregular I
    expected = [lr| MkIrregular {
          i1 = I (-5)
        , i2 = I True
        , i3 = 'b'
        } |]