如何在两个不同的记录上应用相同的镜头? (或者,将字段名称记录为第一类值?)

时间:2017-08-30 10:48:39

标签: haskell lens

考虑以下记录及其镜头:

data Bar = Bar {barField1 :: Int, barField2 :: String}
makeLensesWith abbreviatedFields ''Bar

data BarError = BarError {barerrField1 :: [String], barerrField2 :: [String]}
makeLensesWith abbreviatedFields ''BarError

现在,他们都可以使用镜头field1& field2凭借实施HasField1HasField2类型类。但是,我无法获得以下代码来编译:

-- 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)

2 个答案:

答案 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但是,那个方式对于第二个参数来说太笼统了,所以一般我倾向于怀疑该类型的非底值存在。我们当然无法通过field1field2

由于我们想要传递field1field2,我们需要一些能够统一其类型的内容:HasField1 s a => Lens' s aHasField2 s a => Lens' s a。不幸的是,由于HasField1HasField2不共享(或拥有)任何超级类,因此唯一的类型将这些类型统一为最后一段中给出的类型。

请注意,即使HasField1HasField2共享超类,我们仍然无法完成。您的实施还要求Bar中的字段为Foldable,并且BarError中的字段为IsString的列表。表达这些约束是可能的,但不完全是用户友好的。