函子:恒等律、复合律和 fmap
Functors: Identity, Composition, and fmap

原始链接: https://codehakase.com/blog/2025-03-26-on-functors/

Haskell 中的 Functor 提供了一种方法,可以使用 `fmap` 函数(或其中缀版本 `<$>`)将函数应用于包含在上下文中的值,例如 `Maybe`、列表或 `IO`。这至关重要,因为直接函数应用在上下文值上经常失败。 `Functor` 类型类定义了这种能力,要求其实例实现 `fmap`,它接收一个函数 `(a -> b)` 和一个上下文值 `f a`,返回一个转换后的上下文值 `f b`。 理解 Functor 的关键在于 Functor 定律。同一性律指出 `fmap id` 不应改变函子的值,确保没有意外更改。组合律规定 `fmap (f . g)` 等价于 `fmap f . fmap g`,保证了与函数组合一致的行为。 通过遵守这些定律,Functor 提供了一种可预测且结构化的方式来操作上下文中的值,从而产生更简洁、更易维护的函数式代码。这种抽象简化了处理各种数据类型和复杂操作的工作。

这篇Hacker News帖子讨论了函子,特别是恒等函子、函子组合和`fmap`函数。用户munchler提出了一个问题:在Haskell中,`Set e`(其中`Set e`表示类型为`e`的元素的集合)是否是一个函子?他们还询问了答案在不同编程语言中是否有所不同,并以F#的`Set<'e>`为例。另一位用户chrisoverzero最初(用ROT13编码)暗示`Set e`可能不是函子,原因是元素类型`e`的要求。然而,经过进一步研究,chrisoverzero改变了答案,认为是函子,但有一个排序约束(隐含的“Ord”)。他们对实现细节如何变得相关感到惊讶。讨论的中心是像`Set e`这样的数据类型满足函子要求所需的属性,以及特定语言的特性(如排序)如何影响其函子特性。
相关文章

原文

In writing software, we often encounter scenarios where a value resides within a context, a container of sorts. Standard function application, so straightforward with simple values, presents a challenge in these situations. Consider the Maybe data type in Haskell, which encapsulates the possibility of a value’s absence. Applying functions to values wrapped in Maybe requires a different approach, as direct function application results in a type error.

For example, applying the function (+4) to the integer 2 is trivial:

However, attempting to apply the same function directly to a Maybe wrapped value results in a type error:

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

This is where fmap comes in. fmap provides a way to apply a function to a value within a wrapped context. In the case of Maybe, fmap allows us to apply (+4) to the integer inside a Just context:

ghci> fmap (+4) (Just 2)
Just 6

This leads us to the concept of Functors. The Functor typeclass in Haskell represents any type that can be mapped over. Lists, for example, are instances of the Functor typeclass. The definition of the Functor typeclass is as follows:

class Functor f where
    fmap :: (a -> b) -> f a -> f b

This typeclass defines only one function, fmap. This polymorphic function, works for any type constructor f that is an instance of the Functor typeclass. It takes :

  1. A function of type a -> b, which transforms a value of type a into a value of type b.
  2. A value of type f a, which represents a value of type a within a functorial context f. It then returns a value of type f b, which is a value of type b within the same functorial context f. Essentially, fmap applies the function to the value(s) inside the functorial context.

As demonstrated earlier, we can use fmap (+4) (Just 2) because Maybe is a functor. fmap applies the function to the values inside the Maybe context. The Maybe type itself is defined as:

data Maybe a = Nothing | Just a

instance Functor Maybe where
    fmap f (Just x) = Just (f x)
    fmap f Nothing = Nothing

This implementation reveals that if we have a value of Nothing, fmap will return Nothing. If we have a value wrapped in Just, fmap applies the function to the contents of the Just context.

In languages like Go, which lacks an explicit Maybe type, we often use error handling or other mechanisms to achieve similar results.

For instance, consider this Go code:

metadata, err := user.GetMetadata()
if err != nil {
    return err
}

if metadata == nil {
    return nil
}
return metadata.ProjectedValue

The Haskell equivalent:

getProjectedValue <$> (User.getMetadata "uuid")

Here, <$> is the infix version of fmap. If User.getMetadata returns Just Metadata, we can extract the projected value from it. If it returns Nothing, we’ll return Nothing.

Functor Laws

For a type to be considered a proper Functor, it must adhere to certain laws:

Identity Law

Applying fmap id should not change the functor’s value.

This law ensures that Functors don’t introduce any unexpected changes or side effects when traversing their structure with a function that shouldn’t modify anything.

As we can see from the Maybe implementation of fmap, this law holds.

instance Functor Maybe where
    fmap f (Just x) = Just (f x)
    fmap f Nothing = Nothing

Application of the identity law:

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"

Even in the IO Functor, fmap id leaves the action unchanged.

Composition Law

Applying fmap to a composed function should be the same as applying fmap separately in sequence.

This ensures that Functors behave consistently when dealing with function composition. This is crucial for maintaining predictability and allowing for easier reasoning about code that uses Functors.

fmap (f . g) == fmap f . fmap g

Application of the composition law:

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]

Conclusion

Functors in Haskell provide a powerful abstraction for applying functions to values wrapped in a context, such as Maybe, lists, or IO. By following the identity and composition laws, they ensure predictable behavior and maintain structure while enabling clean, concise, and expressive code. By leveraging fmap, we can seamlessly work with values inside different contexts, making functional programming more flexible and easier to reason about.

联系我们 contact @ memedata.com