首页  >  文章  >  后端开发  >  函数式模式:接口和函子

函数式模式:接口和函子

WBOY
WBOY原创
2024-07-18 21:18:51472浏览

这是题为 函数模式 的系列文章的第 3 部分。

请务必查看其余文章!

  1. 幺半群
  2. 组合和隐式

泛型和类型类

为了正确,函数必须进行类型检查,因此是可证明的。但对于旨在处理各种类型的通用函数来说,这立即显示为一个痛点。为了使双精度函数能够跨类型工作,我们必须单独定义它们!

doubleInt :: Int -> Int
doubleChar :: Char -> Char
doubleFloat :: Float -> Float
-- ...

对于任何有自尊心的程序员来说,你应该已经发现自己对此感到绝对震惊。我们刚刚了解了使用部分应用程序构建案例处理的模式,但我们不能在这里真正应用它,因为我们的类型签名不允许这样做,而我们的函数 进行类型检查。

值得庆幸的是,这已经是大多数现代编程语言的一个功能。我们可以定义一个泛型类型。一种假设的类型,只需验证函数签名或变量声明中的匹配位置。

// c++
template <typename T>
T double(T x) {
    return x*2;
}
// rust
fn double<T>(x: T) -> T {
    return x*2;
}
-- haskell
double :: a -> a
double = (*2)       -- partially applied multiplication

这应该可以解决我们的问题!只要给编译器提供这些泛型,它就可以弄清楚在运行时必须使用什么类型(Rust 实际上仍然在编译时进行这种推断!)。

然而,即使这个实现有优点,仍然存在一个明显的缺陷,Haskell 编译器实际上指出了这个缺陷,因为上面的 Haskell 代码实际上引发了一个错误。

没有因使用“*”而产生“Num a”的实例...

我们定义了一个类型,但我们并不总是能确保该类型的容量加倍。当然,这立即适用于数字,但是是什么阻止用户在字符串上调用 double 呢?一个清单?如果没有预定义的方法来加倍这些类型,那么首先就不应该允许它们作为参数。

因此,与泛型的名称相反,我们必须获得更多具体,但仍然通用的

这就是类型类出现的地方,或者在命令式世界中也更常见地称为接口。再次强调,如果您使用任何晚于 C++ 的语言,您应该有权访问某些接口的实现。

与泛型相比,接口指定了某种类型的功能,可以在其下分类

这是我们之前代码的修复版本。

double :: (Num a) => a -> a     -- a has to be of typeclass Num
double = (*2)

或在 Go 中:

// We first create an interface that is the union of floats and integers.
type Num interface {
    ~int | ~float64
    // ... plus all other num types
}

func double[T Num](a T) T {
    return a * 2
}

为了简洁起见,我们会说 Haskell 并不真正处理其接口中的嵌入状态,例如 Typescript 和 Go 的接口(纯函数规则带来的约束)。因此,即使您可能能够在接口下定义类型所需的属性,也要知道接口只需要定义函数该类型的功能

通过这种上下文中的功能,我们讨论的是类型是否具有加倍函数形式的依赖性 - 编译器教导如何加倍它?

import Control.Monad (join)

class CanDouble a where
  double :: a -> a

instance CanDouble Int where
  double = (* 2)

instance CanDouble Float where
  double = (* 2)

-- we tell the compiler that doubling a string is concatenating it to itself.
instance CanDouble String where 
  double = join (++)    -- W-combinator, f x = f(x)(x)

现在我们在代码重复方面几乎回到了开始的状态,这不是很有趣吗?

但是这种对实现的细粒度控制实际上正是它的力量所在。如果您以前听说过策略模式,那么从功能意义上来说,这就是它了。

quadruple :: (CanDouble a) => a -> a
quadruple = double . double

leftShift :: (CanDouble a) => Int -> a -> a
leftShift n e
  | e <= 0 = n
  | otherwise = leftShift (double n) $ e - 1

现在对这些函数进行类型检查,这一切都是因为我们编译器如何在 CanDouble 类型类下进行双精度类型。

我们可以在 Go 中实现类似的功能,但需要注意的是,我们只能在 非原始 类型上定义接口方法。这意味着,我们必须将包装结构定义为原始类型。

type CanDouble interface {
    double() CanDouble
}

type String string
type Number interface {
    ~int | ~float64
    // ... plus all other num types
}

type Num[T Number] struct {
    v T
}

func (s String) double() String {
    return s + s
}

func (n Num[T]) double() Num[T] {
    return Num[T]{n.v * 2}
}

func quadruple(n CanDouble) CanDouble {
    return n.double().double()
}

func leftShift(n CanDouble, e uint) CanDouble {
    for i := uint(0); i < e; i++ {
        n = n.double()
    }

    return n
}

老实说,这有点令人失望,但不用担心,因为大多数时候你要处理的接口都是自定义类型和结构。

类别

范畴论是数学结构及其关系的一般理论。

我们在《幺半群》中简单地回顾了范畴论,我们希望保持这种状态,只在近距离接触时使用。我会到处引用它,但请放心:您不需要有其中的背景知识就可以掌握接下来的内容。

但是,毫无疑问,我们以前遇到过集合

