假设您具有以下产品类型:
data D = D { getA :: Int, getB :: Char, getC :: [Double] }
并假设您有一个功能:
f :: D -> D
仅读取getA
字段,但修改getB
和getC
。
是否有一种方便的方式以f
的形式来表达这一点?
答案 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
通过让getC
和f :: D -> D
f = f' getA getC
镜头可以让您获得较旧的行为:
lens
之所以可行,是因为getA
使用类型类/通用量化类型欺骗来编码子类型关系:Lens' D Int
具有类型AGetter' D Int
,但是ASetter'
是超类型其中功能减少了,因此保证您只阅读重点内容,而没有其他内容。
技术细节:您已经注意到我写的是Setter'
,而不是ASetter
或AnOᴘᴛɪᴄ
。这意味着什么:
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]
,它可以以各种方式修改getB
和getC
,但是除了重用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)
这是一个不完整的解决方案,因为它基于HasField
的GHC.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 b
说a
实际上等于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
使用f
和f'
来使用:
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