Q: How Much of "the Kotlin Way" Is the Right Way?

Preslav Rachev on December 28, 2018

NOTE: This post was originally intended to provoke a discussion, and despite its length, the original goal is still the same. Please, share your fe... [Read Full]
markdown guide
 

Soon enough, tools, libraries, and frameworks started appearing, having perfectly working equivalents in Java, but written in "idiomatic Kotlin".

Mmm, what is the point of being citizens of the JVM and being interoperable if you end up rewriting stuff that already exists just because of ego? (Because I suspect ego is in play here :D).

Could there be legitimate reasons to rewrite something? Do they go faster if written in Kotlin? I don't know anything about it so I might be missing something. Because if the only reason is "I don't know Java, so I'm rewriting this in Kotlin" that seems very weak (and also it means you're likely going to introduce bugs and solve issues that were already solved in the first place :D)

I started wondering where I had seen this before 🤔.

ehhe

 

I know (a lot of) java (12+ years! Woof!), and I find myself having to rewrite a bunch of stuff because it doesn’t support immutable, fluent, functional, or generic idioms quite as nicely.

Once you peel back that first layer of over-interfaced factorybuilderbuilderfactory pattern that permeates the older java libraries, you end up having to do a lot of cleanup to make the new facing work properly. So then you might as well step one level in and clean that up too!

Oh dear, I’ve rewritten the whole thing.

 

Thanks Jon, why not limit yourself to writing wrappers or façade libraries instead of rewriting? Hiding the complexity of the Java-ish code in nicely thought out APIs. This way you have, for example, kotlin-http-client which explicitly depends on a Java open source lib and tracks its changes. This way you don't split the community.

Is this doable? Or is it the long term goal to rewrite as much as possible in kotlin and then say bye bye to the church of Java 😂?

You had to pick one of the worst examples of Java, didn’t you?

URL.toStream sucks and is not intuitive to read 6 months down the line. (And let’s not forget having to add in your own retry policies, header and response code parsing, turning off ssl verification, etc.)

Apache HttpClient is old and feels old. It gets the job done, but at the cost of having to implement anonymous interface implementations whenever you’re trying to mess with settings.

The biggest kotlin http client repo I can find is fuel. This library is... alright. We’re running into trouble debugging problems with it because it tends to barf deep inside HttpClient, which makes me feel like I should have just bit the bullet and gone with using the Apache jar straight up.

Another source of fun problems is java reflection. Specifically bean reflection. This is most noticeable when attempting to use Jackson to serialize the following class:

data class MyDynamoDbTable(
  val Name:String,
  val Version:Int,
  val SomeProperty:String
)

outputs:

{
  "name": "Jon's Spicy Meatball",
  "version": 2,
  "someProperty": "TBD"
}

Yes, I know that’s not proper kotlin idiom, but it’s non-obvious to me that Jackson would just convert them all from Pascal to camelCase. (The reason is that it’s the bean spec! Use annotations and make your kotlin ugly again! Yes, one of the things driving me to Kotlin is the explosion of Annotatiomania and Lombok. Used sparingly, annotations are good. Using them to code-gen, do too much abstraction, or hack the language are bad.)

Yet another fun one: in Apache Tinkerpop’s gremlin library, there’s a class named __.

If I’m writing a couple lines of interop, then yes, a nice facade will do. Just sweep the mess under the rug so I don’t need to see the FizzBuzzIteratorCheckFactoryBuilderContexts bleeding through every so often.

It’s just that used long enough, the facade takes on a life of its own. Debugging how the facade works and then having to context switch back to 1.6 era Java code when diving through undocumented code is even more taxing!

(And full disclosure, I really wish I had the time to just write all this crappy code in Haskell or Pony instead. Writing Kotlin is better than writing Java, but it’s not truly taking the shackles off)

You had to pick one of the worst examples of Java, didn’t you?

Hi Jon, it was just the first thing that popped in my mind because it's a classic library every language has, I don't have Java experience :D