简单回顾一下,集合可以被认为是元素的集合。这些元素绝对可以是任何东西

{ 0, 1, 2, 3, ... }             -- the set of natural numbers
{ a, b, c, ..., z}              -- the set of lowercase letters
{ abs, min, max, ... }          -- the set of `Math` functions in Javascript
{ {0, 1}, {a, b}, {abs, min} }  -- the set of sets containing the first 2 elements of the above sets

Adding on to that, we have these things called morphisms, which we can think of a mapping between elements.

Very big omission here on the definitions of morphisms, in that they are relations between elements, and not strictly functions/mappings,
you can look it up if you are curious.

We can say a function like toUpper() is a morphism between lowercase letters to uppercase letters, just like how we can say double = (*2) is a morphism from numbers to numbers (specifically even numbers).

And if we group these together, the set of elements and their morphisms, we end up with a category.

Again, omission, categories have more constraints such as a Composition partial morphism and identities. But these properties are not that relevant here.

If you have a keen eye for patterns you'd see that there is a parallel to be drawn between categories and our interfaces! The objects (formal name for a category's set of elements) of our category are our instances, and our implementations are our morphisms!

class CanDouble a where
    double :: a -> a

-- `Int` is our set of elements { ... -1, 0, 1, ... }
-- `(* 2)` is a morphism we defined
-- ... (other omissions)
-- ...
-- Therefore, `CanDouble Int` is a Category.
instance CanDouble Int where
    double = (* 2)

Functors

Man, that was a lot to take in. Here's a little bit more extra:

A Functor is a type of a function (also known as a mapping) from category to another category (which can include itself, these are called endofunctors).

What this essentially means, is that it is a transformation on some category that maps every element to a corresponding element, and every morphism to a corresponding morphism. An output category based on the input category.

In Haskell, categories that can be transformed by a functor is described by the following typeclass (which also makes it a category in of itself, that's for you to ponder):

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

f here is what we call a type constructor. By itself it isn't a concrete type, until it is accompanied by a concrete type. An example of this would be how an array isn't a type, but an array of Int is. The most common form of a type constructor is as a data type (a struct).

From this definition we can surmise that all we need to give to this function fmap is a function (a -> b) (which is our actual functor, don't think about the naming too much), and this would transform a type f a to type f b, a different type in the same category.

Yes, this means Haskell's Functor typeclass is actually a definition for endofunctors, woops!

Functional Patterns: Interfaces and Functors

If all of that word vomit was scary, a very oversimplified version for the requirement of the Functor typeclass is that you are able to map values to other values in the same category.

Arguably the most common Functor we use are arrays:

instance Functor [] where
--  fmap f [] = []
--  fmap f (a:as) = f a : fmap as

    -- simplified
    fmap :: (a -> b) -> [a] -> [b]
    fmap f arr = map f arr

We are able to map an array of [a] to [b] using our function (or functor) f. The typeconstructor of [] serves as our category, and so our functor is a transformation from one type of an array to another.

So, formally: the map function, though commonly encountered nowadays in other languages and declarative frameworks such as React, is simply the application of an endofunctor on the category of arrays.

Wow. That is certainly a description.

Here are more examples of functors in action:

// Go
type Functor[T any] interface {
    fmap(func(T) T) Functor[T]
}

type Pair[T any] struct {
    a T
    b T
}

type List[T any] struct {
    get []T
}

// Applying a functor to a Pair is applying the function
// to both elements
func (p *Pair[T]) fmap(f func(T) T) Pair[T] {
    return Pair[T]{     // apply f to both a and b
        f(p.a),
        f(p.b),
    }
}

func (a *List[T]) fmap(f func(T) T) List[T] {
    res := make([]T, len(a.get))    // create an array of size len(a.get)

    for i, v := range a.get {
        res[i] = f(v)
    }

    return List[T]{res}
}
-- haskell
data Pair t = P (t, t)

instance Functor Pair where
    fmap f (P (x, y)) = P (f x, f y)

So all that it takes to fall under the Functor (again, endofunctor), interface is to have a definition on how to map the contents of the struct to any other type (including its own).

This is another simplifcation, functors also need to have property of identity and composition.

To put simply, whenever you do a map, you're not only transforming the elements of your array (or struct), you're also transforming the functions you are able to apply on this array (or struct). This is what we mean by mapping both objects and morphisms to different matching objects and morphisms in the same category.

This is important to note as even though we end up in the same category (in this context, we map an array, which results in another array), these might have differing functions or implementations available to them (though most of them will be mapped to their relatively equivalent functions, such as a reverse on an array of Int to reverse on an array of Float).

这就是过度简化让我们有点混乱的地方,因为如果我们只遵循我们的定义,我们可以说诸如 sum 和 concat 之类的约简函数是从数组类别到原子类别的函子,但这不一定真的。由于函子还要求您保留分类结构,本系列文章不会介绍这一点,因为它在范畴论中根深蒂固。


如果本文包含比应用程序更多的数学,但理解这些定义将极大地帮助我们理解本系列后面更难的模式,即应用程序和最后的 Monad。

单子是endofunctors类别中的幺半群

我们到了! :>

以上是函数式模式:接口和函子的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn