我有一个包含不同类型字段的记录,以及适用于所有这些类型的函数。作为一个小(愚蠢)的例子:
data Rec = Rec { flnum :: Float, intnum :: Int } deriving (Show)
说,我想定义一个每个字段添加两个记录的函数:
addR :: Rec -> Rec -> Rec
addR a b = Rec { flnum = (flnum a) + (flnum b), intnum = (intnum a) + (intnum b) }
有没有办法表达这一点而不重复每个字段的操作(记录中可能有很多字段)?
实际上,我有一个仅由Maybe
字段组成的记录,我想将实际数据与包含某些字段的默认值的记录组合在一起,以便在实际数据为{{{ 1}}。
(我想应该可以使用模板haskell,但我对“便携式”实现更感兴趣。)
答案 0 :(得分:6)
另一种方法是使用GHC.Generics:
{-# LANGUAGE FlexibleInstances, FlexibleContexts,
UndecidableInstances, DeriveGeneric, TypeOperators #-}
import GHC.Generics
class AddR a where
addR :: a -> a -> a
instance (Generic a, GAddR (Rep a)) => AddR a where
addR a b = to (from a `gaddR` from b)
class GAddR f where
gaddR :: f a -> f a -> f a
instance GAddR a => GAddR (M1 i c a) where
M1 a `gaddR` M1 b = M1 (a `gaddR` b)
instance (GAddR a, GAddR b) => GAddR (a :*: b) where
(al :*: bl) `gaddR` (ar :*: br) = gaddR al ar :*: gaddR bl br
instance Num a => GAddR (K1 i a) where
K1 a `gaddR` K1 b = K1 (a + b)
-- Usage
data Rec = Rec { flnum :: Float, intnum :: Int } deriving (Show, Generic)
t1 = Rec 1.0 2 `addR` Rec 3.0 4
答案 1 :(得分:5)
您可以使用gzipWithT。
我不是专家,所以我的版本有点傻。应该可以只调用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
等)应用于相应字段对。
答案 2 :(得分:3)
使用vinyl
("可扩展记录"包):
import Data.Vinyl
-- `vinyl` exports `Rec`
type Nums = Rec Identity [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
https://hackage.haskell.org/package/vinyl-0.5.2/docs/Data-Vinyl-Class-Method.html#v:recAdd
https://hackage.haskell.org/package/vinyl-0.5.2/docs/Data-Vinyl-Tutorial-Overview.html
答案 3 :(得分:2)
我认为没有办法做到这一点,从字段中获取值,您需要指定它们的名称或模式匹配 - 并且类似于设置字段,您可以指定它们的名称,或者使用常规构造函数语法来设置它们 - 语法顺序很重要。
也许稍微简化就是使用常规构造函数语法并为操作添加闭包
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
,它几乎相同但减去 - 您可以使用{{1将行为抽象为函数}}
RankNTypes