"Isomorfismo" é um dos conceitos básicos da matemática moderna. Usando exemplos concretos em Haskell e C #, não apenas explicarei a teoria para não-matemáticos (sem usar símbolos e termos matemáticos obscuros), mas também mostrarei como isso pode ser usado na prática cotidiana.
O problema é que a igualdade estrita (por exemplo, 2 + 2 = 4) costuma ser muito estrita. Aqui está um exemplo:
Haskelladd :: (a, a) -> a add (x, y) = x + y
C # int Add(Tuple<int, int> pair) { return pair.Item1 + pair.Item2; }
No entanto, há uma maneira mais complicada e, em muitas situações, muito mais prática de definir a mesma função em um sentido :
Haskell add' :: a -> a -> a add' x = \y -> x + y
C # Func<int, int> Add_(int x) { return y => x + y; }
Ao contrário do fato óbvio de que, para quaisquer dois x, y, ambas as funções sempre retornarão o mesmo resultado, elas não satisfazem a igualdade estrita:
- a primeira função retorna imediatamente a quantidade (ou seja, executa o cálculo no momento da exportação),
- enquanto a segunda função retorna outra função (que no final retornará a soma - se alguém chamar, é claro, caso contrário, nenhum cálculo será realizado: este é um exemplo de cálculo atrasado e também há um isomorfismo aqui, ao qual retornarei um pouco mais tarde).
E isso é "ser muito rigoroso".
O isomorfismo é "bastante rigoroso"; não requer igualdade completa e abrangente, mas limita-se à igualdade "em certo sentido", que é sempre determinada por um contexto específico.
Como você pode imaginar, as duas definições acima são isomórficas. Isso significa exatamente o seguinte: se apenas um deles é dado a mim, então ambos são dados implicitamente : tudo graças ao isomorfismo - um conversor bidirecional de um para o outro . Resumindo um pouco os tipos:
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); }
... e agora para qualquer x, y :
Haskell curry add $ x, y = uncurry add' $ (x, y)
C # Curry(Add)(x)(y) = Uncurry(Add_)(Tuple.Create(x, y))
Um pouco mais de matemática para os especialmente curiososDe fato, deve ficar assim:
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)); }
A identificação deve ser entendida como "nada aconteceu". Como o isomorfismo é um transformador bidirecional por definição, você sempre pode 1) pegar uma coisa, 2) convertê-lo em outro e 3) convertê-lo de volta no primeiro. Existem apenas duas operações desse tipo: porque no primeiro estágio (Nº 1), a escolha é de apenas duas opções. E em ambos os casos, a operação deve levar exatamente ao mesmo resultado, como se nada tivesse acontecido (é por esse motivo que uma igualdade estrita está envolvida - porque nada mudou, e não "algo" não mudou).
Além disso, existe um teorema de que o elemento id é sempre único. Observe que a função Id é genérica, polimórfica e, portanto, verdadeiramente única em relação a cada tipo específico.
O isomorfismo é muito, muito útil precisamente porque é rigoroso, mas não muito. Ele preserva certas propriedades importantes (no exemplo acima - o mesmo resultado com os mesmos argumentos), permitindo transformar livremente as próprias estruturas de dados (portadoras de comportamento e propriedades isomórficas). E isso é absolutamente seguro - porque o isomorfismo sempre funciona nas duas direções, o que significa que você sempre pode voltar sem perder essas “propriedades importantes”. Vou dar outro exemplo que é tão útil na prática que até sustenta muitas linguagens de programação "avançadas" como a de 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(); }
Esse isomorfismo preserva o resultado da computação adiada - essa é a “propriedade importante”, enquanto as estruturas de dados são diferentes.
A conclusão? OOP, tipicamente fortemente tipado, (forçado) funciona no nível de "igualdade estrita". E, portanto - na sequência dos exemplos acima - muitas vezes é muito rigoroso. Quando você se acostuma a pensar "estritamente" (e isso acontece de maneira imperceptível - vaza para o programador, especialmente se ele não está buscando inspiração em matemática), suas decisões perdem inconscientemente a flexibilidade desejada (ou pelo menos objetivamente possível). Entendendo o isomorfismo - em uma comunidade com uma tentativa consciente de estar mais atento ao próprio
e código estrangeiro - ajuda a definir mais claramente o círculo de "propriedades importantes", abstraindo de detalhes desnecessários: a partir de estruturas de dados específicas nas quais essas "propriedades importantes" são impressas (elas também são "detalhes de implementação"). Antes de tudo, essa é uma maneira de pensar, e só então - soluções (micro) arquiteturais mais bem-sucedidas e, como conseqüência natural, uma abordagem revisada para testes.
PS Se constatar que o artigo foi beneficiado, voltarei aos tópicos de “soluções (micro) arquitetônicas mais bem-sucedidas” e “uma abordagem revisada para testes”.