"Isomorphismus" ist eines der Grundkonzepte der modernen Mathematik. Anhand konkreter Beispiele in Haskell und C # werde ich nicht nur die Theorie für Nicht-Mathematiker erklären (ohne obskure mathematische Symbole und Begriffe zu verwenden), sondern auch zeigen, wie dies in der täglichen Praxis verwendet werden kann.
Das Problem ist, dass die strikte Gleichheit (zum Beispiel 2 + 2 = 4) oft zu streng ist. Hier ist ein Beispiel:
Haskelladd :: (a, a) -> a add (x, y) = x + y
C # int Add(Tuple<int, int> pair) { return pair.Item1 + pair.Item2; }
Es gibt jedoch noch eine weitere - schwierigere und in vielen Situationen viel praktischere - Möglichkeit, dieselbe Funktion in gewissem Sinne zu definieren:
Haskell add' :: a -> a -> a add' x = \y -> x + y
C # Func<int, int> Add_(int x) { return y => x + y; }
Im Gegensatz zu der offensichtlichen Tatsache, dass beide Funktionen fĂĽr zwei x, y immer dasselbe Ergebnis liefern, erfĂĽllen sie nicht die strikte Gleichheit:
- Die erste Funktion gibt sofort den Betrag zurĂĽck (d. h. fĂĽhrt die Berechnung zum Zeitpunkt des Exports durch).
- während die zweite Funktion eine andere Funktion zurückgibt (die am Ende die Summe zurückgibt - wenn jemand sie aufruft, wird natürlich keine Berechnung durchgeführt: Dies ist ein Beispiel für eine verzögerte Berechnung und es gibt auch einen Isomorphismus, auf den ich zurückkommen werde etwas später).
Und das ist "zu streng sein".
Der Isomorphismus ist "ziemlich streng"; es erfordert keine vollständige, allumfassende Gleichheit, sondern beschränkt sich auf die Gleichheit „in einem bestimmten Sinne“, die immer durch einen bestimmten Kontext bestimmt wird.
Wie Sie vielleicht erraten haben, sind beide obigen Definitionen isomorph. Dies bedeutet genau das Folgende: Wenn mir nur einer von ihnen gegeben wird, dann werden mir beide implizit gegeben : alles dank Isomorphismus - ein Zwei-Wege-Konverter von einem in einen anderen . Fassen Sie die Typen ein wenig zusammen:
Haskell curry :: ((a, b) → c) → a → b → c curry fxy = f (x, y), uncurry :: (a → b → c) → (a, b) → c uncurry f (x, y) = fxy
C # Func<TArg1, Func<TArg2, TRes>> Curry(Func<Tuple<TArg1, TArg2>, TRes> uncurried) { return arg1 => arg2 => uncurried(Tuple.Create(arg1, arg2)); } Func<Tuple<TArg1, TArg2>, TRes> Uncurry(Func<TArg1, Func<TArg2, TRes>> curried) { return pair => curried(pair.Item1)(pair.Item2); }
... und jetzt fĂĽr jedes x, y :
Haskell curry add $ x, y = uncurry add' $ (x, y)
C # Curry(Add)(x)(y) = Uncurry(Add_)(Tuple.Create(x, y))
Ein bisschen mehr Mathe fĂĽr die besonders NeugierigenIn der Tat sollte es so aussehen:
Haskell curry . uncurry = id uncurry . curry = id id x = x
C # Compose(Curry, Uncurry) = Id Compose(Uncurry, Curry) = Id, : T Id<T>(T arg) => arg; Func<TArg, TFinalRes> Compose<TArg, TRes, TFinalRes>( Func<TArg, TRes> first, Func<TRes, TFinalRes> second) { return arg => second(first(arg)); } ... extension- ( Id ): Curry.Compose(Uncurry) = Id Uncurry.Compose(Curry) = Id, : public static Func<TArg, TFinalRes> Compose<TArg, TRes, TFinalRes>( this Func<TArg, TRes> first, Func<TRes, TFinalRes> second) { return arg => second(first(arg)); }
Ich sollte verstehen, dass "nichts passiert ist". Da Isomorphismus per Definition ein Zwei-Wege-Transformator ist, können Sie immer 1) eine Sache nehmen, 2) sie in eine andere umwandeln und 3) sie zurück in die erste umwandeln. Es gibt nur zwei solche Operationen: In der ersten Phase (Nr. 1) stehen nur zwei Optionen zur Auswahl. Und in beiden Fällen sollte die Operation zu genau dem gleichen Ergebnis führen, als wäre überhaupt nichts passiert (aus diesem Grund ist eine strikte Gleichheit erforderlich - weil sich überhaupt nichts geändert hat und sich nicht „etwas“ geändert hat).
DarĂĽber hinaus gibt es einen Satz, dass das id-Element immer eindeutig ist. Beachten Sie, dass die Id-Funktion generisch, polymorph und daher in Bezug auf jeden bestimmten Typ wirklich einzigartig ist.
Isomorphismus ist sehr, sehr nützlich, gerade weil er streng ist, aber nicht zu viel. Es behält bestimmte wichtige Eigenschaften bei (im obigen Beispiel - dasselbe Ergebnis mit denselben Argumenten) und ermöglicht es Ihnen, die Datenstrukturen selbst frei zu transformieren (Träger von isomorphem Verhalten und Eigenschaften). Und das ist absolut sicher - denn Isomorphismus funktioniert immer in beide Richtungen, was bedeutet, dass Sie jederzeit zurückkehren können, ohne diese „wichtigen Eigenschaften“ zu verlieren. Ich werde ein weiteres Beispiel nennen, das in der Praxis so nützlich ist, dass es sogar viele "fortgeschrittene" Programmiersprachen wie Haskells unterstützt:
Haskell toLazy :: a -> () -> a toLazy x = \_ -> a fromLazy :: (() -> a) -> a fromLazy f = f ()
C # Func<TRes> Lazy(TRes res) { return () => res; } TRes Lazy(Func<TRes> lazy) { return lazy(); }
Dieser Isomorphismus bewahrt das Ergebnis der verzögerten Berechnung selbst - dies ist die „wichtige Eigenschaft“, während die Datenstrukturen unterschiedlich sind.
Die Schlussfolgerung? OOP, besonders stark typisiert, arbeitet (erzwungen) auf der Ebene der "strengen Gleichheit". Und deshalb ist es - im Anschluss an die obigen Beispiele - oft zu streng. Wenn Sie sich daran gewöhnen, „zu streng“ zu denken (und dies geschieht unmerklich - es dringt in den Programmierer ein, insbesondere wenn er nicht nach Inspiration in der Mathematik sucht), verlieren Ihre Entscheidungen unabsichtlich ihre gewünschte (oder zumindest objektiv mögliche) Flexibilität. Isomorphismus verstehen - in einer Gemeinschaft mit dem bewussten Versuch, mehr auf die eigenen zu achten
und Fremdcode - es hilft, den Kreis der "wichtigen Eigenschaften" klarer zu definieren und von unnötigen Details zu abstrahieren: nämlich von bestimmten Datenstrukturen, auf die diese "wichtigen Eigenschaften" aufgedruckt sind (sie sind auch "Implementierungsdetails"). Zuallererst ist dies eine Denkweise und erst dann - erfolgreichere (Mikro-) Architekturlösungen und als natürliche Konsequenz ein überarbeiteter Testansatz.
PS Wenn ich sehe, dass der Artikel davon profitiert hat, werde ich auf die Themen „erfolgreichere (Mikro-) Architekturlösungen“ und „einen überarbeiteten Testansatz“ zurückkommen.