Hey all - I'm trying to understand extension funct...
# getting-started
m
Hey all - I'm trying to understand extension functions. I see the value in adding extension methods for other library authors code. However I'm trying to figure out the reason for doing so in your own code. I'm sure there is a reason because I'm seeing it a lot within the Ktor code base. I'm curious since the Ktor devs have access to their own code, why they didn't just add them as regular methods on a class. Would some one be able to shed some light on this? Thank you!
e
I don't have anything ktor-specific knowledge, but a couple general cases: • with
inline fun
for higher-order functions for
reified
types, only static and extension functions work because they must be resolved statically • for effectively
final
functions, they don't go through virtual function resolution anyway, so pick whichever organizes the code best
r
A good read from a while back by @elizarov: https://elizarov.medium.com/extension-oriented-design-13f4f27deaee
☝️ 1
r
Other reasons: • There are generic methods whose type signatures can only be done in extension functions. e.g. https://github.com/Mahoney-playground/goos/blob/bf8cce396b751ca1b983001c35c38776adc0821d/gradle/build-plugins/extract-plugin/src/either/Either.kt#L59-L68 • It makes for much nicer null handling.
val x: T? = foo?.bar()
is a lot better than
val x: T? = foo?.run { bar(it) }
, or
val x: T? = if (foo != null) bar(foo) else null
or making
bar
accept a nullable parameter. This is particularly true with chaining -
x?.foo()?.bar()
where
foo
and
bar
are both extension functions on a non-null receiver can save you a lot of null types. • Even within a codebase there are places where subject-verb-object is a desirable word order (or subject-verb) but where the verb and/or the object are not appropriate to the core domain of the subject. e.g. you might have a
data class
representing an entity. Your data class should not know how to persist itself, but it's quite nice to be able to call
entity.save()
.
save()
can then be an extension function on
Entity
which has access to your repository. Of course you could just call
save(entity)
but it's surprising how much more legible it can be changing the order, and it can reduce your nesting quite a bit too.
A favourite of mine is turning single arg constructors / converters into extension functions. I'd much rather read
val status: HttpStatus = statusStr.toInt().toHttpStatus()
than
val status: HttpStatus = HttpStatus(Integer.parseInt(statusStr))
. The former lets me read left to right - I start with a String, turn it into an Int, turn that into an HttpStatus. The latter I have to hunt to the end for the input and expand out through two layers of nesting.
And I can make it accept a null or invalid string trivially:
val status: HttpStatus? = statusStr?.toIntOrNull()?.toHttpStatus()
which is a lot easier than handling null without the extension functions.
m
Thanks for the thoughts, all! Really cool to see that extension function implications are much more than just attaching methods on classes you don't own.