首頁 >後端開發 >Golang >函數式模式:介面和函子

函數式模式:介面和函子

WBOY
WBOY原創
2024-07-18 21:18:51490瀏覽

這是題為 函數模式 的系列文章的第 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