>  기사  >  백엔드 개발  >  기능적 패턴: 인터페이스와 펑터

기능적 패턴: 인터페이스와 펑터

WBOY
WBOY원래의
2024-07-18 21:18:51405검색

이 글은 기능적 패턴이라는 제목의 기사 시리즈 중 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을 호출하는 것을 막는 것은 무엇입니까? 목록? 이러한 유형을 두 배로 늘리기 위해 미리 정의된 메서드가 없으면 애초에 인수로 허용되어서는 안 됩니다.

그래서

제네릭이라는 이름과 달리 좀 더 구체적이면서도 일반적인을 얻어야 합니다.

여기서

typeclasses가 사용되며 명령형 세계에서는 인터페이스로 더 일반적으로 알려져 있습니다. 다시 말하지만, 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)

그러나 구현을 세밀하게 제어하는 ​​것이 실제로 이 기능의 힘이 발휘되는 곳입니다. 이전에

전략

패턴에 대해 들어본 적이 있다면 기능적 측면에서 보면 이것이 거의 전부입니다.

이제 이 함수의 유형 검사는 우리가 CanDouble 유형 클래스에서 이중 유형을 지정하는 방법을 컴파일러에
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
가르쳤기

때문입니다. 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
}

카테고리

범주이론은 수학적 구조와 그 관계에 대한 일반적인 이론입니다.

우리는 The Monoid에서
범주 이론

을 간략하게 살펴보았는데, 가까운 만남만 유지하고 싶습니다. 여기저기서 참조하겠지만 안심하십시오. 다음 내용을 이해하기 위해 배경 지식이 필요하지 않습니다. 하지만 우리가 이전에도

세트

를 접한 적이 있다는 것은 의심의 여지가 없습니다. 간단히 요약하면 세트는 요소의

컬렉션

이라고 생각할 수 있습니다. 이러한 요소는 무엇이든 될 수 있습니다.

{ 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과 같은 축소 함수는 배열 범주에서 원자까지의 펑터라고 말할 수 있지만 반드시 그런 것은 아닙니다. 진실. 또한 펑터에서는 범주 구조보존해야 하는데, 이는 범주 이론에 너무 깊이 뿌리를 두고 있기 때문에 이 기사 시리즈에서는 다루지 않습니다.


이 기사에 애플리케이션보다 너무 더 많은 수학이 포함되어 있었다면 사과드립니다. 하지만 이러한 정의를 이해하면 이 시리즈의 뒷부분에 나오는 더 어려운 패턴, 즉 애플리케이션과 마지막으로 모나드를 이해하는 데 큰 도움이 됩니다.

모나드는 엔도펑터범주에 속하는 모노이드입니다.

우리는 거기에 도달하고 있습니다! :>

위 내용은 기능적 패턴: 인터페이스와 펑터의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.