DEV Community

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

Collapse
 
rhymes profile image
rhymes

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

Collapse
 
jbristow profile image
Jon Bristow • Edited

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.

Collapse
 
rhymes profile image
rhymes

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 😂?

Thread Thread
 
jbristow profile image
Jon Bristow • Edited

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)

Thread Thread
 
rhymes profile image
rhymes • Edited

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...

Thread Thread
 
jbristow profile image
Jon Bristow

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.

Thread Thread
 
rhymes profile image
rhymes

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.

Thread Thread
 
jbristow profile image
Jon Bristow • Edited

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.
Thread Thread
 
rhymes profile image
rhymes

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 :-)