packages feed

large-generics-0.1.0.0: test/Test/Record/Generic/Sanity/Lens/VL.hs

{-# LANGUAGE FlexibleContexts          #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE ScopedTypeVariables       #-}
{-# LANGUAGE TypeApplications          #-}
{-# LANGUAGE TypeFamilies              #-}

{-# OPTIONS_GHC -Wno-missing-signatures #-}

module Test.Record.Generic.Sanity.Lens.VL (tests) where

import Data.Char (toUpper)
import Data.Functor.Identity
import Data.Maybe (fromJust)
import Data.Proxy
import Data.SOP
import Lens.Micro (Lens', (^.), (&), (%~))

import Test.Tasty
import Test.Tasty.HUnit

import Data.Record.Generic
import Data.Record.Generic.Lens.VL
import Data.Record.Generic.SOP
import Data.Record.Generic.Transform

import qualified Data.Record.Generic.Rep as Rep

import Test.Record.Generic.Infra.Examples
import Test.Record.Generic.Infra.Beam.Interpretation
import Test.Record.Generic.Infra.Beam.Mini

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

regularLenses :: Regular (RegularRecordLens Regular f)
regularLenses = lensesForRegularRecord (Proxy @DefaultInterpretation)

MkRegular {
      regularField1 = RegularRecordLens regularLens1
    , regularField2 = RegularRecordLens regularLens2
    , regularField3 = RegularRecordLens regularLens3
    } = regularLenses

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

mixinTableLenses :: MixinTable (RegularRecordLens MixinTable Identity)
mixinTableLenses = lensesForRegularRecord (Proxy @BeamInterpretation)

MkMixinTable {
      mixinTableField1 = RegularRecordLens mixinTableLens1
    , mixinTableField2 = RegularRecordLens mixinTableLens2
    } = mixinTableLenses

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

-- We cannot define this now:
--
-- > irregularLenses :: Irregular (RegularRecordLens Irregular I)
-- > irregularLenses = lensesForRegularRecord (Proxy @DefaultInterpretation)
--
-- It will complain that @String@ is not equal to
--
-- > Interpreted (DefaultInterpretation (RegularRecordLens Irregular I)) String
--
-- 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

-- Unlike the beam tutorial, we match to get these lenses in two steps: first,
-- we get 'SimpleRecordLens' out, which does not rely on impredicativity;
-- then we get the Van Laarhoven lenses out in three separate bindings. This
-- avoids problems with ghc type inference which gets very confused by that
-- pattern match.

irregularLens1' :: SimpleRecordLens (Irregular f) (f Int)
irregularLens2' :: SimpleRecordLens (Irregular f) (f Bool)
irregularLens3' :: SimpleRecordLens (Irregular f) String

(    Field irregularLens1'
  :* Field irregularLens2'
  :* Field irregularLens3'
  :* Nil ) = irregularLenses

irregularLens1 :: Lens' (Irregular f) (f Int)
irregularLens2 :: Lens' (Irregular f) (f Bool)
irregularLens3 :: Lens' (Irregular f) String

SimpleRecordLens irregularLens1 = irregularLens1'
SimpleRecordLens irregularLens2 = irregularLens2'
SimpleRecordLens irregularLens3 = irregularLens3'

{-------------------------------------------------------------------------------
  Beam-like example (using the 'Lenses' indirection).

  This still does not support all beam features; in particular, this only works
  for the regular 'MixinTable', not for the complete 'FullTable'. To do that, we
  would need to introduce a separate type class (instead of 'IsRegularField')
  that then needs to be available for every field, so that we can distinguish
  between mixins and normal ('Columnar') fields. For a completely worked out
  exmaple, see the @beam-large-records@ package
  <https://github.com/well-typed/beam-large-records>.
-------------------------------------------------------------------------------}

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

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

mixinBeamLikeLenses :: MixinTable (Lenses MixinTable Identity)
mixinBeamLikeLenses = beamLikeLenses

MkMixinTable {
      mixinTableField1 = WrapLens mixinBeamLikeLens1
    , mixinTableField2 = WrapLens mixinBeamLikeLens2
    } = mixinBeamLikeLenses

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

tests :: TestTree
tests = testGroup "Test.Record.Generic.Sanity.Lens.VL" [
      testCase "regular_get"   test_regular_get
    , testCase "regular_set"   test_regular_set
    , testCase "mixin_get"     test_mixin_get
    , testCase "mixin_set"     test_mixin_set
    , testCase "irregular_get" test_irregular_get
    , testCase "irregular_set" test_irregular_set
    , testCase "beamlike_get"  test_beamlike_get
    , testCase "beamlike_set"  test_beamlike_set
    ]

test_regular_get :: Assertion
test_regular_get =
    assertEqual "" (I True)
      (exampleRegular ^. regularLens2)

test_regular_set :: Assertion
test_regular_set =
    assertEqual "" expected $
        exampleRegular
      & regularLens1 %~ mapII negate
      & regularLens3 %~ mapII (map toUpper)
  where
    expected :: Regular I
    expected = MkRegular {
          regularField1 = I (-5)
        , regularField2 = I True
        , regularField3 = I "A"
        }

test_mixin_get :: Assertion
test_mixin_get =
    assertEqual "" 3.14
      (exampleMixinTable ^. mixinTableLens2)

test_mixin_set :: Assertion
test_mixin_set =
    assertEqual "" expected $
        exampleMixinTable
      & mixinTableLens1 %~ succ
      & mixinTableLens2 %~ negate
  where
    expected :: MixinTable Identity
    expected = MkMixinTable {
          mixinTableField1 = 'b'
        , mixinTableField2 = -3.14
        }

test_irregular_get :: Assertion
test_irregular_get =
    assertEqual "" (I True)
      (exampleIrregular ^. irregularLens2)

test_irregular_set :: Assertion
test_irregular_set =
    assertEqual "" expected $
        exampleIrregular
      & irregularLens1 %~ mapII negate
      & irregularLens3 %~ map toUpper
  where
    expected :: Irregular I
    expected = MkIrregular {
          irregularField1 = I (-1234)
        , irregularField2 = I True
        , irregularField3 = "HI"
        }

test_beamlike_get :: Assertion
test_beamlike_get =
    assertEqual "" 3.14
      (exampleMixinTable ^. mixinBeamLikeLens2)

test_beamlike_set :: Assertion
test_beamlike_set =
    assertEqual "" expected $
        exampleMixinTable
      & mixinBeamLikeLens1 %~ succ
      & mixinBeamLikeLens2 %~ negate
  where
    expected :: MixinTable Identity
    expected = MkMixinTable {
          mixinTableField1 = 'b'
        , mixinTableField2 = -3.14
        }