Kotlin, but we do something stupid with it

Did you know you can use Kotlin for evil? Let's explore one of the ways!

Kotlin, but we do something stupid with it
Photo by Marc Reichelt / Unsplash

At work recently we decided it would be fun to create a "Worst Practices" example service, a service full of the worst ideas we could come up with to show off things not to do. The obvious things are in there, mixing indentation rules, tabs AND spaces, iNVERSE_pASCAL_dELAYED_sCREAMING_sNAKE_cASE, all the basics. But then we started playing around with Kotlin extension functions and trying to figure out how badly we could fuck things up using them improperly. That led to classes without functions defined in them, but instead everything is defined as an extension function in other classes.

Then a co-worker asked me "Hey Devin, is it possible to inject extension functions using Spring's DI?" To which I immediately replied "Nope." And that was it, discussion over, back to real work.

. . . But I couldn't get that question out of my head. Surely this was an impossibility. Surely there wasn't any way to actually make that happen. It's not like you can annotate extension functions with @Component right?

@Component // This annotation is not applicable to target 'top level function'
fun UserRepository.doWork() {
    
}

There. Perfect, it was settled. You can't mark an extension function as a component, so it can't be a bean. And that was it, settled in my head now that I'd proven out that you couldn't do it.

. . . Until it wasn't. Because I just couldn't let it go. At all. There was a way, damn it, and I was going to figure out it. Sure, I can't mark a top-level function as a component, but what about a member function?

In Kotlin, any class can declare a member function as an extension function. This is clever syntactical sugar if a specific class is going to be doing some work derived from an object, but no other class needs it, as it lets the functionality be tied to a call to the target object rather than a method on the parent object. This is useful if every time you expect to call a function you want to do a specific set of actions before/after it, like logging. It looks like this:

class Other(private val userRepository: UserRepository) {
    fun UserRepository.doWork(id: Long) {
	    val user = userRepository.findUserById(id)
        doSomething(user)
    }
    
    fun workHorseMethod() {
      // ...
      userRepository.doWork()
      // ...
    }
    
    private fun doSomething(user: User) {...}
}

So, could we do it? Could we target a member function to do an injection?

class Other {
    @Component // This annotation is not applicable to target 'member function'
    fun UserRepository.doWork() {

    }
}

No, of course not. That would be insane. Once again, our friend the compiler points out the futility and insanity of what we're asking and just like that I once again put to the question to bed, never to be brought up again.

. . . But what if we use a class to export the extension function as a bean?  And here's where things get wild.

If you're unfamiliar with Spring, it allows you to create an injectable dependency from any class, not just ones you mark as a @Component (Or any of its subclasses like @Service). To do that, you just need to have a method return the object you want and mark it with @Bean (One caveat, you must do this from a @Component or @Configuration annotated class). This is used frequently for configuration when you need to create things like customized authentication managers, but in our case, we can use it to create injectable extension functions.

@Service
class UserService(
    val userRepository: UserRepository
)

@Configuration
class UserServiceConfiguration {
    //A simple "find by id" method
    @Bean
    fun findUserById(): UserService.(Long) -> User? = fun UserService.(id: Long): User? {
        return userRepository.findUserById(id)
    }
}

And here we are, by declaring UserService.(Long) -> User? as the return type, we get to "magically" allow Spring to inject this type when requested!  To understand why, we need to consider how things like function types actually work in Kotlin on the JVM.

When we talk about function types in Kotlin we're talking about types like (Int, Int) -> Int and (String, Boolean) -> Long. These are represented in the JVM as implementations of a series of interfaces, Function0 through Function42. The JVM, at runtime, is told of Function0..22 and FunctionN for anything with more than 22 inputs, and uses these as the class definition for the functions. This comes into play for us because our extension function UserService.(Long) -> User? ends up represented by Function2<UserService, Long, User>, and since it has a class definition Spring is able to represent it as a bean in its dependency tree!  You can read more about this here.

And what's more, Kotlin is smart enough to recognize that the inject function is an extension for UserService and allow us to call it directly!  This is handled by a feature known as Function Literals with Receiver and it's extremely slick.

@RestController
@RequestMapping("/user")
class UserController(
    val userService: UserService,
    val userServiceFindUser: UserService.(Long) -> User?
) {
    // The restful end-point
    @GetMapping("/id")
    fun getUser(@RequestParam("id") id: Long): User? {
        // The injected function, being called on the class!
        return userService.userServiceFindUser(id)
    }
}

"But Devin, this seems like a horrible idea that would decouple functions from their domain, creating a rat's nest of dependency injection, ultimately making it harder to develop because no one is ever sure of exactly what a Service should be capable of because extension functions like this can be declared anywhere!"

Uh, yeah, that's the point. Remember, this all stemmed out of the idea of a "Worst Practices" service. And when I talk about "worst practices" what I really mean is "Ideas that have some modicum of a good idea on the surface but don't survive a deep dive." I can 100% see a junior/intermediate developer coming into an architecture design meeting with this idea:

Good Intentioned Intermediate (GII):  "OK, so my idea for creating the new service is as follows: Rather than force every service who needs our UserService to import a massive class full of functions they'll never use, we should instead declare the UserService as an empty class holding only the dependencies it needs and declare the actual functions as extension functions that can be injected into a service. This benefits us two-fold, it forces developers to understand exactly what they're bringing in and makes sure we can easily track everywhere functions are being used!"

See, when we say it like that it almost seems reasonable to do it this way!  Of course, though, this entirely ignores some of the more glaring problems with this (How would you handle annotations like transactional that Spring would normally handle extending? How do we handle multiple functions with the same signature in a way that is reasonable and maintainable? How do we avoid people creating their own extension functions for a service that live outside of the domain of the service? How do we handle the massive amount of imports services might need? How do we document this?) but the root idea may come from a good intention.

Anyway, that's today's fun (if stupid) fact about Kotlin.