Blog Infos
Author
Published
Topics
Published
Learn how to code libraries that your iOS teammates will not frown upon using them. In this chapter: generics

An Android head encapsulating an apple — DALLE-2

 

My initial plan was that an article on Flows would follow the chapter about Coroutines, and then I would close the series with Generics. However, I realized that I needed first to talk about Generics. After all, FlowStateFlow, and SharedFlow are generics types. Therefore understanding the limitations around the translation of Kotlin Generics to Objective-C, and by extension Swift, will be the basis to comprehend the solutions for using Flows in Swift.

This article is part of a series, see the other articles here

Lightweight Generics in Objective-C

Objective-C did neither have nullability nor Generic types until WWDC 2015. Both were introduced with the goal of improving the interoperability between Objective-C and Swift. In the case of Generics, the focus was to provide typed collections. Prior to Lightweight Generics, collection types such as NSArrayNSSet, and NSDictionary were bridged to Swift Lists, Sets, and Dictionaries of AnyObject. Now it was possible to specify the exact contained type, so, for example, an instance of NSArray<NSString *> could be mapped to [String] (a list of String). They are called lightweight because they rely on type erasure and do not require changes to Objective-C runtime.

In this article, I will try several examples of Generics interoperability between Kotlin and Swift and analyze whether the translation maintains the nullability, variance, and bounds originally defined in the Kotlin code.

As a complement to this article, I recommend the following readings:

  • ’s Kotlin Native Interop Generics article, which provides insight into the decisions behind the implementation of the interop;

  • Kotlin/Multiplatform for iOS developers: state & future, KotlinConf 2023 lecture by

    , which presents a relatively complex example of Generics interop.

Classes

Kotlin Classes are mapped to Objective-C classes, the only type really supported by Lightweight Generics.

Nullability
// KOTLIN API CODE
class GenericClass<T>(val value: T)
class GenericClassNonNullable<T: Any>(val value: T)
// EXPORTED OBJ-C HEADER
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("GenericClass")))
@interface SharedGenericClass<T> : SharedBase
- (instancetype)initWithValue:(T _Nullable)value __attribute__((swift_name("init(value:)"))) __attribute__((objc_designated_initializer));
@property (readonly) T _Nullable value __attribute__((swift_name("value")));
@end

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("GenericClassNonNullable")))
@interface SharedGenericClassNonNullable<T> : SharedBase
- (instancetype)initWithValue:(T)value __attribute__((swift_name("init(value:)"))) __attribute__((objc_designated_initializer));
@property (readonly) T value __attribute__((swift_name("value")));
@end
// HEADER "TRANSLATED" TO SWIFT 
public class GenericClass<T> : KotlinBase where T : AnyObject {
    public init(value: T?)
    open var value: T? { get }
}

public class GenericClassNonNullable<T> : KotlinBase where T : AnyObject {
    public init(value: T)
    open var value: T { get }
}
// SWIFT CLIENT CODE
func nullabilityTest() {
    class Person {
        let firstName: String
        let lastName: String
        init(firstName fn: String, lastName ln: String) {
            firstName = fn
            lastName = ln
        }
    }
    let willSmith = Person(firstName: "Will", lastName: "Smith")
    
    let a: Person = GenericClass<Person>(value: willSmith).value!
    let b: Person = GenericClassNonNullable<Person>(value: willSmith).value
}

From the above, we see that unless you bound your generic type to a non-nullable type like Any, Kotlin we will assume the worst case and set the type as nullable since it does not have control over how it is going to be used in Swift. Your Swift developer will be forced to unwrap.

Another thing to notice is that in Swift our generic type is bound to AnyObjectAnyObject is the Kotlin equivalent of Any and Java’s ObjectAny in Swift is a broader super type, including support to not only objects of classes but instances of structs, enums, protocols, function types, and really anything else. Therefore we cannot have GenericClass<String> because in Swift, String is a struct.

Variance
// KOTLIN API CODE
class GenericClassCovariant<out T>(val value: T)
class GenericClassContravariant<in T>(val value: Int)
// EXPORTED OBJ-C HEADER
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("GenericClassCovariant")))
@interface SharedGenericClassCovariant<__covariant T> : SharedBase
- (instancetype)initWithValue:(T _Nullable)value __attribute__((swift_name("init(value:)"))) __attribute__((objc_designated_initializer));
@property (readonly) T _Nullable value __attribute__((swift_name("value")));
@end

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("GenericClassContravariant")))
@interface SharedGenericClassContravariant<__contravariant T> : SharedBase
- (instancetype)initWithValue:(int32_t)value __attribute__((swift_name("init(value:)"))) __attribute__((objc_designated_initializer));
@property (readonly) int32_t value __attribute__((swift_name("value")));
@end
// HEADER "TRANSLATED" TO SWIFT 
public class GenericClassCovariant<T> : KotlinBase where T : AnyObject {
    public init(value: T?)
    open var value: T? { get }
}

public class GenericClassContravariant<T> : KotlinBase where T : AnyObject {
    public init(value: Int32)
    open var value: Int32 { get }
}

Variance for Lightweight Generics is an interesting case. From the code above we do see that out and in in Kotlin are mapped to the __covariant and __contravariant annotations in Objective-C. In the test below we do see that the variance works in Objective-C and the expected assignments are allowed.

// OBJ-C CLIENT CODE
#import <Foundation/Foundation.h>
#import <shared/shared.h>

void objc_variant_test(void)
{
    SharedGenericClass<SharedInt *> *a = [[SharedGenericClass alloc] initWithValue: @1];
    // Warning: Incompatible pointer types initializing 'SharedGenericClass<SharedNumber *> *'
    // with an expression of type 'SharedGenericClass<SharedInt *> *'
    SharedGenericClass<SharedNumber*> *b = a;
    
    SharedGenericClassCovariant<SharedInt *> *c = [[SharedGenericClassCovariant alloc] initWithValue: @1];
    // No Warnings
    SharedGenericClassCovariant<SharedInt *> *d = c;
    
    SharedGenericClassContravariant<SharedNumber *> *f = [[SharedGenericClassContravariant alloc] initWithValue: 1];
    // No Warnings
    SharedGenericClassContravariant<SharedInt *> *g = f;
}

But, surprisingly variance is lost in translation during the bridging to Swift, and the assignments between super and sub classes become illegal. You can force-cast with as!, but type safety checks will be nonexistent.

// SWIFT CLIENT CODE
func swiftVariantTest() {
    let a: GenericClass<KotlinInt> = GenericClass(value: 1)
    //Error: Cannot assign value of type 'GenericClass<KotlinInt>' to type 'GenericClass<KotlinNumber>'
    let b: GenericClass<KotlinNumber> = a

    let c: GenericClassCovariant<KotlinInt> = GenericClassCovariant(value: 1)
    //Error: Cannot assign value of type 'GenericClassCovariant<KotlinInt>' to type 'GenericClassCovariant<KotlinNumber>'
    let d: GenericClassCovariant<KotlinNumber> = c

    let e: GenericClassContravariant<KotlinNumber> = GenericClassContravariant(value: 1)
    //Error: Cannot assign value of type 'GenericClassContravariant<KotlinNumber>' to type 'GenericClassContravariant<KotlinInt>'
    let f: GenericClassContravariant<KotlinInt> = e
}
Bounds

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Kotlin Multiplatform- From “Hello World” to the Real World

By now you’ve surely heard of Kotlin Multiplatform, and maybe tried it out in a demo. Maybe you’ve even integrated some shared code into a production app.
Watch Video

Kotlin Multiplatform- From “Hello World” to the Real World

Russell Wolf
Kotlin Multiplatform
Touchlab

Kotlin Multiplatform- From “Hello World” to the Real World

Russell Wolf
Kotlin Multiplatform ...
Touchlab

Kotlin Multiplatform- From “Hello World” to the Real World

Russell Wolf
Kotlin Multiplatform
Touchlab

Jobs

// KOTLIN API CODE
class GenericClassBoundNumber<T: Number> (val value: T)
class GenericClassBoundComparable< T: Comparable<T>>(val value: T)
// EXPORTED OBJ-C HEADER
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("GenericClassBoundNumber")))
@interface SharedGenericClassBoundNumber<T> : SharedBase
- (instancetype)initWithValue:(T)value __attribute__((swift_name("init(value:)"))) __attribute__((objc_designated_initializer));
@property (readonly) T value __attribute__((swift_name("value")));
@end

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("GenericClassBoundComparable")))
@interface SharedGenericClassBoundComparable<T> : SharedBase
- (instancetype)initWithValue:(T)value __attribute__((swift_name("init(value:)"))) __attribute__((objc_designated_initializer));
@property (readonly) T value __attribute__((swift_name("value")));
@end
// HEADER "TRANSLATED" TO SWIFT 
public class GenericClassBoundNumber<T> : KotlinBase where T : AnyObject {
    public init(value: T)
    open var value: T { get }
}

public class GenericClassBoundComparable<T> : KotlinBase where T : AnyObject {
    public init(value: T)
    open var value: T { get }
}

