What Isomorphic types are and why you might need them developing with Swift.

Maksym Teslia
Better Programming
Published in
7 min readNov 10, 2023

--

Image by author.

A bit of general math

* Note: we are not going to dive deep into theoretical definitions, instead will just discuss the conception using common terms.

An isomorphism is a correspondence (relation) between objects or systems of objects expressing the equality of their structures in some sense. — Encyclopedia of Mathematics.

Isomorphism is a core mathematical concept which is used for comparing various entities, such as groups, graphs, or vector spaces. It involves examination of their fundamental structure on order to determine if there is a correlation between their essential systemic properties. The presence of such correlation implies the isomorphism existence, confirming that these objects share a deep, intrinsic similarity.

In simple words, types are isomorphic when their properties are fundamentally related to each other. If we consider isomorphism from a practical perspective, we can describe it as a state when objects of different types can be converted to each other with no data loss. Actually, mathematical proof of this concept describes it very well. Let’s see how it looks like (very simplified and non-math-human readable):

Types A and B are isomorphic if there are two functions F: A -> B and G: B -> A, and for all objects (x) of type A calling G(F(x)) is equal to x as well as for all objects (y) of type B the result of calling F(G(y)) is equal to y.

“Very simplified and non-math-human readable”, but still does not look understandable enough? Well, let’s try to implement it in Swift, that should help to figure the above statement out, so you are able to fully grasp that knowledge.

Swift representation of this logic

In order to observe an example of Isomorphic behaviour in Swift, we would need to create a custom BoolImitation enumeration, which will have just two cases, pseudoTrue and pseudoFalse:

Now, you can construct two functions, one will accept BoolImitation instance and will return real Bool and second one will vice versa accept Bool and return BoolImitation:

For those who want to throw a rock in me due to “f” and “g” naming, I know it is not to be done this way, just want to be closer to math declarations, provided earlier.

Now, if you want to prove Bool and BoolImitation are isomorphic, you can call those functions in the manner, which has been described above:

As you can see, in both cases the resulting value of a call is gonna be the same as the value, which has been initially passed into a function chain. Let me explain how that works to enlighten it just in case. Will consider the first example.

When true is being passed inside of a g function, its inner logic transforms it into pseudoTrue along with returning it into an input parameter of function f. After that, f’s internal logic converts this pseudoTrue into true and returns it, so that it can be written into an fResult property. Logic of a second assignment is going to be operating in the same way.

If we shift closer to math and try to describe it using its language, we can state that “There are functions f of type (BoolImitation) -> Bool and g of type (Bool) -> BoolImitation, and for any object x of type BoolImitation calling g(f(x)) will result in returning x as well as for any object y if type Bool calling f(g(y)) will result in returning y”.

Therefore, remembering what we’ve studied in the first part, we can state that types can be transformed from one to another without losing any details, so, they are isomorphic!

How you can apply that knowledge

Issue

I bet you’ve seen such an example during your career path. You have two enumerations in your application, and both of them have identical cases naming. More than likely, they were represented as submodels of bigger view or data models.

That usually happens in applications which include ‘Buy-Use’ pattern of product handling, when you can order or purchase some product inside an application and then utilise in there. Typically, you receive the list of “available product” models from the server and then, once user purchases one of them, you construct “bought product” model, including “the type” of the product being secured. Let’s look closer into this pattern in order to check an example out.

So, we have two enums inside of our application, one is responsible for available product type and second one is responsible for purchased product type:

In the real world you’d have them inside of other bigger “AvailableProduct” and “PurchasedProduct” models, but let’s at that point ignore this for the sake of simplicity.

At some point, more than likely when the product is finally bought, you will have to transform “AvailableProduct” into a “PurchasedProduct”, including the type of it. Particularly for this, you’d need to construct this kind of function (other conditional checking approach can be used of course, but the conceptual idea would remain the same):

Generally, there is nothing wrong about it, but adding more products will make you to produce more cases, which will result in cyclomatic complexity increase, so your code becomes less readable and analysable.

Solution

If we inspect what happens inside getPurchasedProductType function, one specific behaviour can be spotted: in all the scenarios AvailableProductType instance will be transformed into a same name case of PurchasedProductType, therefore, in other words, they will be converted with no data loss.

Based on the above, we can state that those types are isomorphic, hence, there is a mathematical statement under the hood of their transformation. If there is one, we can surely describe it somehow within our application. For that, we would need to find some fundamental relation between their properties, which can be used by us in code. But what would that be?

I won’t muddy the waters and will tell you right away. For two enums with identically named cases that fundamental relation is a hashValue — if you have two enum cases with same names, it will be the same for both of them. If by now it is not clear why we would need it, hang tight, we will get to the implementation shortly.

Let’s start implementing our transforming functionality by establishing a protocol, which will describe any enum, which can be transformed through our custom isomorphic-related mechanism:

Protocol conformances are required to:

  • Hashable — to be sure that instance is going to be populated with a hashValue property.
  • CaseIterable — to be sure we are using an enum and to have the ability to iterate over the cases of this enum.

You will see why both of those protocols are required a bit further.

In order to conduct a transformation, we would need to create an “isomorphic context”, which is going to handle converting of one Isomorphicable instance into another one. In order to make this context reusable for any protocol conformant, we need to stick around generics and create a structure with two abstract types:

Instance of a structure will accept only one value into initialiser, that will be actual enum case of F type, which is then to be transformed into identical case of S type.

After we have the structure, we need the API which will return transformed case of type S to us. Not thinking of the name for a long time, we create it this way:

I promised you will see where Hashable and CaseIterable protocols come into play, so here they are.

First, we need to check if both enums have equal cases. If they don’t, we produce an error. Conceptually, ‘fatalError’ pattern for handling errors isn’t really suitable for production applications, but if the functionality is covered by tests and you are sure that potential crash isn’t going to leak into production, it is acceptable. This approach has been chosen here for the sake of simplicity, however, if you implement this mechanism in your app, you can either throw an error or use Result type, depending on your preferences.

If during the check we did not receive an error, we then iterate over all of the cases of “Second” enum to see which of them has the same hashValue as the one which was passed as an initialValue. Once such case is found, we return it to a final consumer. Force-unwrap is fully safe here, cause we know for sure that such a case exists after conducting an error check.

So, as we have everything settled, we can now use our Isomorphic structure to get rid of a switch inside getPurchasedProductType function and substitute it by the following call:

Voilà! Now if product count increases, you won’t have to care about that, cause this transformation is gonna be handled automatically, with help of isomorphic context we’ve created.

There are a few important points to keep in mind though. Firstly, you have to always be sure that cases of enums, placed into Isomorphic instance, are the same, otherwise, the error will be produced. Secondly, you can not use it on enums with different raw and associated values, cause it will influence hash values for their cases.

In this article we went through a mathematical concept of isomorphism and investigated in which cases it might be helpful in your daily development routine. There is a potential improvements wiggle room in that pattern of generic types transformation, you can be establishing this kind of approach for other cases of similar types conversion.

--

--