系统地将函数应用于haskell记录的所有字段

系统地将函数应用于haskell记录的所有字段,haskell,data-structures,record,Haskell,Data Structures,Record,我有一个包含不同类型字段的记录,以及一个适用于所有这些类型的函数。举个小(愚蠢)的例子: 比方说,我想定义一个函数,每个字段添加两条记录: addR :: Rec -> Rec -> Rec addR a b = Rec { flnum = (flnum a) + (flnum b), intnum = (intnum a) + (intnum b) } 是否有一种方法可以表达这一点,而不必对每个字段重复操作(记录中可能有许多字段) 实际上,我有一个专门由可能字段组成的记录,我想将

我有一个包含不同类型字段的记录,以及一个适用于所有这些类型的函数。举个小(愚蠢)的例子:

比方说,我想定义一个函数,每个字段添加两条记录:

addR :: Rec -> Rec -> Rec
addR a b = Rec { flnum = (flnum a) + (flnum b), intnum = (intnum a) + (intnum b) }
是否有一种方法可以表达这一点,而不必对每个字段重复操作(记录中可能有许多字段)

实际上,我有一个专门由
可能
字段组成的记录,我想将实际数据与包含某些字段默认值的记录相结合,以便在实际数据为
时使用


(我想使用template haskell应该是可能的,但我更感兴趣的是“可移植”的实现。)

我认为没有任何方法可以做到这一点,要从字段中获取值,您需要指定字段名称,或在字段上进行模式匹配-同样,要设置字段,您需要指定字段名称,或者使用常规构造函数语法来设置它们-语法顺序很重要

也许稍微简化一下,可以使用常规构造函数语法并为操作添加闭包

addR' :: Rec -> Rec -> Rec
addR' a b = Rec (doAdd flnum) (doAdd intnum)
  where doAdd f = (f a) + (f b)
doAdd
的类型为
(Num a)=>(Rec->a)->a

此外,如果您计划对记录执行多个操作(例如,
subR
,其执行的操作几乎相同,但会进行减法),则可以使用
RankNTypes
将行为抽象为函数

{-# LANGUAGE RankNTypes #-}

data Rec = Rec  { flnum :: Float, intnum :: Int } deriving (Show)

opRecFields :: (forall a. (Num a) => a -> a -> a) -> Rec -> Rec -> Rec
opRecFields op a b = Rec (performOp flnum) (performOp intnum)
  where performOp f = (f a) `op` (f b)

addR = opRecFields (+)

subR = opRecFields (-)
你可以用它

我不是专家,所以我的版本有点傻。应该可以只调用一次
gzipWithT
,例如使用
extQ
extT
,但我找不到这样做的方法。无论如何,这是我的版本:

{-# LANGUAGE DeriveDataTypeable #-}

import Data.Generics

data Test = Test {
  test1 :: Int,
  test2 :: Float,
  test3 :: Int,
  test4 :: String,
  test5 :: String
  }
  deriving (Typeable, Data, Eq, Show)

t1 :: Test
t1 = Test 1 1.1 2 "t1" "t11"

t2 :: Test
t2 = Test 3 2.2 4 "t2" "t22"

merge :: Test -> Test -> Test
merge a b = let b' = gzipWithT mergeFloat a b
                b'' = gzipWithT mergeInt a b'
            in gzipWithT mergeString a b''

mergeInt :: (Data a, Data b) => a -> b -> b
mergeInt = mkQ (mkT (id :: Int -> Int)) (\a -> mkT (\b -> a + b :: Int))

mergeFloat :: (Data a, Data b) => a -> b -> b
mergeFloat = mkQ (mkT (id :: Float -> Float)) (\a -> mkT (\b -> a + b :: Float))

mergeString :: (Data a, Data b) => a -> b -> b
mergeString = mkQ (mkT (id :: String -> String)) (\a -> mkT (\b -> a ++ b :: String))

main :: IO ()
main = print $ merge t1 t2
输出:

Test {test1 = 4, test2 = 3.3000002, test3 = 6, test4 = "t1t2", test5 = "t11t22"}
代码是模糊的,但想法很简单,
gzipWithT
将指定的通用函数(
mergeInt
mergeString
,等等)应用于一对相应的字段。

另一种方法是使用:

使用
乙烯基
(一个“可扩展记录”包):

这相当于

data Nums' = Nums' (Identity Float) (Identity Int)
data Nums'' = Nums'' Float Int
这本身就相当于

data Nums' = Nums' (Identity Float) (Identity Int)
data Nums'' = Nums'' Float Int
然后,
addR
就是

-- vinyl defines `recAdd`
addR :: Nums -> Nums -> Nums
addR = recAdd
如果您添加了一个新字段

type Nums = Rec Identity [Float, Int, Word]
您不需要触摸
addR

顺便说一句,
recAdd
很容易定义您自己,如果您想“提升”您自己的自定义数字操作,只需

-- the `RecAll f rs Num` constraint means "each field satisfies `Num`"
recAdd :: RecAll f rs Num => Rec f rs -> Rec f rs -> Rec f rs
recAdd RNil RNil = RNil
recAdd (a :& as) (b :& bs) = (a + b) :& recAdd as bs
为方便起见,您可以定义自己的构造函数:

nums :: Float -> Int -> Num
nums a b = Identity a :& Identity b :& RNil
甚至还有一种构造和匹配值的模式:

-- with `-XPatternSynonyms`
pattern Nums :: Float -> Int -> Num
pattern Nums a b = Identity a :& Identity b :& RNil
用法:

main = do
 let r1 = nums 1 2  
 let r2 = nums 3 4
 print $ r1 `addR` r2

 let (Nums a1 _) = r1
 print $ a1

 let r3 = i 5 :& i 6 :& i 7 :& z -- inferred
 print $ r1 `addR` (rcast r3) -- drop the last field
因为
r3
被推断为

(Num a, Num b, Num c) => Rec Identity [a, b, c]
你可以(安全地)把它向上抛到

rcast r3 :: (Num a, Num b) => Rec Identity [a, b]
然后你把它专门化

rcast r3 :: Nums


我目前正在做这个“关闭”的事情;仍然有很多重复。
rcast r3 :: Nums