Thanks for taking the time to make me understand it's not just a simple matter of writing interfaces

It’s just that used long enough, the facade takes on a life of its own. Debugging how the facade works and then having to context switch back to 1.6 era Java code when diving through undocumented code is even more taxing!

That's true indeed. A facade in a sense it's an anti-pattern, because it's easy in the beginnig but if not treated like a giant TODO: refactor what's inside it will give you pain in the end

(And full disclosure, I really wish I had the time to just write all this crappy code in Haskell it Pony instead. Kotlin is better than Java, but it’s not truly taking the shackles off)

I found out just now by googling that there's a version of Haskell for the JVM called Eta but maybe what you're looking for is... Clojure? Even though it's dynamic...

Hickey is right about a lot of things, but he’s wrong about types and nulls.

Types eliminate an entire class of errors.

Nulls destroy type systems.

Well, Clojure has types, they are just dynamic. I feel like the argument of static vs dynamic typing is as old as programming. Both camps have valid arguments.

I tend to agree more about the problems brought by null, it's one of those ideas that seem genius in the beginning, until they aren't.

He's wrong about how much productivity is lost/gained using dynamic vs. static.

Here's an overly simple and contrived example:

A function that returns the first letter of a string.

A la haskell:

firstLetter :: String -> String  
firstLetter "" = "Empty string, whoops!"  
firstLetter all@(x:xs) = "The first letter of " ++ all ++ " is " ++ [x]

A la clojure:

(defn first-letter [[x & xs :as all]] 
  (cond 
    (nil? all)
    nil

    (= "" all)
    "Empty string, whoops!"

    :otherwise
    (format "The first letter of %s is %s" all x)))

I left a bug in the Clojure code to illustrate my point. In haskell, I know exactly what happens when I try to call: firstLetter [1,2,3]. It doesn't compile, and my IDE/Vim/repl tells me as soon as I hit save.

