有没有一种方法可以表达类型的记录的读/写/读写属性?

时间:2020-01-23 09:13:14

标签: haskell

假设您具有以下产品类型:

data D = D { getA :: Int, getB :: Char, getC :: [Double] }

并假设您有一个功能:

f :: D -> D

仅读取getA字段,但修改getBgetC

是否有一种方便的方式以f的形式来表达这一点?

5 个答案:

答案 0 :(得分:5)

因此,让我们考虑一个示例:

f :: D -> D
f d = d { getC = map (+ fromIntegral (getA d)) (getC d) }

很显然,一旦有了D -> D这样的具体类型,所有保证都将失效:可以想象,此函数可以通过其参数来执行任何

如果要防止这种情况,则需要用抽象的D代替具体的f :: d -> d ,例如

d

但是当然,该实现将不再起作用,因为在 • Couldn't match expected type ‘d’ with actual type ‘D’ ‘d’ is a rigid type variable bound by the type signature for: f :: forall d. d -> d 上您无能为力。

{-# LANGUAGE TemplateHaskell #-}
import Control.Lens

data D = D { _getA :: Int, _getB :: Char, _getC :: [Double] }
makeLenses ''D

f :: D -> D
f d = d & getC %~ map (+ fromIntegral (d^.getA))

要仅重新启用所需的特定操作,可以将它们作为参数传递。那么,什么是“读取操作或修改操作参数”?
输入lenses。让我们首先使用它们重写所有原始示例:

d

现在,可以通过将type AGetter' s a = Getting a s a -- for some reason this isn't defined -- in the `lens` library f' :: AGetter' d Int -> ASetter' d [Double] -> d -> d f' getInt setDbls d = d & setDbls %~ map (+ fromIntegral (d^.getInt)) 抽象化但将必要的访问操作作为参数传递来轻松地将其概括/增强:

getA

通过让getCf :: D -> D f = f' getA getC 镜头可以让您获得较旧的行为:

lens

之所以可行,是因为getA使用类型类/通用量化类型欺骗来编码子类型关系:Lens' D Int具有类型AGetter' D Int,但是ASetter'是超类型其中功能减少了,因此保证您只阅读重点内容,而没有其他内容。


技术细节:您已经注意到我写的是Setter',而不是ASetterAnOᴘᴛɪᴄ。这意味着什么:

  • Oᴘᴛɪᴄ的{​​{1}}版本是它们的 rank-0通讯者。所以ALens只能 用作镜头,不能用作例如Lens可以用作吸气剂,设置器,遍历或折叠。
    将函数参数限制为具体的AnOᴘᴛɪᴄ版本被认为是一种很好的样式,因为这意味着编译器实际上并不需要摆弄2级类型。 (Lens本身的类型只是等级1的多态,但是将其作为参数传递将使接受函数等级2的多态。)
  • Oᴘᴛɪᴄ'的{​​{1}}版本是非更改类型的变体。原则上,例如设置者还可以更改其关注的字段类型,例如当您将Oᴘᴛɪᴄ元组的snd类型更改为(Bool, Char)时,将是String,但是如果仅将第二个字段更改为另一个Setter (Bool,Char) (Bool,String) Char String ,它只是一个Char(实际上是a synonym,用于类型更改设置程序,碰巧更改为同一类型)。

答案 1 :(得分:2)

如果您像我这样对镜头恐惧症,那么仅使用参数和2级类型就可以得到令人满意的解决方案。

{-# LANGUAGE Rank2Types #-}

import Data.Char (toLower)

-- The goal of the question: a type that expresses
-- - Reading an Int
-- - Modifying a Char
-- - Modifying a [Double]
-- Parametricity guarantees your can't do anything else with that t
type YourParticularType = forall t .
     (t -> Int)
  -> ((Char -> Char) -> t -> t)
  -> (([Double] -> [Double]) -> t -> t)
  -> (t -> t)

-- One example of something in that type.
-- No mention of D here, so the user can be sure it won't do
-- anything silly.
f_parametric :: YourParticularType
f_parametric getInt modifyChar modifyDoubles t =
    modifyDoubles (fromIntegral (getInt t) :)
  . modifyChar toLower
  $ t

data D = D
  { getA :: Int
  , getB :: Char
  , getC :: [Double]
  } deriving (Show)

modifyB :: (Char -> Char) -> D -> D
modifyB f d = d { getB = f (getB d) }

modifyC :: ([Double] -> [Double]) -> D -> D
modifyC f d = d { getC = f (getC d) }

-- Shows that D is of suitable form to match YourParticularType
run_f_at_d :: YourParticularType -> D -> D
run_f_at_d f = f getA modifyB modifyC

d1 :: D
d1 = D 42 'Z' [3.14, 1.41]

d2 :: D
d2 = run_f_at_d f_parametric d1

答案 2 :(得分:1)

有两种查看方式,如果您只想“修改” D的函数,则它的类型为f :: D -> D。参见此处的示例:

f :: D -> D
f (D a b c) = D a (modifyB a b) (modifyC a c)
  where modifyB = undefined -- function of type Int -> Char -> Char
        modifyC = undefined -- function of type Int -> [Double] -> [Double]

另一种方法是将两个函数用作f的参数,其中一个类型为Int -> Char -> Char,而另一个类型为Int -> [Double] -> [Double]。这是一个示例:

f :: (Int -> Char -> Char)
   -> (Int -> [Double] -> [Double])
   -> D
   -> D
f modifyB modifyC (D a b c) = D a (modifyB a b) (modifyC a c)

答案 3 :(得分:1)

问题是D太具体了。您对类型了解的越多,使用它的值就越多。反之亦然:您知道的 less 越少可以处理。极端的例子是id

id :: a -> a

由于您对a一无所知,因此,对于类型为a的输入,您唯一可以做的就是按原样返回它。

从降低D的具体意义开始:

data D' a b c = D' { getA :: a, getB :: b, getC :: c }

现在,您可以定义f' :: D' a Char [Double] -> D' a Char [Double],它可以以各种方式修改getBgetC,但是除了重用getA之外,什么都不能做。在输出中。

您可以通过传递执行工作的函数作为参数来进一步限制f'对这两个字段的作用,类似于Jack Higgins的建议:

f' :: (b -> b) -> (c -> c) -> D' a b c -> D' a b c

现在f'只有一个实际的实现:

f' f g (D' x y z) = D' x (f y) (g z)

再走一步,D' trifunctor 的示例,它是函子的直接(虽然不是预定义或常用的)扩展。

class Trifunctor (p :: * -> * -> * -> *) where
    trimap :: (a -> b) -> (c -> d) -> (e -> f) -> p a c e -> p b d f


instance Trifunctor D' where
    trimap f g h (D' x y z) = D' (f x) (g y) (h z)

然后

f' :: (b -> b) -> (c -> c) -> D' a b c -> D' a b c
f' bf cf = trimap id bf cf

答案 4 :(得分:1)

这是一个不完整的解决方案,因为它基于HasFieldGHC.Records类型类,其中so far)仅提供getter,而不提供setter。我们可以编写以下函数,显式列出必填字段作为约束:

{-# LANGUAGE DataKinds, TypeApplications, FlexibleContexts #-}

import GHC.Records
import GHC.TypeLits

f :: HasField "getA" r Int => r -> r
f r = let _ = getField @"getA" r 
       in undefined -- do some stuff

以这种方式使用类型类,可以避免客户意外地将错误的镜头作为参数传递给潜在的问题。

我们可能还希望保留“标称”类型:禁止客户端错误地传递非D类型的记录,而只是偶然地具有兼容字段。记录如下类型:

{-# LANGUAGE DuplicateRecordFields #-}

data Z = Z { getA :: Int, getB :: Char, getC :: [Double] } deriving Show

我们需要定义此辅助模块:

{-# LANGUAGE TypeOperators, FlexibleInstances, MultiParamTypeClasses #-}
module Opaque(Opaque(..)) where

import Data.Type.Equality ((:~:)(Refl))    

newtype HiddenEq a b = HiddenEq (a :~: b)

-- fix concrete a, be polymorphic over b
class Opaque a b where
    opaque :: HiddenEq a b

-- all types have this instance!
instance Opaque a a where
    opaque = HiddenEq Refl 

Opaque a ba实际上等于b,但是它不允许您访问证据。现在我们可以编写如下函数:

f' :: (Opaque D r, HasField "getA" r Int) => r -> r
f' r = let _ = getField @"getA" r 
        -- _ = getField @"getB" -- we know r is D, but we can't touch the "getB" field
        in undefined

使用ff'来使用:

main :: IO ()
main = do
    print $ f (D 3 'c' [1.0]) -- compiles
    print $ f (Z 3 'c' [1.0]) -- compiles
    print $ f' (D 3 'c' [1.0]) -- compiles
    print $ f' (Z 3 'c' [1.0]) -- doesn't compile