Functors: Identity, Composition 和 fmap 的理解与应用
Functors: Identity, Composition, and fmap
2025年3月26日
在编写软件时,我们经常遇到值存在于上下文(可以理解为某种容器)中的情况。对于简单值来说,标准的函数应用非常直接,但在这些情况下,应用函数会面临挑战。例如,Haskell 中的 Maybe
数据类型封装了一个值可能不存在的情况。 对包裹在 Maybe
中的值应用函数需要不同的方法,因为直接应用函数会导致类型错误。
例如,将函数 (+4)
应用于整数 2
非常简单:
ghci> (+4) 2
6
但是,尝试将同一个函数直接应用于 Maybe
包裹的值会导致类型错误:
ghci> x = Just 2
ghci> (+4) x
<interactive>:25:1: error: [GHC-39999]
• No instance for ‘Num (Maybe Integer)’ arising from a use of ‘it’
• In the first argument of ‘print’, namely ‘it’
In a stmt of an interactive GHCi command: print it
这时 fmap 就派上用场了。fmap
提供了一种将函数应用于包裹在上下文中的值的方法。对于 Maybe
,fmap
允许我们将 (+4)
应用于 Just
上下文中的整数:
ghci> fmap (+4) (Just 2)
Just 6
这引出了 Functor 的概念。Haskell 中的 Functor
typeclass 1 表示任何可以被映射的类型。例如,列表是 Functor
typeclass 的实例。Functor
typeclass 的定义如下:
class Functor f where
fmap :: (a -> b) -> f a -> f b
这个 typeclass 只定义了一个函数,fmap
。这个多态函数适用于任何作为 Functor
typeclass 实例的类型构造器 f
。它接受:
- 一个类型为
a -> b
的函数,它将类型a
的值转换为类型b
的值。 - 一个类型为
f a
的值,它表示 Functor 上下文f
中的类型a
的值。然后它返回一个类型为f b
的值,它是同一 Functor 上下文f
中的类型b
的值。本质上,fmap
将函数应用于 Functor 上下文中的值。
正如前面演示的,我们可以使用 fmap (+4) (Just 2)
,因为 Maybe
是一个 Functor。 fmap
将函数应用于 Maybe
上下文中的值。 Maybe
类型本身的定义如下:
data Maybe a = Nothing | Just a
instance Functor Maybe where
fmap f (Just x) = Just (f x)
fmap f Nothing = Nothing
这个实现表明,如果我们有一个 Nothing
值,fmap
将返回 Nothing
。 如果我们有一个包裹在 Just
中的值,fmap
将函数应用于 Just
上下文的内容。
在像 Go 这样的缺少显式 Maybe
类型的语言中,我们经常使用错误处理或其他机制来实现类似的结果。
例如,考虑以下 Go 代码:
metadata, err := user.GetMetadata()
if err != nil {
return err
}
if metadata == nil {
return nil
}
return metadata.ProjectedValue
Haskell 等价物:
getProjectedValue <$> (User.getMetadata "uuid")
在这里,<$>
是 fmap
的中缀版本。 如果 User.getMetadata
返回 Just Metadata
,我们可以从中提取 projected value。 如果它返回 Nothing
,我们将返回 Nothing
。
Functor Law
为了使一个类型被认为是合适的 Functor,它必须遵守某些定律:
Identity Law (同一性定律)
应用 fmap id
不应更改 Functor 的值。
此定律确保 Functor 在使用不应修改任何内容的函数遍历其结构时,不会引入任何意外更改或副作用。
从 fmap
的 Maybe
实现中我们可以看到,这个定律成立。
instance Functor Maybe where
fmap f (Just x) = Just (f x)
fmap f Nothing = Nothing
同一性定律的应用:
Maybe:
ghci> fmap id (Just "change")
Just "change"
ghci> id (Just "change")
Just "change"
ghci> fmap id Nothing
Nothing
ghci> id Nothing
Nothing
List:
ghci> fmap id [1, 2, 3, 4]
[1, 2, 3, 4]
ghci> id [1, 2, 3, 4]
[1, 2, 3, 4]
IO:
ghci> fmap id (return "test")
"test"
ghci> id (return "test")
"test"
即使在 IO
Functor 中,fmap id
也不会更改该操作。
Composition Law (组合律)
将 fmap
应用于组合函数应与按顺序分别应用 fmap
相同。
这确保 Functor 在处理函数组合时表现一致。 这对于保持可预测性并更容易推理使用 Functor 的代码至关重要。
fmap (f . g) == fmap f . fmap g
组合律的应用:
Maybe:
ghci> let f = (*3)
ghci> let g = (+5)
ghci> let c = f . g
ghci> fmap c (Just 10)
Just 45
ghci> fmap f (fmap g (Just 10))
Just 45
List:
ghci> let f = (*3)
ghci> let g = (+5)
ghci> let c = f . g
ghci> fmap c [1, 2, 3]
[18,21,24]
ghci> fmap f (fmap g [1, 2, 3])
[18,21,24]
结论
Haskell 中的 Functor 为将函数应用于包裹在上下文中的值(例如 Maybe
、列表或 IO
)提供了一个强大的抽象。 通过遵循 identity 和 composition laws,它们确保了可预测的行为并保持了结构,同时实现了干净、简洁和富有表现力的代码。 通过利用 fmap
,我们可以无缝地处理不同上下文中的值,从而使函数式编程更加灵活且易于推理。
- Haskell 中的 typeclass 为不同类型定义了一个共享接口,通过类型特定的实现启用多态性 ↩︎