As we see from the code above, the bounds are ignored. That allows us to use the defined classes with Person, a type that either does not subclass Number or conforms to (implements) Comparable. As a result of that, we have a big potential for bugs as it is possible to use the generic class with types that it does not expect.

// SWIFT CLIENT CODE
func boundTest() {
    class Person {
        let firstName: String
        let lastName: String
        init(firstName fn: String, lastName ln: String) {
            firstName = fn
            lastName = ln
        }
    }
    let willSmith = Person(firstName: "Will", lastName: "Smith")
    
    let a = GenericClassBoundComparable<Person>(value: willSmith)
    let b = GenericClassBoundNumber<Person>(value: willSmith)
}
Interfaces

Kotlin Interfaces are mapped to Objective-C Protocols, which are not supported by Lightweight Generics. In fact, Swift Protocols do not rely on Type Parameters but on Associated Types.

Nullability
// KOTLIN API CODE
interface GenericInterface<T> { val value: T }
interface GenericInterfaceNonNullable<T: Any> { val value: T }
// EXPORTED OBJ-C HEADER
__attribute__((swift_name("GenericInterface")))
@protocol SharedGenericInterface
@required
@property (readonly) id _Nullable value __attribute__((swift_name("value")));
@end

__attribute__((swift_name("GenericInterfaceNonNullable")))
@protocol SharedGenericInterfaceNonNullable
@required
@property (readonly) id value __attribute__((swift_name("value")));
@end
// HEADER "TRANSLATED" TO SWIFT 
public protocol GenericInterface {
    var value: Any? { get }
}

public protocol GenericInterfaceNonNullable {
    var value: Any { get }
}

Although the generic types are lost, binding to a non-nullable type will, like in the class case, translate to Any, a non-null type.

Variance and Bounds
// KOTLIN API CODE
interface GenericInterfaceCovariant<in T> { val value: Int }
interface GenericInterfaceContraVariant<out T> { val value: T }
interface GenericInterfaceBoundNumber<T: Number> { val value: T }
interface GenericInterfaceBoundComparable<T: Comparable<T>> { val value: T }
// EXPORTED OBJ-C HEADER
__attribute__((swift_name("GenericInterfaceCovariant")))
@protocol SharedGenericInterfaceCovariant
@required
@property (readonly) int32_t value_ __attribute__((swift_name("value_")));
@end

__attribute__((swift_name("GenericInterfaceContraVariant")))
@protocol SharedGenericInterfaceContraVariant
@required
@property (readonly) id _Nullable value __attribute__((swift_name("value")));
@end

__attribute__((swift_name("GenericInterfaceBoundNumber")))
@protocol SharedGenericInterfaceBoundNumber
@required
@property (readonly) id value __attribute__((swift_name("value")));
@end

__attribute__((swift_name("GenericInterfaceBoundComparable")))
@protocol SharedGenericInterfaceBoundComparable
@required
@property (readonly) id value __attribute__((swift_name("value")));
@end
// HEADER "TRANSLATED" TO SWIFT 
public protocol GenericInterfaceCovariant {
    var value_: Int32 { get }
}

public protocol GenericInterfaceContraVariant {
    var value: Any? { get }
}

public protocol GenericInterfaceBoundNumber {
    var value: Any { get }
}

public protocol GenericInterfaceBoundComparable {
    var value: Any { get }
}

As said before, protocols do not support generics. That being so, any information about the type parameter is lost. Note the use of id in Objective-C which is bridged to Any. Both denote a reference to any type. As a result of that, while in Kotlin an instance of GenericInterface<String> can not be assigned to GenericInterface<Int>, because String and Int are incompatible types, it will be possible in Swift because they will both be seen as an instance of Any.

Note that even if you bound the type parameter, the type in Swift will still be Any, and not the bound type as it would normally happen after type erasure.

Functions and Methods

Both are not supported by Lightweight Generics, but there’s a few interesting things to note:

  • Again, biding to a non-nullable type will ensure the type is not null in Objective-C and Swift.
  • Non-bound type arguments are mapped to Any. Because of that, there is no way to enforce type constraints. For instance, in someFunctionTwoTypeArguments, both valueOfTypeT and anotherValueOfTypeT are bound to type argument T. But in Swift, it will be possible to use different types for them.
  • When the type argument is bound to an exported type, Kotlin native compiler will fortunately do type erasure, which will bring some type safety.
// KOTLIN API CODE
interface MyInterface
open class MyClass
open class Container<out T>(val value: T)