(first-letter `(1 2 3)) has no such problems, it can even be inserted in the repl as part of a macro or definition without revealing the problem. But as soon as it gets run... "The first letter of (1 2 3 4) is 1". Hmm, seems like it's not returning the first letter at all! Oh wait... that's not it...

Yes this example is contrived and overly simple, but static typing systems prevent this entire class of bug from happening, so what I lose in refactor time I gain in not having to do type checking as part of function logic.

Hickey's key argument is that I can't change my contracts on my users, and that's fair. However in practice I find the following:

  1. Management never wants to pay for time to write proper tests.
  2. Spinning algol people up on functional style programming is not trivial (it's not hard per se, but walking people through to their light-switch "Aha!" moment is appreciably different for every student), so minimizing the teaching space by removing potential bugs is ideal for me.
  3. Typically the only person consuming my code's contracts is myself, and a refactor is completely under my control. When I change something from returning a tuple to a fully reified Type, that's on me and affects no external customers.
  4. When I am developing libraries that are consumed by others, new functionality becomes a new contract, and old contracts are left in place with sunset warnings and migration guides. Old versions of libraries are made available, but not supported without haranguing the users to tell us why they can't migrate so we can fix the new contract.
  5. I love lisps. They're cool and mathy.
  6. I love ML. Pattern matching is something I miss in any language.
  7. If you make me use a strongly typed language without something like Hindley–Milner type inference (golang, yours doesn't go far enough), then I will be sad.

He's wrong about how much productivity is lost/gained using dynamic vs. static.

It's funny because one could make the exact opposite argument.That's why I was saying that static vs dynamic is an argument as old as programming and a non starter in a sense. There are advantages in both.

My experience of "productivity" is higher in dynamic languages because I don't have to deal with telling the compiler which type is every single variable I use. REPLs also tend to help there. It might also be that I wasn't using the right statically typed languages :-)

The example you're using between Haskell and Clojure is definitely valid, Haskell seems clearer (though I'm not versed in either of them :D) but let me answer to your points, which make me think we have had different experiences in sw development, which is great because there's not a single way to do things.

Management never wants to pay for time to write proper tests.

I've never been asked to write or not to write tests. I've always written them, regardless of which language we were using. Tests are part of the software and I estimate them as such :-)

so minimizing the teaching space by removing potential bugs is ideal for me

Again, I went from static to dynamic and to me the first dynamic language was heaven on earth, I have bad memories of statically typed languages at the beginnning of my career :-) I like them more now.

Most dynamic languages also tend to be easier to teach in my experience, exactly because you have less stuff to read in the source file (which is always a plus) and to explain and because you can change contracts in a easier way

Typically the only person consuming my code's contracts is myself, and a refactor is completely under my control. When I change something from returning a tuple to a fully reified Type, that's on me and affects no external customers.

You can do the same in dynamic language. Changing contracts is always easy if you're the sole consumer of the code. BTW duck typing in dynamic languages tends to help here. A real world example: at some point in Python 3 they changed the internal type returned by the function range() from a list to an iterator. That didn't break because they behave the same way to the consumer 99.99% of the time, they just uniformed all functions returning iterators in the standard library IIRC.

When I am developing libraries that are consumed by others, new functionality becomes a new contract, and old contracts are left in place with sunset warnings and migration guides.

Yeah but this is the same with dynamic languages. Dynamic doesn't mean "I break the contract without telling you and now it's your problem". Good programming practices usally span between types of languages.

I love lisps. They're cool and mathy.

Cool, but Lisp dialects, if I'm not mistaken, are all dynamically typed

I love ML. Pattern matching is something I miss in any language.

Elixir seem to be having a good run, it has pattern matching! No JVM though. Maybe your ideal language is a ML for JVM. Ocaml-Java seems to be dead unfortunately.

If you make me use a strongly typed language without something like Hindley–Milner type inference (golang, yours doesn't go far enough), then I will be sad.

Sorry, I'm not sure what is that. I'm going to look it up :-)

 

I completely agree with the above, just a nitpicking:

what is the point of being citizens of the JVM

the point of being citizens of JVM might be JVM itself in the first place. Good VM is orders of magnitude harder to create than a new fancy syntax that compiles into the same bytecode.

 

Well, I think it's completely okay to make things the idiomatic way. There are certain things that, while perfectly working in Java, are simply too cumbersome to use and can be vastly improved. My favourite example of this is the new Android KTX that are basically extension methods over what already exists.

Like this:

view.viewTreeObserver.addOnPreDrawListener(
    object : ViewTreeObserver.OnPreDrawListener {
        override fun onPreDraw(): Boolean {
            viewTreeObserver.removeOnPreDrawListener(this)
            actionToBeTriggered()
            return true
        }
    }
)

Versus this:

view.doOnPreDraw {
     actionToBeTriggered()
}

So of course, the Java way works perfectly, but it's also a pain in the ass to write. Isn't it why Kotlin was made in the first place? If everything was great with Java I don't think there was any reason for Scala or Kotlin to exist.

 

Nitpicking your example, the differences between the two methods is hardly down to intrinsic ability of the languages. The Kotlin version is just a wrapper around the Java one, and a utility class can be written in Java to the same end.

The Kotlin KTX libraries are really more or less apache utils for Android, and would be just as useful written if written in Java, the advantage Kotlin offers in this regard is solely the benefit of extension methods instead of static utility functions which is more of an idiomatic plus than a functional one.

 

You're right, however I don't think we'd have any of these libraries in Java since that's not an idiomatic way of writing Java.

Ultimately what I meant is that what Kotlin brought to the JVM was another way of writing code that was compatible with the huge amount of libraries that exists in the ecosystem; and of course when people realised that they can write libraries taking advantage of some features of the language that makes thing easier, they'd do it.

Maybe a better example of this are Anko or Koin since both are libraries that feature a DSL built on top of Kotlin.

 

The general answer to rewriting libs in idiomatic kotlin is often to provide a more concise API for kotlin consumers. In many cases this could be achieved just writing extension methods and many libraries fall in this category such as core Android having many kotlin extensions, gson being another example. The strong uptick in libs lately is for kotlin MPP which I believe provides what you really want. Kotlin can provide the cross platform foundation and existing libraries on target platforms can help acceleration of development. For instance writing code that runs on node, android, and iOS. In this case you will end up using many platform specific libraries which is terrific while also mixing in new libs that simplify multiplatform.

 

Mmh, that's a very valid point.
There is no single answer IMHO.

Sometimes it's useful to reinvent the wheel and to break with the Java world.
Coroutines is probably the best example.
Kotlintest is so much better than junit.
DI in pure Kotlin is so much simpler than dagger.

Generally the best approach is more to add on top of the existing Java library the few things that makes it nice to use from Kotlin.

The logic of making it nicer for Kotlin can definitely be pushed too far. Kotlinpoet for example has all those builders to make it usable from Java. Ain't that obsolete and clumsy? I thought so. Not anymore. It's not a big deal in practice. Not splitting communities is more important.

 

I think this is really important. I will die on the hill that Kotlin is more enjoyable to develop with compared to Java, but I don't think we need to reinvent the wheel just to do it "the Kotlin way".

A great example comes from my own experience at my current employer. We have a BaseActivity class that is in Java, that could be shortened a lot by switching to Kotlin. We would get null safety, we would be able to use a when instead of if/else, but we haven't converted the file.

We haven't converted it, because it works fine, and we rarely touch this file to begin with. If we reached a limitation in Java, or we found ourselves touching this file regularly to the point that we'd benefit from using Kotlin, that might be a different story. However, I feel that with this file and in some of the examples you're referencing yourself, that's just not the case.

 

kotlin should have never been created, because scala exists. of course it's going to get its own libraries and grow a new ecosystem. better just to come over to scala, because we've already figured out most of the hard stuff the kotlin-using kiddies will be discovering soon

 

Kotlin was invented because scala exists.

The thing that drove them over the edge was the compile time loop. Their secondary goals were to make the language simpler than Scala, and to add features Java developers had been asking for in Java itself.

infoworld.com/article/2622405/java...

 

And yet they've ended up with a language with many more special cases than Scala and poor interop with post-8 Java (in particular with the decision to integrate null tightly at the language level just as Java was moving away from it), just as many predicted at the time.

 

I could not agree more.

Beware of the crowd of the “beginners friendly” evangelists who would literally trample you saying “the syntax must be easy enough for everybody” and all that crap they say about Go.

 

Many existing Java libraries are written in ways that make them impractical to use from code that follows modern good practice (whether you call that practice "functional", "SOLID", "idiomatic kotlin", "idiomatic Scala", or something else). It's good to write replacements that are more amenable to a better code style, and this isn't really a Java-versus-Kotlin problem - I've seen the same thing happen in Python where people write new libraries that offer the same functionality as existing libraries but in a more functional-idiomatic way.

This does mean the pitch for Kotlin was always based on a lie though. Scala already offered perfect JVM interop, but this isn't and will never be enough to bridge the gap between a mutation-oriented ecosystem and the style that we now realise is better. Kotlin was always doomed to become either a trivial syntactic sugar over Java, or a language that offered substantial benefits but at the cost of forking out a separate ecosystem, exactly as Scala did and for exactly the same reasons.

Of course Kotlin is now in the same place that Scala was 5-10 years ago - the same choices still have the same results, and it's impossible to deliver the benefits of Scala without paying the same costs. Being 5 years behind Scala is not such a bad place to be, but you're better off in today's Scala, and likely always will be.

 

Use whatever you like. Kotlin make it easy to use existing no kotlin code kotlin way by writing some extensions and so on... You don't need to rewrite library, just add some code for it.

 

Porting to Kotlin also gains you multi-platform code sharing. I'm highly motivated to port existing Java domain models and serialization DTOs into Kotlin so that I can build an iOS framework from it.

code of conduct - report abuse