L '«isomorphisme» est l'un des concepts de base des mathématiques modernes. En utilisant des exemples concrets dans Haskell et C #, je vais non seulement expliquer la théorie aux non-mathématiciens (sans utiliser de symboles et de termes mathématiques obscurs), mais je montrerai également comment cela peut être utilisé dans la pratique quotidienne.
Le problème est que l'égalité stricte (par exemple, 2 + 2 = 4) est souvent trop stricte. Voici un exemple:
Haskelladd :: (a, a) -> a add (x, y) = x + y
C # int Add(Tuple<int, int> pair) { return pair.Item1 + pair.Item2; }
Cependant, il existe un autre moyen - plus délicat et dans de nombreuses situations beaucoup plus pratique - de définir la même fonction dans un sens :
Haskell add' :: a -> a -> a add' x = \y -> x + y
C # Func<int, int> Add_(int x) { return y => x + y; }
Contrairement au fait évident que pour deux x, y les deux fonctions retourneront toujours le même résultat, elles ne satisfont pas à une stricte égalité:
- la première fonction renvoie immédiatement le montant (c'est-à-dire effectue le calcul au moment de l'exportation),
- tandis que la deuxième fonction renvoie une autre fonction (qui au final renverra la somme - si quelqu'un l'appelle, bien sûr, sinon aucun calcul ne sera effectué: ceci est un exemple de calcul retardé et il y a aussi un isomorphisme ici, auquel je reviendrai un peu plus tard).
Et c'est "être trop strict".
L'isomorphisme est "assez strict"; elle ne requiert pas une égalité complète et globale, mais se limite à l'égalité «dans un certain sens», qui est toujours déterminée par un contexte spécifique.
Comme vous pouvez le deviner, les deux définitions ci-dessus sont isomorphes. Cela signifie exactement ce qui suit: si un seul d'entre eux m'est donné, alors les deux me sont donnés implicitement : tout cela grâce à l' isomorphisme - un convertisseur bidirectionnel de l'un en un autre . Résumant un peu les types:
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); }
... et maintenant pour tout x, y :
Haskell curry add $ x, y = uncurry add' $ (x, y)
C # Curry(Add)(x)(y) = Uncurry(Add_)(Tuple.Create(x, y))
Un peu plus de maths pour les plus curieuxEn fait, cela devrait ressembler à ceci:
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)); }
Cela devrait être compris comme «rien ne s'est passé». L'isomorphisme étant par définition un transformateur bidirectionnel, vous pouvez toujours 1) prendre une chose, 2) la convertir en une autre et 3) la reconvertir en la première. Il n'y a que deux opérations de ce type: car à la première étape (n ° 1), le choix se fait entre deux options seulement. Et dans les deux cas, l'opération devrait aboutir exactement au même résultat, comme si rien ne s'était passé (c'est pour cette raison qu'il s'agit d'une stricte égalité - car rien n'a changé du tout, et pas «quelque chose» n'a pas changé).
En plus de cela, il existe un théorème selon lequel l'élément id est toujours unique. Notez que la fonction Id est générique, polymorphe et donc vraiment unique en ce qui concerne chaque type particulier.
L'isomorphisme est très, très utile précisément parce qu'il est strict, mais pas trop. Il conserve certaines propriétés importantes (dans l'exemple ci-dessus - le même résultat avec les mêmes arguments), tout en vous permettant de transformer librement les structures de données elles-mêmes (porteuses de comportement et de propriétés isomorphes). Et cela est absolument sûr - parce que l'isomorphisme fonctionne toujours dans les deux sens, ce qui signifie que vous pouvez toujours revenir en arrière sans perdre ces «propriétés importantes». Je vais vous donner un autre exemple qui est si utile dans la pratique qu'il sous-tend même de nombreux langages de programmation "avancés" comme Haskell:
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(); }
Cet isomorphisme préserve le résultat du calcul différé lui-même - c'est la «propriété importante», tandis que les structures de données sont différentes.
La conclusion? La POO, particulièrement fortement typée, fonctionne (forcée) au niveau de "stricte égalité". Et donc - dans le sillage des exemples ci-dessus - c'est souvent trop strict. Lorsque vous vous habituez à penser «trop strictement» (et cela se produit imperceptiblement - cela s'infiltre dans le programmeur, surtout s'il ne cherche pas d'inspiration en mathématiques), vos décisions perdent involontairement leur flexibilité souhaitée (ou du moins objectivement possible). Comprendre l’isomorphisme - dans une communauté qui cherche consciemment à être plus attentive à la sienne
et code étranger - il aide à définir plus clairement le cercle des "propriétés importantes", en faisant abstraction des détails inutiles: à savoir, des structures de données spécifiques sur lesquelles ces "propriétés importantes" sont imprimées (ce sont aussi des "détails de mise en œuvre", d'ailleurs). Tout d'abord, c'est une façon de penser, et alors seulement - des solutions (micro) architecturales plus efficaces et, comme conséquence naturelle, une approche révisée des tests.
PS Si je constate que l'article en a profité, je reviendrai sur les thèmes des «solutions (micro) architecturales plus performantes» et «d'une approche révisée des tests».