class FunctionGenerics {
    fun <T> someFunction(value: T): T = value
    fun <T> someFunctionNonNull(value: T): T where T: Any = value
    fun <T> someFunctionReturnsClass(value: T): Container<T> = Container(value)
    fun <T> someFunctionReturnsInterface(): Sequence<T> = sequenceOf()
    fun <T, U> someFunctionTwoTypeArguments(valueOfTypeT: T, anotherValueOfTypeT: T, valueOfTypeU: U) = Unit
    fun <T : MyInterface> someFunctionBoundByInterface(a: T): T = a
    fun <T : MyClass> someFunctionBoundByClass(a: T): T = a
    fun <T : Comparable<T>> someFunctionBoundByGenericInterface(a: T, b: T): Int = a.compareTo(b)
    fun <T : Container<T>> someFunctionBoundByGenericClass(a: T): T = a
}
// EXPORTED OBJ-C HEADER
__attribute__((swift_name("MyInterface")))
@protocol SharedMyInterface
@required
@end

__attribute__((swift_name("MyClass")))
@interface SharedMyClass : SharedBase
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
@end

__attribute__((swift_name("Container")))
@interface SharedContainer<__covariant T> : SharedBase
- (instancetype)initWithValue:(T _Nullable)value __attribute__((swift_name("init(value:)"))) __attribute__((objc_designated_initializer));
@property (readonly) T _Nullable value __attribute__((swift_name("value")));
@end

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("FunctionGenerics")))
@interface SharedFunctionGenerics : SharedBase
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
- (id _Nullable)someFunctionValue:(id _Nullable)value __attribute__((swift_name("someFunction(value:)")));
- (SharedMyClass *)someFunctionBoundByClassA:(SharedMyClass *)a __attribute__((swift_name("someFunctionBoundByClass(a:)")));
- (SharedContainer *)someFunctionBoundByGenericClassA:(SharedContainer *)a __attribute__((swift_name("someFunctionBoundByGenericClass(a:)")));
- (int32_t)someFunctionBoundByGenericInterfaceA:(id)a b:(id)b __attribute__((swift_name("someFunctionBoundByGenericInterface(a:b:)")));
- (id<SharedMyInterface>)someFunctionBoundByInterfaceA:(id<SharedMyInterface>)a __attribute__((swift_name("someFunctionBoundByInterface(a:)")));
- (id)someFunctionNonNullValue:(id)value __attribute__((swift_name("someFunctionNonNull(value:)")));
- (SharedContainer<id> *)someFunctionReturnsClassValue:(id _Nullable)value __attribute__((swift_name("someFunctionReturnsClass(value:)")));
- (id<SharedKotlinSequence>)someFunctionReturnsInterface __attribute__((swift_name("someFunctionReturnsInterface()")));
- (void)someFunctionTwoTypeArgumentsValueOfTypeT:(id _Nullable)valueOfTypeT anotherValueOfTypeT:(id _Nullable)anotherValueOfTypeT valueOfTypeU:(id _Nullable)valueOfTypeU __attribute__((swift_name("someFunctionTwoTypeArguments(valueOfTypeT:anotherValueOfTypeT:valueOfTypeU:)")));
@end
// HEADER "TRANSLATED" TO SWIFT 
public protocol MyInterface {
}

open class MyClass : KotlinBase {
    public init()
}

open class Container<T> : KotlinBase where T : AnyObject {
    public init(value: T?)
    open var value: T? { get }
}

public class FunctionGenerics : KotlinBase {
    public init()
    
    open func someFunction(value: Any?) -> Any?
    open func someFunctionNonNull(value: Any) -> Any
    open func someFunctionReturnsClass(value: Any?) -> Container<AnyObject>
    open func someFunctionReturnsInterface() -> KotlinSequence
    open func someFunctionTwoTypeArguments(valueOfTypeT: Any?, anotherValueOfTypeT: Any?, valueOfTypeU: Any?)
    open func someFunctionBoundByInterface(a: MyInterface) -> MyInterface
    open func someFunctionBoundByClass(a: MyClass) -> MyClass
    open func someFunctionBoundByGenericInterface(a: Any, b: Any) -> Int32
    open func someFunctionBoundByGenericClass(a: Container<AnyObject>) -> Container<AnyObject>
}

This time we saw how Generics are supported by Kotlin Multiplatform. In the next and final chapter, we will combine the lessons learned in this article and the previous one to tackle Flows.

References

 

 

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
I love Swift enums, even though I am a Kotlin developer. And iOS devs…
READ MORE
blog
After successfully implementing the basic Kotlin multiplatform app in our last blog, we will…
READ MORE
blog
Kotlin Multiplatform despite all of its benefits sometimes has its own challenges. One of…
READ MORE
blog
I develop a small multi-platform(android, desktop) project for finding cars at an auction using…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu