Hi, I have a question, why is the Kotlin SDK defin...
# getting-started
a
Hi, I have a question, why is the Kotlin SDK defining extensions methods of classes that haven't any parent or not implementing anything ? Is that better than defining the method inside the class ? What are the benefits so ?
r
Could you mention some specific examples?
d
What SDK?
You mean the standard library?
a
Yes
r
@elizarov wrote an article on that a while back: https://elizarov.medium.com/extension-oriented-design-13f4f27deaee
s
Consider the case of a String class. Here you can’t your own method inside String class. But kotlin extension method, you can do it
a
Yes but for personal classes, what is the case ?
Thanks @Ruckus I will read that !
👍 1
r
It's short, but very insightful. Definitely made me rethink some of my designs.
r
For me it has four important use cases: 1) Adding methods to types you don’t control. Many Kotlin stdlib extension methods decorate java stdlib types. 2) Adding methods that it would be inappropriate to add to the original type, perhaps because they are far too specific to a particular context - otherwise a type could end up having thousands of methods. You can group extension functions for a particular context in a file to reduce the cognitive load of understanding the “basic” class. 3) Adding methods that cross domains Perhaps you want to have this:
Copy code
val rex: Dog = TODO()
val bmw: Car = TODO()

rex.getInto(bmw)
Obviously the domain of
Dog
shouldn’t know about the domain of
Car
, and vice versa. But it’s quite nice to call it that way, rather than
putDogIntoCar(rex, bmw)
or something. 4) Adding methods which would otherwise introduce circular dependencies. For instance
Any
having a method
toString()
while
String : Any
introduces a circular dependency between
Any
and
String
. It’s there because Java defined it that way on
java.lang.Object
, but it would be much better if there were an extension method
Any.toString()
- then there’s no circular dependency. Of course you could just have plain old functions, but extension functions: 1) Allow you to maintain “subject verb object” ordering 2) Allow using null dereferencing -
fun Foo.bar()
can be called
foo?.bar()
if
foo
is nullable, which is much nicer than
foo?.let { bar(it) }
3) Allow discovery in an IDE - it’s nice to be able to go
foo.
, hit control space and see the extension methods as possible things you want to do.
r
@Rob Elliot I can't say I agree with your 4th point. Extension functions are, by design, statically resolved, and thus not polymorphic. If
toString
was an extension instead of a member, it couldn't be overridden in sub-classes. Also, I wouldn't consider a parent method returning a sub-type a circular dependency. That's just narrowing and is very common and useful.
r
It is a circular dependency. You cannot compile Any without compiling String, and you cannot compile String without compiling Any. It’s unquestionably a circular dependency.
r
Sure you can. You don't need to compile a class to use it as a return type. If you did need to, effectively no (type safe) code could compile. Classes couldn't even return themselves as that would require them being compiled before they can be compiled.
r
You can’t refer to a type without the compiler knowing it exists. So you cannot compile either in isolation.
r
Oh sure, but they're not in isolation. They're in the same module.
"What if they weren't in the same module" isn't really a useful question as that's kind of the whole point of compilation units (e.g. modules) in the first place...
a
Thanks a lot all, it's more clear for me now !
👍 1
r
I don’t think we’re going to reach agreement on the circular dependency question. As far as I can see them being in the same module doesn’t stop it being a circular dependency between the classes, it just makes it a circular dependency that the compiler & runtime can cope with. You’re quite right & I was wrong about the extension method - of course it wouldn’t be a get out of jail card to allow
toString()
on Any - the static compilation means the polymorphic version would be ignored.
(In terms of the polymorphism, I think you should need to implement an interface
interface Show { fun toString(): String }
. String interpolation should require an explicit call to
toString
, too - so often you see untested code, particularly logging, where an object which hasn’t got a useful toString is interpolated. I’m with Haskell on this. I also like the way that implementing an interface happens after the declaration of the type in Haskell, so you can effectively make
Any
implement
Show
after the declarations of
Any
and
Show
, avoiding the circularity.)
I think 4) still holds as a reason for extension methods, but Any was a poor example. And 4) is probably therefore a specific example of 3), in the case where the domains are in different modules, and adding the method as a member of a class in one module would introduce a circular dependency.
r
That's fair. I guess my disagreement was with the example and not the concept itself. Whether
toString
should be on
Any
vs in an interface is an entirely separate question though. And Haskell (as well as Rust since that's another common alternative brought up in such discussions) isn't a great example since it's not object oriented, and design decisions don't often carry across paradigms well (in my experience at least). A lot of Haskell, Rust, C, etc. implementations work well within their context, but don't really fit as well once you have to take in to account how it will interact with OOP.
👍 1
@Ayfri Also, sorry to hijack your question with a tangential discussion. I hope you don't mind 🙂
a
No problem it's interesting !
👍 2
j
@Ayfri To add to the use case Rob mentioned regarding your question
Yes but for personal classes, what is the case ?
As Rob mentioned, sometimes we want to extend functionality that is only relevant to part of the code, and not the class itself. And using extension functions has the advantage over utility functions in that calling a function on the class as a receiver can feel more “natural” in its use than passing it into a function. Let’s say in one part of my code I need to validate a person. Validation is specific to the place the Person object is being used in. For example here validation is they are 21 or older. In another part of the code a “valid” person might be 16 and older and have a driver’s license. So I would not add the function to the Person class, but to where I need it. I can write either:
Copy code
private fun validate(person: Person): Boolean = person.age >= 21
private fun Person.isValid(): Boolean = this.age >= 21
When I use those, the extension function just presents itself better in the code. It reads more naturally.
Copy code
if (validate(person)) { ... }
if (person.isValid()) { ... }
Also, with extension functions, I can create a fluent API to use rather than calling a series of utility functions:
Copy code
internal fun Document.normalize(): Document
{
    // normalization code
    return this
}

internal fun Document.convert(): Document
{
    // conversion code
    return this
}

internal fun Document.process(): Result
{
    // process code
    return Result()
}
And when I use it, it results in very readable succinct code:
Copy code
val document = ...
val result = document.normalize().convert().process()
If I had to do that with utility functions, I’d have something like:
Copy code
var document = ...
document = normalize(document)
document = convert(document)
val result = process(document)
Not only is that more lines of code, it doesn’t read nearly as well. As mentioned in the book Kotlin in Action, a leading design goal of Kotlin is to make it as readable as possible. That’s because as developers, we actually spend more time reading code, then writing it. Moreover, in the first use with the extension functions, my
document
is a
val
. But when I use the utility functions, I had to make it a
var
We want to favor immutables as they result in less bugs, and are better for functional programming.
a
Thanks you for your response I understand when to use them and why!