如何简洁地修改记录的字段?
How can I concisely modify a record's field?
data Person = Person
{
name :: String
, counter :: Int
}
incrementPersonCounter :: Person -> Person
incrementPersonCounter p@(Person _ c) = p { counter = c + 1 }
有没有更简洁的方法来做上面的事情?是否有我可以使用的函数,我可以在其中指定记录、它的字段之一(在本例中为 name
/ counter
)和一个应用于 return 值的函数?
我在想一些事情:
applyRecord r f f' = r
{ f = f' (f r) }
尽管这行不通,因为:
error: Not in scope: ‘f’
|
13 | { f = f' (f r) }
概括incrementPersonCounter
的一种方法是对修改函数进行抽象:
modifyPersonCounter :: (Int -> Int) -> Person -> Person
modifyPersonCounter f p = (\c -> p { counter = c}) $ f (counter p)
事实上,一个常见的模式是对我们想要在该领域执行的效果进行抽象:
counterLens :: forall f. Functor f => (Int -> f Int) -> (Person -> f Person)
counterLens f p = (\c -> p { counter = c }) <$> f (counter p)
例如,我们可能希望从控制台或数据库中读取计数器增加(两者都是 IO
效果)。
我们可以给synonym这样的函数类型,给定一个(可能有效的)更改字段的方法,return一个转换整个记录的函数:
type Lens' a b = forall f. Functor f => (b -> f b) -> (a -> f a)
纯粹修改一条记录,现在我们需要一个可以调用的辅助函数over
,只需要定义一次:
over :: Lens' a b -> (b -> b) -> a -> a
over l f p = runIdentity $ l (Identity . f) p
例如:
*Main> over counterLens (+1) (Person "foo" 40)
Person {name = "foo", counter = 41}
我们已经抽象了修改函数和它可能产生的效果,但是我们仍然需要为每个字段定义这些"lenses",这很烦人。在实践中,人们使用 Template Haskell 来自动定义它们并避免样板。
但是如果我们想要一个 单个 函数让我们指定字段的名称呢?遗憾的是,这更复杂。您需要一种将类型级字符串作为参数传递的方法,以及一个 multi-parameter type class that encodes the relationship between a field's name, the type of the record, and the type of the field. There are some packages 来执行此操作(同样,模板 Haskell 帮助样板文件),但据我所知,它们并未被广泛使用。
镜头的主库称为 lens, and there is also microlens,这是一种具有较轻依赖足迹的替代方案。它们是可互操作的:使用一个库定义的镜头可以与另一个库一起使用。
使用lens你可以这样写:
incrementPersonCounter :: Person -> Person
incrementPersonCounter = counter +~ 1
示例:
λ> incrementPersonCounter $ Person "foo" 42
Person {_name = "foo", _counter = 43}
完整代码:
{-# LANGUAGE TemplateHaskell #-}
module Lib where
import Control.Lens
data Person = Person
{
_name :: String
, _counter :: Int
} deriving (Show, Eq)
makeLenses ''Person
incrementPersonCounter :: Person -> Person
incrementPersonCounter = counter +~ 1
data Person = Person
{
name :: String
, counter :: Int
}
incrementPersonCounter :: Person -> Person
incrementPersonCounter p@(Person _ c) = p { counter = c + 1 }
有没有更简洁的方法来做上面的事情?是否有我可以使用的函数,我可以在其中指定记录、它的字段之一(在本例中为 name
/ counter
)和一个应用于 return 值的函数?
我在想一些事情:
applyRecord r f f' = r
{ f = f' (f r) }
尽管这行不通,因为:
error: Not in scope: ‘f’
|
13 | { f = f' (f r) }
概括incrementPersonCounter
的一种方法是对修改函数进行抽象:
modifyPersonCounter :: (Int -> Int) -> Person -> Person
modifyPersonCounter f p = (\c -> p { counter = c}) $ f (counter p)
事实上,一个常见的模式是对我们想要在该领域执行的效果进行抽象:
counterLens :: forall f. Functor f => (Int -> f Int) -> (Person -> f Person)
counterLens f p = (\c -> p { counter = c }) <$> f (counter p)
例如,我们可能希望从控制台或数据库中读取计数器增加(两者都是 IO
效果)。
我们可以给synonym这样的函数类型,给定一个(可能有效的)更改字段的方法,return一个转换整个记录的函数:
type Lens' a b = forall f. Functor f => (b -> f b) -> (a -> f a)
纯粹修改一条记录,现在我们需要一个可以调用的辅助函数over
,只需要定义一次:
over :: Lens' a b -> (b -> b) -> a -> a
over l f p = runIdentity $ l (Identity . f) p
例如:
*Main> over counterLens (+1) (Person "foo" 40)
Person {name = "foo", counter = 41}
我们已经抽象了修改函数和它可能产生的效果,但是我们仍然需要为每个字段定义这些"lenses",这很烦人。在实践中,人们使用 Template Haskell 来自动定义它们并避免样板。
但是如果我们想要一个 单个 函数让我们指定字段的名称呢?遗憾的是,这更复杂。您需要一种将类型级字符串作为参数传递的方法,以及一个 multi-parameter type class that encodes the relationship between a field's name, the type of the record, and the type of the field. There are some packages 来执行此操作(同样,模板 Haskell 帮助样板文件),但据我所知,它们并未被广泛使用。
镜头的主库称为 lens, and there is also microlens,这是一种具有较轻依赖足迹的替代方案。它们是可互操作的:使用一个库定义的镜头可以与另一个库一起使用。
使用lens你可以这样写:
incrementPersonCounter :: Person -> Person
incrementPersonCounter = counter +~ 1
示例:
λ> incrementPersonCounter $ Person "foo" 42
Person {_name = "foo", _counter = 43}
完整代码:
{-# LANGUAGE TemplateHaskell #-}
module Lib where
import Control.Lens
data Person = Person
{
_name :: String
, _counter :: Int
} deriving (Show, Eq)
makeLenses ''Person
incrementPersonCounter :: Person -> Person
incrementPersonCounter = counter +~ 1