考虑以下记录及其镜头:
data Bar = Bar {barField1 :: Int, barField2 :: String}
makeLensesWith abbreviatedFields ''Bar
data BarError = BarError {barerrField1 :: [String], barerrField2 :: [String]}
makeLensesWith abbreviatedFields ''BarError
现在,他们都可以使用镜头field1
& field2
凭借实施HasField1
和HasField2
类型类。但是,我无法获得以下代码来编译:
-- 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
错误讯息:
/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
注意:我没有使用^.
和%~
而是使用^#
和#%~
因为我想{{3} }}
编辑:用于演示此问题的更简单的代码段是:
-- 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)
答案 0 :(得分:3)
所以基本上你的问题与镜头无关,但是(accessor-)函数可以在不同的类型上运行,每个都给出不同类型的结果。
这立即意味着麻烦:如果access-field类型应该依赖于contains-struct类型,那么这是dependent type。 Haskell不是一种依赖类型的语言。这是你可以轻松完成的任务,例如Python通过名称(以字符串的形式)调用字段,然后通过duck typing在字段上操作,但Haskell会删除像记录标签字符串在运行时出于很好的理由,当然编译器需要知道所有类型,因此它们不能在运行时被推断。从这个意义上说,你要问的是根本不可能的。
或者是吗? GHC实际上已经变得非常善于依赖类型。现在已经有一段时间可以将非类型特定的标签处理为类型级别的字符串值,称为Symbol
s。最近,有work on allowing fields of any record to be accessed by name,就像在Python中一样,但在编译时都是如此,无论字段中包含什么类型。
重要的是你需要表达将记录标签和记录类型映射到包含元素类型的类型级函数。这由HasField
class表示。
{-# LANGUAGE DataKinds, KindSignatures, FlexibleInstances, FlexibleContexts, FunctionalDependencies, ScopedTypeVariables, UnicodeSyntax, TypeApplications, AllowAmbiguousTypes #-}
import GHC.Records
import GHC.TypeLits (Symbol)
data Bar = Bar {barField1 :: Int, barField2 :: String}
data BarError = BarError {barerrField1 :: [String], barerrField2 :: [String]}
deriving (Show)
type LensOn s a = (a, a -> s) -- poor man's lens focus
instance HasField "Field2" Bar (LensOn Bar String) where
getField (Bar i s) = (s, \s' -> Bar i s')
instance HasField "Field2" BarError (LensOn BarError [String]) where
getField (BarError f₁ f₂) = (f₂, \f₂' -> BarError f₁ f₂')
validateLength :: ∀ (f :: Symbol)
. ( HasField f Bar (LensOn Bar String)
, HasField f BarError (LensOn BarError [String]) )
=> (Int,Int) -> Bar -> BarError -> BarError
validateLength (mn,mx) record errRecord
= let len = length . fst $ getField @f record
in if len < mn || len > mx
then case getField @f errRecord of
(oldRec, setRec) -> setRec $ "incorrect length" : oldRec
else errRecord
main :: IO ()
main = let x = Bar 10 "hello there"
xErr = BarError [] []
in print $ validateLength @"Field2" (3,10) x xErr
使用GHC-8.3.20170711进行测试,可能并不适用于较旧的版本。
答案 1 :(得分:1)
如果您希望作为参数传递的值以两种不同的类型运行,则需要Rank2Types
(或等效的RankNTypes
)扩展名。
然后,由于GHC中永远不会推断出排名为2或更高的类型,因此您需要明确地编写类型签名。
我们的第一遍可能看起来像:IsString a => (Int, Int) -> (forall s a. Lens' s a) -> Bar -> BarError -> BarError
但是,那个方式对于第二个参数来说太笼统了,所以一般我倾向于怀疑该类型的非底值存在。我们当然无法通过field1
或field2
。
由于我们想要传递field1
或field2
,我们需要一些能够统一其类型的内容:HasField1 s a => Lens' s a
和HasField2 s a => Lens' s a
。不幸的是,由于HasField1
和HasField2
不共享(或拥有)任何超级类,因此唯一的类型将这些类型统一为最后一段中给出的类型。
请注意,即使HasField1
和HasField2
共享超类,我们仍然无法完成。您的实施还要求Bar
中的字段为Foldable
,并且BarError
中的字段为IsString
的列表。表达这些约束是可能的,但不完全是用户友好的。