1. 程式人生 > >Kotlin: The Good, The Bad, and The Ugly

Kotlin: The Good, The Bad, and The Ugly

The Ugly

And finally, here are two design decisions that the Kotlin team made that I strongly disagree with, and that I don’t expect to change in the future.

SAM conversion and Unit returning lambdas

This one is a really baffling design decision.

One of the best features of Kotlin is the way it embraces lambda functions. If you have a Java function that takes a SAM interface as a parameter (an interface with a Single Abstract Method):

public void registerCallback(View.OnClickListener r)

You can call it by passing a plain lambda from either Kotlin or Java:

// Java
registerCallback(() -> { /** do stuff */ })
//Kotlin
registerCallback { /** do stuff */ }

This is great. But trying to define a similar method in Kotlin is inexplicably harder. The direct translation is called the same from Java, but requires an explicit type when called from Kotlin:

fun registerCallback(r: View.OnClickListener)
// Kotlin. Note that parenthesis are required now.
registerCallback(View.OnClickListener { /** do stuff */ })

That’s annoying to have to type out, especially if you convert some Java code to Kotlin and find out that it breaks existing Kotlin code.

The idiomatic way to define that function in Kotlin would be with a function type:

fun registerCallback(r: () -> Unit)

Which allows the nice function call syntax in Kotlin, but since all Kotlin functions are required to return a value, this makes calling the function from Java much worse. You have to explicitly return Unit from Java lambdas, so expression lambdas are no longer possible:

registerCallback(() -> {
/** do stuff */
return Unit.INSTANCE;
})

If you’re writing a library in Kotlin, there isn’t any good way to write a method with a function parameter that is ideal to call from both Java and Kotlin. I try to work around this in my FlexAdapter library by providing overloads for each method with a function parameter that take either a SAM interface or a Kotlin function type. That lets you have a nice call syntax from both languages, but makes the library API less concise.

Hopefully the Kotlin designers change their mind and allow SAM conversions for functions defined in Kotlin in the future, but I’m not optimistic.

Closed by default

Every downside to Kotlin I’ve talked about so far are mostly small syntax details that are not quite as clean I’d like, but aren’t a big deal overall. But there’s one design decision that is going to cause a huge amount of pain in the future: All classes and functions in Kotlin are closed by default. It’s a design decision pushed by Effective Java, and it might sound nice in theory, but it’s an obviously bad choice to anyone who’s had to use a buggy or incomplete third-party library.

Make all of your leaf classes final. After all, you’re done with the project — certainly no one else could possibly improve on your work by extending your classes. And it might even be a security flaw — after all, isn’t java.lang.String final for just this reason? If other coders in your project complain, tell them about the execution speed improvement you’re getting. — Roedy Green, How to Write Unmaintainable Code

“Best practices say that you should not allow these hacks anyway”

The arguments for closed inheritance are mostly centered around the “Fragile Base Class Problem”, which is the idea that, if you allow someone to subclass your library code, they could change the way it works, potentially causing bugs. While that’s a possibility, there are lots of way to use a library incorrectly that will cause bugs. If you override some functionality in a class, it obviously your responsibility if you break something.

I use the word “obviously”, because overriding functionality is the way to use a library that most explicitly places responsibility on the user. I tutored Computer Science students for years, and while they managed to make nearly every mistake you can imagine, they were never surprised when they broke something by overriding a method. There are much more subtle ways to break a library you use, like passing a value with the correct type but wrong units, or forgetting to call a required method.

I appreciate the approach of writing code that has fewer places that can break, and making classes final might seem to do that. But it’s a certainty that libraries will be incomplete or incorrect, and you will eventually need to use one of those libraries. Modifying the behavior of a closed class is going to require much worse hacks that are more likely to result in bugs than if you could just override one or two of that class’s methods.

But you don’t even have to take my word for it. Here’s a real world example that might have impacted you personally if you’re an Android dev:

Where did the memory leaks come from? In order to implement VectorDrawable inflation, the support library authors needed to update the way Context.getDrawable is implemented. But that function is final, so they had to make every View create a new copy of a Resources wrapper that could handle VectorDrawables. Besides being a large amount of work, this caused the various wrapped Resources to become out of sync, and to use a large amount of memory due to the duplication. If that function wasn’t final, that mess wouldn’t have happened.

“People successfully use other languages (C++, C#) that have similar approach”

People also successfully use languages like Python that allow anything at all to be changed at any time. Python has “non-public” methods like _asdict that are documented to by implementation details. It also has name mangled functions like __intern that are harder to accidentally call. You can freely monkey-patch or override any of those functions whenever you want, and Python won’t complain.

In five years of full time Python development, I can’t think of a single time when someone broke my code by overriding a function. I can think of many instances where I’ve altered a non-public function in a safe, correct way in much less time than it would have taken to implement the same functionality if Python prevent me from doing so.

I’m not advocating blindly altering implementation detail of every class you come in contact with, but there’s no reason to make that impossible if it becomes necessary. A common saying in the Python community is that “We’re all consenting adults here.” If you need to make a change to one of my classes, you should be able to.

“If people really want to hack, there still are ways: you can always write your hack in Java and call it from Kotlin (see Java Interop), and Aspect frameworks always work for these purposes”

This is of course a ridiculous argument. You still can’t override closed Kotlin methods in Java without unacceptable use of reflection, so this doesn’t hold any weight.

Not being able to extend from a library means that it becomes very difficult to add features or fix bugs. And in the real world, more libraries than not will need hacking. That’s simply reality, and is never going to change. A library author is never going to be able to predict every possible use case that users will have. Making all of your classes final only prevents users from easily implementing the features you can’t. This was a surprisingly dogmatic choice given how practical the Kotlin designers were in the rest of the language.

If you write a Kotlin library, please make all of your public functions open. You’ll make life easier on your users in the future.

Conclusion

Kotlin is overall a great language. It is much less verbose than Java, and has an excellent standard library that removes the need to use a lot of the libraries that make Java life bearable. Converting an app from Java to Kotlin is made much easier thanks to automated syntax conversion, and the result is almost always an improvement. If you’re an Android developer, you owe it to yourself to give it a try.

At Keepsafe, all new Android development is in Kotlin, and legacy Java code is steadily being converted to Kotlin as we make changes to it.

Interested in working with us? Have a look at our job openings.