At Avo we work on code generation based on tracking plans.
Recently we added a new interface for calling events - before we were generating a method for each event in the tracking plan, i. e. there were methods Avo.userSubscribed(userType, subscriptionType)
and Avo.commentAdded(authorId, mentionedUserIds)
.
With the new interface we create a type for each event, i.e. UserSubscribed
with userType
and subscriptionType
as fields and CommentAdded
with authorId
and mentionedUserIds
fields, and a new method called track
that expects instances of those types, like this Avo.track(UserSubscribed(userType, subscriptionType))
Why?
This change makes Avo more flexible to use in large codebases, for example you can build the UserSubscribed
events in different parts of you app gradually and pass it around.
You can also build a simple test implementation of the new track
interface that does not require mocking.
What are the challenges?
Firstly, we don't want users of the generated code to pass unexpected things to the track method. Only the events defined in the tracking plan are expected.
Secondly, we want the events to be meaningfully comparable.
Solution
Let's see what both languages can offer us to solve this little challenge.
1. Defining the event types.
The ways of choice to define a limited set of types are:
In Kotlin - sealed classes. Sealed classes are abstract classes and you can define their child classes only in the same file. User's won't be able to create their implementation of our sealed class, exactly as we want. This also makes the compiler know the exact set of descendants of our sealed class.
sealed class AvoEvent {
data class UserSubscribed(val userType: String, val subscriptionType: String): AvoEvent()
data class CommentAdded(val authorId: String, val mentionedUserIds: List<String>): AvoEvent()
}
In Swift - enums. Enums in Swift are more powerful than in most other similar languages. Each enum case can have any number of variously typed parameters, which is nice for our case. User's are not able to add cases outside of the enum.
public enum AvoEvent: Equatable {
case userSubscribed(userType: String, subscriptionType: String);
case commentAdded(authorId: String, mentionedUserIds: [String]);
}
- Here things are quite even, since classes are a bit more flexible (more on it in the next section) and also in Kotlin you use common
class
interface for most things, while in Swift you have to use different entities -classes
/structs
andenums
. On the other hand Swift code looks cleaner.
2. Making the types equitable
In Kotlin we use data classes. Every data class automatically gets equals
method implementation without the need to write any code based on the primary constructor values.
In Swift we set our enum to conform the Equitable
protocol. In modern Swift if all the used types of a thing we add Equitable
protocol to are Equitable
you don't need to write any implementation. This is similar for both languages.
Everything is great until we get a enum member with an Any
type parameter.
The problem in Swift is that the Any
type is not Equitable
. And once it appears you have to implement the compare method (==
) manually. Moreover, once you have the compare method you have to manually implement comparison of each enum case. (In Kotlin for example we can implement the equality method on a separate single child class of our sealed class, that's the advantage of sealed classes over enums I mentioned in the previous section).
In Kotlin Any
type is equitable out of the box. Since it's has equals
method it is designed to be.
- This point is won by Kotlin.
3. Picking what to do based on the provided parameter
Here everything is quite simple:
In Kotlin we use when
statement.
In Swift we use switch
statement.
The good thing about the switch
statements in Swift is that they are required to be exhaustive. This makes perfect sense in a statically typed languages - I want to be sure that if I add a new case to my enum and not add it to the switch
I'm alarmed by the compiler.
Unfortunately it is not the case in Kotlin. When you use when
as a statement, not assigning it's result to a variable or using it in some other way, which is a default way for those who come from Java and many other C-like languages, it is not required to be exhaustive. You can use when
as expression and then it becomes exhaustive
, but that's not enough. Here is a bit more on that topic.
- In this part I give the point to Swift.
Results
All in all, it is a draw in out little face off, Kotlin is a winner on the data structure design side and Swift wins in the operator design.
Top comments (0)