Consider the following records and their lenses:
data Bar = Bar {barField1 :: Int, barField2 :: String}
makeLensesWith abbreviatedFields ''Bar
data BarError = BarError {barerrField1 :: [String], barerrField2 :: [String]}
makeLensesWith abbreviatedFields ''BarError
Now, both of them have access to the lenses field1
& field2
by virtue of implementing the HasField1
and HasField2
type-classes. However, I am unable to get the following piece of code to compile:
-- Most-general type-signature inferred by the compiler, if I remove the
-- KindSignatures from `record` & `errRecord` below:
--
-- validateLength :: (IsString a) => (Int, Int) -> ALens t t [a] [a] -> t -> t -> t
--
validateLength (mn, mx) l (record :: Bar) (errRecord :: BarErr) =
let len = length (record ^# l)
in if ((len<mn) || (len>mx))
then errRecord & l #%~ (\x -> ("incorrect length"):x)
else errRecord
-- Usage scenario:
--
-- let x = Bar 10 "hello there"
-- xErr = BarError [] []
-- in validateLength (3, 10) field2 x xErr
Error message:
/Users/saurabhnanda/projects/vl-haskell/src/TryLens.hs:18:20: error:
• Couldn't match type ‘BarError’ with ‘Bar’
Expected type: BarError -> BarError
Actual type: Bar -> BarError
• In the second argument of ‘(&)’, namely
‘l #%~ (\ x -> ("incorrect length") : x)’
In the expression:
errRecord & l #%~ (\ x -> ("incorrect length") : x)
In the expression:
if ((len < mn) || (len > mx)) then
errRecord & l #%~ (\ x -> ("incorrect length") : x)
else
errRecord
Note: Instead of using ^.
and %~
I'm using ^#
and #%~
because I'd like to treat the lens (l
) as a getter & setter simultaneously.
Edit: A simpler snippet to demonstrate the problem is:
-- intended type signature:
-- funkyLensAccess :: l -> r1 -> r2 -> (t1, t2)
--
-- type signature inferred by the compiler
-- funkyLensAccess :: Getting t s t -> s -> s -> (t, t)
--
funkyLensAccess l rec1 rec2 = (rec1 ^. l, rec2 ^. l)
If you want a value passed as an argument to operate at two different types, you'll need
Rank2Types
(or the equivalentRankNTypes
) extension.Then, since rank-2 or higher types are never inferred in GHC, you'll need to write the type signature explicitly.
Our first pass might look something like:
IsString a => (Int, Int) -> (forall s a. Lens' s a) -> Bar -> BarError -> BarError
But, that's way too general for that second argument, so general I tend to doubt a non-bottom value of that type exists. We certainly can't passfield1
orfield2
there.Since we want to pass
field1
orfield2
we need something that unifies their types:HasField1 s a => Lens' s a
andHasField2 s a => Lens' s a
. Unfortunately, sinceHasField1
andHasField2
do not share (or have) any super classes, the only type that unifies these the the type given in the last paragraph.Note that even if
HasField1
andHasField2
shared a super class, we still wouldn't be done. Your implementation also requires that the field inBar
be aFoldable
and that the field inBarError
be a list ofIsString
. Expressing those constraints is possible, but not exactly user-friendly.