Protocol Oriented Programming, Part 3

Final article on protocol-oriented programming.







In this part, we will look at how variables of a generic type are stored and copied, and how the dispatch method works with them.







Unshared version



protocol Drawable { func draw() } func drawACopy(local: Drawable) { local.draw() } let line = Line() drawACopy(line) let point = Point() drawACopy(point)
      
      





Very simple code. drawACopy



takes a parameter of type Drawable and calls its draw method - that’s all.







Generalized version



Let's look at the generalized version of the code above:







 func drawACopy<T: Drawable>(local: T) { local.draw() } ...
      
      





Nothing seems to have changed. We can still just call the drawACopy



function, as its drawACopy



version, and nothing more, but the most interesting as usual under the hood.

Generalized code has two important features:







  1. static polymorphism (also known as parametric)
  2. a specific and unique type in the context of the call (a generic type T is defined at compile time)


Consider this as an example:







 func foo<T: Drawable>(local: T) { bar(local) } func bar<T: Drawable>(local: T) { ... } let point = Point(...) foo(point)
      
      





The most interesting part begins when we call the foo



function. The compiler knows exactly the type of the point



variable - it's just Point. Moreover, the T: Drawable type in the foo



function can be freely inferred by the compiler from the moment we pass a variable of the known Point type to this function: T = Point. All types are known at compile time and the compiler can perform all its wonderful optimizations - the most important thing is to inline the foo



call.







 This: ```swift let point = Point(...) foo<T = Point>(point) Becomes this: ```swift bar<T = Point>(point)
      
      





The compiler simply embeds the foo



call with its implementation and displays the generic type of T: Drawable bar, too. In other words, the compiler first embeds a call to the foo method with type T = Point, then it embeds the result of the previous embedding - the bar method with type T = Point.







Implementation of generic methods



 func drawACopy<T: Drawable>(local: T) { local.draw() } drawACopy(Point(...))
      
      





Internally, drawACopy



Swift uses a protocol-method table (which contains all implementations of the T method) and a life cycle table (which contains all the life cycle methods for the T instance). In pseudo code, it looks like this:







 func drawACopy<T: Drawable>(local: T, pwt: T.PWT, vwt: T.VWT) {...} drawACopy(Point(...), Point.pwt, Point.vwt)
      
      





VWT and PWT are associated types (associatedtype) in T - as type aliases (typealias), only better. Point.pwt and Point.vwt are static properties.







Since in our example T is Point, T is well defined, therefore, the creation of a container is not required. In the previous drawACopy



version of drawACopy



(local: Drawable), the creation of an existential container was carried out as necessary - we examined this in the second part of the article.







A lifecycle table is required in functions due to the creation of an argument. As we know, arguments in Swift are passed through values, not through links, therefore, they must be copied, and the copy method for this argument belongs to the lifecycle table like this argument. There are also other lifecycle methods there: allocate, destruct and deallocated.







A life cycle table is required in generic functions due to the use of methods for generic code parameters.







Generalized or non-generalized?



Is it true that using generic types makes code execution faster than using protocol types only? Is the generic function func foo<T: Drawable>(arg: T)



faster than its protocol-like counterpart fun foo(arg: Drawable)



?







We noticed that generalized code gives a more static form of polymorphism. It also includes compiler optimizations called "Generic Code Specialization." Let's get a look:







Again we have the same code:







 func drawACopy<T: Drawable>(local: T) { local.draw() } drawACopy(Point(...)) drawACopt(Line(...))
      
      





Specialization of a generic function creates a copy with specialized generic types of this function. For example, if we call drawACopy



with a variable of type Point, then the compiler will create a specialized version of this function - drawACopyOfPoint



(local: Point), and we get:







 func drawACopyOfPoint(local: Point) { local.draw() } func drawACopyOfLine(local: Line) { local.draw() } drawACopy(Point(...)) drawACopt(Line(...))
      
      





What can be reduced by crude compiler optimization before this:







 Point(...).draw() Line(...).draw()
      
      





All these tricks are available because generic functions can only be called if all generic types are defined - in the drawACopy



method drawACopy



generic type (T) is well defined.







Generic Stored Properties



Consider a simple struct pair:







 struct Pair { let fst: Drawable let snd: Drawable } let pair = Pair(fst: Line(...), snd: Line(...))
      
      





When we use this in this way, we get 2 allocations on the heap (the exact memory conditions in this scenario were described in the second part), but we can avoid this with the help of a generalized code.







The generic version of Pair looks like this:







 struct Pair<T: Drawable> { let fst: T let snd: T }
      
      





From the moment the type T is defined in the generalized version, the property types fst



and snd



same and are also defined. Since the type is defined, the compiler can allocate a specialized amount of memory for these two properties - fst



and snd



.







In more detail about the specialized amount of memory:







when we are working with a fst



version of Pair



, the property types fst



and snd



are Drawable. Any type can correspond to Drawable, even if it takes 10 KB of memory. That is, Swift will not be able to draw a conclusion about the size of this type and will use a universal memory location, for example, an existential container. Any type can be stored in this container. In the case of generic code, the type is well recognized, the actual size of the properties is also recognizable, and Swift can create a specialized memory location. For example (generalized version):







 let pair = Pair(Point(...), Point(...))
      
      





Type T is now Point. Point takes N bytes of memory and in Pair we get two of them. Swift will allocate 2 * N amount of memory and put pair



there.







So, with the generic version of Pair, we get rid of unnecessary allocations on the heap, because types are easily recognizable and can be located specifically - without the need to create universal memory templates, since everything is known.







Conclusion



1. Specialized Generic Code - Value Types



has the best execution speed, since:









2. Specialized generalized code - reference types



It has an average execution speed, since:









3. Non-specialized generalized code - small values





4. Non-specialized generalized code - large values





This material does not mean that classes are bad, structures are good, and structures in combination with generalized code are the best. We want to say that as a programmer, you have the responsibility of choosing a tool for your tasks. Classes are really good when you need to keep large values ​​and that there is a semantics of links. Structures are best for small values ​​and when you need their semantics. Protocols are best suited to generic code and structures, and so on. All tools are specific to the task that you are solving, and have positive and negative sides.







And also do not pay for dynamism when you do not need it . Find the right abstraction with the least runtime requirements.









Use indirect storage to work with large values.







And do not forget - it is your responsibility to choose the right tool.

Thank you for your attention to this topic. We hope that these articles have helped you and were interesting.







Good luck








All Articles