Dies ist Teil 3 einer Artikelserie mit dem Titel Funktionale Muster.
Schauen Sie sich unbedingt auch die restlichen Artikel an!
- Das Monoid
- Kompositionen und Implizitheit
Um korrekt zu sein, muss eine Funktion einer Typprüfung unterzogen werden und ist daher beweisbar. Aber im Fall von verallgemeinerten Funktionen, die für den Umgang mit verschiedenen Typen gedacht sind, zeigt sich dies sofort als Problem. Damit eine Doppelfunktion typübergreifend funktioniert, müssten wir sie separat definieren!
doubleInt :: Int -> Int doubleChar :: Char -> Char doubleFloat :: Float -> Float -- ...
Und jeder Programmierer, der etwas auf sich hält, sollte darüber schon absolut entsetzt sein. Wir haben gerade etwas über ein Muster für den Aufbau der Fallbehandlung mithilfe von Teilanwendung erfahren, können es hier jedoch nicht wirklich anwenden, da unsere Typsignaturen dies nicht zulassen und unsere Funktion dies zur Typprüfung.
Glücklicherweise ist dies in den meisten modernen Programmiersprachen bereits eine Funktion. Wir dürfen einen generischen Typ definieren. Einhypothetischer Typ, der nur übereinstimmende Positionen in der Funktionssignatur oder Variablendeklarationen überprüfen muss.
// 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
Generika zur Verfügung gestellt werden, kann er zur Laufzeit herausfinden, welche Typen er verwenden muss (Rust führt diesen Rückschluss tatsächlich immer noch zur Kompilierungszeit durch!).
Obwohl diese Implementierung durchaus sinnvoll ist, gibt es immer noch einen eklatanten Fehler, auf den der Haskell-Compiler tatsächlich hinweist, da der obige Haskell-Code tatsächlich einen Fehler auslöst.
Keine Instanz für „Num a“, die sich aus der Verwendung von „*“ ergibt...Wir haben einen Typ definiert, aber wir werden nicht immer sicher sein, dass dieser Typ die
Kapazität zum Verdoppeln hat. Sicher, das funktioniert sofort bei Zahlen, aber was hindert den Benutzer daran, double für einen String aufzurufen? Eine Liste? Ohne eine vordefinierte Methode zum Verdoppeln dieser Typen sollten sie überhaupt nicht als Argumente zulässig sein.
Im Gegensatz zum NamenGenerika müssen wir also etwas spezifischer, aber dennoch allgemeiner vorgehen.
Hier kommenTypklassen ins Spiel, die in der Imperativwelt auch häufiger als Schnittstellen bekannt sind. Auch hier gilt: Wenn Sie eine Sprache verwenden, die später als C++ erstellt wurde, sollten Sie Zugriff auf einige Implementierungen von Schnittstellen haben.
Schnittstellen spezifizieren im Vergleich zu Generika eine ArtFähigkeit von Typen, die darunter kategorisiert werden können.
Hier ist eine korrigierte Version unseres vorherigen Codes.
double :: (Num a) => a -> a -- a has to be of typeclass Num double = (*2)
// 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 }
Attribute eines Typs definieren können, die unter einer Schnittstelle liegen, sollten Sie wissen, dass reine Schnittstellen nur Funktionen oder Fähigkeitendes Typs. Und bei Fähigkeiten geht es in diesem Zusammenhang darum, ob der Typ eine
Abhängigkeit in Form einer Verdopplungsfunktion hat – wird dem Compiler beigebracht, wie man sie verdoppelt?
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)
Aber in dieser feinkörnigen Kontrolle der Umsetzung liegt tatsächlich die Stärke davon. Wenn Sie schon einmal von dem
Strategiemuster gehört haben, ist dies im funktionalen Sinne so ziemlich alles.
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
haben, wie Double-Typen unter der Typklasse CanDouble erfolgen. In Go können wir etwas Ähnliches erreichen, mit der großen Einschränkung, dass wir Schnittstellenmethoden nur für
nicht-primitive Typen definieren können. Das heißt, wir müssen Wrapper-Strukturen für primitive Typen definieren.
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 }
Kategorien
KategorientheorieWir haben uns in „The Monoid“ kurz mit der
befasst, und wir möchten, dass es dabei bleibt, nur enge Begegnungen. Ich werde hier und da darauf verweisen, aber seien Sie versichert: Sie müssen keine Vorkenntnisse darin haben, um zu verstehen, was folgt. Es besteht jedoch kein Zweifel daran, dass wir schon einmal auf
Setsgestoßen sind. Um es kurz zusammenzufassen: Sets können als eine
Sammlungvon Elementen betrachtet werden. Diese Elemente können absolut alles sein.
{ 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)
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!
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).
Hier bringt uns die übermäßige Vereinfachung ein wenig durcheinander, denn wenn wir nur unserer Definition folgen, könnten wir sagen, dass reduzierende Funktionen wie sum und concat Funktoren aus der Kategorie der Arrays bis hin zu Atomen sind, aber das ist nicht unbedingt der Fall WAHR. Da Funktoren auch die Bewahrung der kategorialen Struktur erfordern, wird diese in dieser Artikelserie nicht behandelt, da sie viel zu tief in der Kategorientheorie verwurzelt ist.
Es tut uns leid, wenn dieser Artikel viel mehr Mathematik als Anwendungen enthielt, aber das Verständnis dieser Definitionen wird uns sehr dabei helfen, die schwierigeren Muster später in dieser Serie zu verstehen, nämlich Applikative und schließlich Monaden.
Eine Monade ist ein Monoid in der Kategorie der Endofunktoren.
Wir sind am Ziel! :>
Das obige ist der detaillierte Inhalt vonFunktionsmuster: Schnittstellen und Funktoren. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!