Kotlin: Idiomatic usage of extension functions - putting extension functions next to the class it extends
Asked Answered
A

2

5

I see some usages of Extension functions in Kotlin I don't personally think that makes sense, but it seems that there are some guidelines that "apparently" support it (a matter of interpretation).

Specifically: defining an extension function outside a class (but in the same file):

data class AddressDTO(val state: State,
                      val zipCode: String,
                      val city: String,
                      val streetAddress: String
)

fun AddressDTO.asXyzFormat() = "${streetAddress}\n${city}\n${state.name} $zipCode"

Where the asXyzFormat() is widely used, and cannot be defined as private/internal (but also for the cases it may be).

In my common sense, if you own the code (AddressDTO) and the usage is not local to some class / module (hence behing private/internal) - there is no reason to define an extension function - just define it as a member function of that class.

  • Edge case: if you want to avoid serialization of the function starting with get - annotate the class to get the desired behavior (e.g. @JsonIgnore on the function). This IMHO still doesn't justify an extension function.

The counter-response I got to this is that the approach of having an extension function of this fashion is supported by the Official Kotlin Coding Conventions. Specifically:

Use extension functions liberally. Every time you have a function that works primarily on an object, consider making it an extension function accepting that object as a receiver. Source

And:

In particular, when defining extension functions for a class which are relevant for all clients of this class, put them in the same file where the class itself is defined. When defining extension functions that make sense only for a specific client, put them next to the code of that client. Do not create files just to hold "all extensions of Foo". Source

I'll appreciate any commonly accepted source/reference explaining why it makes more sense to move the function to be a member of the class and/or pragmatic arguments support this separation.

Aloisia answered 10/2, 2021 at 13:3 Comment(0)
P
7

That quote about using extension functions liberally, I'm pretty sure means use them liberally as opposed to top level non-extension functions (not as opposed to making it a member function). It's saying that if a top-level function conceptually works on a target object, prefer the extension function form.

I've searched before for the answer to why you might choose to make a function an extension function instead of a member function when working on a class you own the source code for, and have never found a canonical answer from JetBrains. Here are some reasons I think you might, but some are highly subject to opinion.

  • Sometimes you want a function that operates on a class with a specific generic type. Think of List<Int>.sum(), which is only available to a subset of Lists, but not a subtype of List.
  • Interfaces can be thought of as contracts. Functions that do something to an interface may make more sense conceptually since they are not part of the contract. I think this is the rationale for most of the standard library extension functions for Iterable and Sequence. A similar rationale might apply to a data class, if you think of a data class almost like a passive struct.
  • Extension functions afford the possibility of allowing users to pseudo-override them, but forcing them to do it in an independent way. Suppose your asXyzFormat() were an open member function. In some other module, you receive AddressDTO instances and want to get the XYZ format of them, exactly in the format you expect. But the AddressDTO you receive might have overridden asXyzFormat() and provide you something unexpected, so now you can't trust the function. If you use an extension function, than you allow users to replace asXyzFormat() in their own packages with something applicable to that space, but you can always trust the function asXyzFormat() in the source package.
  • Similarly for interfaces, a member function with default implementation invites users to override it. As the author of the interface, you may want a reliable function you can use on that interface with expected behavior. Although the end-user can hide your extension in their own module by overloading it, that will have no effect on your own uses of the function.

For what it's worth, I think it would be very rare to choose to make an extension function for a class (not an interface) when you own the source code for it. And I can't think of any examples of that in the standard library. Which leads me to believe that the Coding Conventions document is using the word "class" in a liberal sense that includes interfaces.

Palatalized answered 10/2, 2021 at 14:26 Comment(0)
B
4

Here's a reverse argument…

One of the main reasons for adding extension functions to the language is being able to add functionality to classes from the standard library, and from third-party libraries and other dependencies where you don't control the code and can't add member functions (AKA methods).  I suspect it's mainly those cases that that section of the coding conventions is talking about.

In Java, the only option in this cases is utility methods: static methods, usually in a utility class gathering together lots of such methods, each taking the relevant object as its first parameter:

public static String[] splitOnChar(String str, char separator)
public static boolean isAllDigits(String str)

…and so on, interminably.

The main problem there is that such methods are hard to find (no help from the IDE unless you already know about all the various utility classes).  Also, calling them is long-winded (though it improved a bit once static imports were available).

Kotlin's extension methods are implemented exactly the same way down at the bytecode level, but their syntax is much simpler and exactly like member functions: they're written the same way (with this &c), calling them looks just like calling a member function, and your IDE will suggest them.

(Of course, they have drawbacks, too: no dynamic dispatch, no inheritance or overriding, scoping/import issues, name clashes, references to them are awkward, accessing them from Java or reflection is awkward, and so on.)

So: if the main purpose of extension functions is to substitute for member functions when member functions aren't possible, why would you use them when member functions are possible?!

(To be fair, there are a few reasons why you might want them.  For example, you can make the receiver nullable, which isn't possible with member functions.  But in most cases, they're greatly outweighed by the benefits of a proper member function.)

This means that the vast majority of extension functions are likely to be written for classes that you don't control the source code for, and so you don't have the option of putting them next to the class.

Biquarterly answered 10/2, 2021 at 14:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.