Operator overloading can be misused, but for certain things it makes stuff much prettier as well.
https://github.com/JetBrains/kotlin/blob/924c28507067cbfbf78...
This is not that different from Java unmodifiable. In contrast the Scala immutable Set supports constant time addition,
https://docs.scala-lang.org/overviews/collections-2.13/perfo...
This isn't just an ivory tower concern either. Because Kotlin immutables aren't actually immutable (they just don't expose mutation interfaces) this means that if I get a ref to an immutable Set, I can't modify it but I can't rely on it not changing because it's still back by a mutable one.
In general the theme of that release seems to cherry pick a lot of stuff that Kotlin and Scala have been supporting for some time. That's a good thing. Java developers have been missing out on a lot of good stuff. There's quite a bit more of that of course where this came from.
Scala was an academic experiment on how to match nicely object oriented world with functional programing paradigm that got some hype because Java development was crawling like a snail. I am not sure if this experiment was successful after all, though.
Also, it got Java to move up development, together with the move to cloudnative.
From a language enthousiast point of view I’m still curious why Kotlin and now these features in Java get so much traction while most have been available for 20 years in Scala. Was it marketing? Was it people? Was it symbol soup? Who knows?
Java is simple. There were few operators, and really only one way to call a function. All function calls require parentheses. Conventions are available for pretty much any problem you can think of.
In functional land, you don't need to bother with calling a function a factory, or adapter, or whatever overwrought GoF pattern was maladapted. Despite being verbose and convoluted, Java was all basically the same things- classes and methods and a few annotations, and easy to Google concepts.
Enter symbol soup. Now, your conventions aren't named, you have to recognize them from experience. That creates writer's anxiety- even if I understand what I am reading, if I need to start from scratch I don't know that I'm organizing things right. There are multiple symbols that apply, combine or call functions, googling symbols is hard, asking questions is hard in meat space without a screen to show the unfamiliar syntax, and that's all table stakes before you get into understanding performance implications, code organization, maintainability, etc.
If I, hypothetical developer, don't know these things and don't have someone to hold my hand, but I DO know java, what's the point of learning the functional language?
This is the same problem F# had. Over time, C# kept getting all the good bits from F# without the baggage. Java has taken the longer road to get there, but today, if I were to pick one, the value proposition of the functional language appears to be on a trajectory of disappearing.
When I know that any library I invoke is literally incapable of changing the data I pass to it, that's tremendously freeing.
May be I'll replace some `if`-s with pattern matching, just like I use `instanceof` pattern matching today, but that's absolutely minor thing which is hardly worth mentioning.
I would even dare to say that lambdas and `var`-s for me are questionable features.
Actually as I grow older, I appreciate ascetic nature of Go and sometimes I think that using Java 1.4 might actually be preferable for many codebases.
I understand that for code which manipulates trees like compilers with their ASTs, pattern matching might be god send. The thing is... 99.99% of developers don't write this code, so optimizing language for it is strange goal.
Sometimes I think that some language features are driven by hype, even with Java.
I always happy about runtime improvements, though. And Java delivers in that front, so I can tolerate pattern matching and streams if I can get virtual threads and struct values.
Without knowing your application domain or seeing the code, it's hard to guess why. But one reason is that pattern matching and union/algebraic type tend to go hands to hands. So depending on how your data domain was modeled, it is normal that you don't find yourself not needing to pattern match as much.
In general,it's normal to find one self "not needing" a feature that one is not used to, professional are very good at structuring things is a way that work best with the current toolings.
> I understand that for code which manipulates trees like compilers with their ASTs, pattern matching might be god send
Very true, but i can assure you , pattern matching (combined with record and ADT) are very useful tools in general computation as well.
ADTs and pattern matching go hand in hand, and they - along with immutable structures - are the cornerstones of functional programming. They provide you very strong compile-time guarantees in a very elegant and readable form. I like to write code this way, and out of 10k LoC I would estimate at least half is ADTs and pattern matching for me. Maybe more.
Too bad that although Java's ADTs are coming along nicely, pattern matching is in it's infancy (due to limitations inherent in Java). This is the single language feature I miss the most from Java and Kotlin from Scala.
These days you can use Java 8 features with no issues, and Kotlin isn't really solving the fundamental problem of runtime version on target devices — it's just spitting out Java 8 bytecode by default. Android build system already _desugars_ newer Java language features for older runtimes, and they could do the same thing for all the new fancy features. I guess they choose not to because Kotlin is already there.
That said, I do believe Kotlin is still more concise, has better nullability handling, and upcoming exciting features of its own like compiler plugins
It turns out that Kotlin being able to consume Java libraries is worthless if Android can only use jurassic libraries.
Switch expressions and all the pattern-matching stuff though — that's all implemented entirely in the compiler. I use it on Android quite extensively with no issues.
These improvements are still nice for when you're stuck dealing with Java code, but in my experience getting projects to run on the latest version of Java isn't very easy with various dependencies all needing support first.
https://mail.openjdk.org/pipermail/valhalla-spec-observers/2...
But even if they do, I'm guessing that's still going to be a long ways away.
If Java were to include NulLAway in their standard language, which they clearly can do if they wanted to, I would consider it to have feature parity with other modern languages.
Meanwhile using Java, means using JDK out of the box with no extra sugar. Pretty healthy.
This doesn't change much for me, because I can't imagine writing Java without IntelliJ. Eclipse is just a disaster by comparison.
(I say this as a Java dev who occasionally gets tempted to try to help out in repos that are Kotlin/Scala, and gives up very quickly.)
Also, devs who use Go and C++ usually have a good reason (embedded systems and such), but Kotlin and Scala use seems to be motivated mostly by vaguely hipster-y annoyance with Java 8. And, like, sure, if you are annoyed by Java 8 and then use a language designed to have nothing in common with Java 8 except that it can import Java libraries and run in the JVM... well, it's gonna be a pain in the butt for your colleagues who do Java all day. And some of them aren't going to make the effort to work with it.
Just.. drop go from that. Go is closer to JS than to C++/embedded.
But I've never seen a large-ish Go application that I didn't hate. Because, yeah, as you said, it's closer to JS in a lot of ways.
It is ok for CLIs and small utilities, but I can’t really stand looking at it (they went with a type syntax that is neither C-like, neither Haskell-like and is absolutely unreadable to me, even though I can usually get the gist of any language from having seen my fair share of syntaxes).
> Kotlin and Scala use seems to be motivated mostly by vaguely hipster-y annoyance with Java 8.
Now that I think about it they seem like Instagram of language/code/devs. Mostly obsessed on surface level, superficial syntax features. IMO it is great in same sense as cooking with meal kit cooking is superior to cooking with grocery shopping.
I hate to admit it as someone who enjoy trying new languages and enjoyed my time using haskell a long time ago, but scala 2.x was definitely the most confusing and intuitive language i have ever used professionally. Really felt like a collage of desperate features without any coherence between them.
A tiny language with only a few, but powerful, features which can be used as building blocks to express even the most advanced patterns.
The problem are just the people "holding it wrong".
If you try to use it as Java++ Scala will be more cumbersome than Kotlin.
If you try to use it as Haskell this will become very painful quite quickly.
You need to use it as Scala… This means you need to embrace its features and philosophy. Just use the best parts of OOP and FP together!
And yet from the scala 3 website itself :
> One underlying core concept of Scala was (and still is to some degree) to provide users with a small set of powerful features that can be combined to great (and sometimes even unforeseen) expressivity. For example, the feature of implicits has been used to model contextual abstraction, to express type-level computation, model type-classes, perform implicit coercions, encode extension methods, and many more. Learning from these use cases, Scala 3 takes a slightly different approach and focuses on intent rather than mechanism. Instead of offering one very powerful feature, Scala 3 offers multiple tailored language features, allowing programmers to directly express their intent:
> The problem are just the people "holding it wrong".
If enough people have the same problem with a programming language, at one point it become the problem of the language, not the people.
> Just use the best parts of OOP and FP together!
Assuming that one can cleanly extract the best part of OOP and FP without bringing the baggage of eiter. It's not clear to me that those part don't have a certain level of "contradiction" which leads to the whole beeing less coherent than just OOP or FP.
unintuitive, perhaps?
Here are the things that if Java had, I probably wouldn't see a reason for other languages:
1. Lack of first-class lambda syntax. In Kotlin/Scala you can write something like:
fun doSomething(handler: (String, Int) -> Foo): Blah
In Java, all you have are the "Function<>" and related interfaces, which are clunky to use.2. Opaque types (Scala 3). These have been one of the most impactful programming features I've ever used, and I sorely miss them in languages that lack them.
object Foo:
opaque type UserId = Int
opaque type Email = String
def mkEmail(s: String): Email = s
def findUserIdByEmail(email: Email): UserId = 42
import Foo.*
val email: Email = mkEmail("foo")
val valid: UserId = findUserIdByEmail(email)
val invalid1: UserId = findUserIdByEmail("bar") // error: can't use String as Email
val invalid2: UserId = 123 // error: can't use Int as UserId
3. Union types (Scala 3). You can emulate them in Java/Kotlin with Sealed Types but it's much more verbose. type JsonScalar = String | Int | Boolean
type Json = JsonScalar | Map[String, JsonScalar] | List[JsonScalar]
// Makes writing functions that take multiple argument types much easier:
def handle(it: String | Int | Boolean): Unit = ...
4. Context-oriented programming with "given/using" in Scala 3 and "context-receivers" in Kotlin.This one is harder to explain succinctly but essentially it allows you to decorate methods/classes with required "contextual" args.
Instead of passing them as regular function arguments, you must invoke the function inside of an "environment"/"context" where the requirements are satisfied.
This makes threading dependencies through your code much easier, and eliminates the need for dependency injection frameworks in many cases.
interface Logger {
fun log(message: String)
}
object ConsoleLogger : Logger {
override fun log(message: String) = println(message)
}
ctx(Logger)
fun doSomething(): Int {
log("Hello")
return 42
}
fun main() {
val logger = ConsoleLogger
with (logger) {
doSomething()
}
}
5. First-class support for asynchronous programming. With "suspend" in Kotlin and a current prototype being done in Scala 3:- REPO: https://github.com/lampepfl/async | SLIDES: https://github.com/lampepfl/async/blob/main/scalar-slides.pd... | YOUTUBE TALK: https://www.youtube.com/watch?v=0Fm0y4K4YO8
6. Passing arguments by name, rather than positionally.
fun doSomething(a: Int, b: Int, c: Int): Int = ...
doSomething(a = 1, b = 2, c = 3)
7. Tuples (Scala). They're like anonymous data classes/records. val t: (Int, String) = (1, "foo")
val (a, b) = t parameter -> expression
(parameter1, parameter2) -> expression
(parameter1, parameter2) -> { code block }
and function signatures like the below might be "clunky" from your point of view, but IMHO are more clear since they document explicit types. (and you can navigate to their javadoc) Blah doSomething(BiFunction<String, Integer, Foo> fn)
2. Conceded. There are some JEP's around this but they all got rejected.3. Already covered in existing discussion. Verbosity level is fine.
4. Context oriented programming can easily be achieved using AOP in java. But frankly is readability and maintainability nightmare in any large project. I have seen projects/apps/libs that used this paradigm (in several languages) be re-written to explicitly designate all contexts.
5. Java with virtual threads now has far better support for async programming than Kotlin or Scala. It is now competitive with Golang in async ease-of-use. https://blog.rockthejvm.com/ultimate-guide-to-java-virtual-t...
6. shrug. Many popular languages have explicitly rejected function named parameters. Use a struct/record if you want named arguments is the usual answer.
Blah doSomething(BiFunction<String, Integer, Foo> fn)
What happens when you want to pass a function of arity 3, 4, 5 or 6?Also, it's nice to be able to give identifiers to the arguments:
// In Kotlin (absurd example, just to prove a point)
fun withHandler(
handler: (name: String, age: Int, isAdult: Boolean, callback: (Int, Int, Int) -> Unit) -> Unit
) {
handler("John", 25, true) { println(it, it, it) }
}
// In Java:
interface Handler {
void handle(String name, int age, boolean isAdult, Callback callback);
}
interface Callback {
void call(int a, int b, int c);
}
class JavaClass {
public static void withHandler(Handler handler) {
handler.handle("John", 25, true, (a, b, c) -> {
System.out.println("a: " + a + ", b: " + b + ", c: " + c);
});
}
} @FunctionalInterface
public interface VargsFunction<T,R> {
R apply(T... t);
}
@FunctionalInterface
public interface QuadFunction<T, U, V, W, R> {
public R apply(T t, U u, V v, W w);
}
Of-course Java is nowhere as flexible as C++ in this regard which has variadic templates, template parameter packs and template-template parameters. Well, you can do this with currying if one is feeling lazy, but obviously not recommended: Function<One, Function<Two, Function<Three, Function<Four, Function<Five, Six>>>>> func = a -> b -> c -> d -> e -> 'z';
The second point (named parameters) is one that several language designer greybeards (not just Java) have taken a deliberate design decision against for varying reasons. Use builders/records/structs is the usual advice given here anytime you ask them this question.green thread/stackfull coroutine such as in go/java and stackfless coroutine as in kotlin are well know solutions for introducing async programming in a way that feel natural to dev. They both have strength and limitation, i don't think one offer "far better support for async programming", and even more importantly they are not mutually exclusive and can be used together depending on one needs (see https://www.youtube.com/watch?v=zluKcazgkV4)
Only if your problem set matches one of using threads. There are other async problems that don't fit cleanly into a "force it to be a blocking thread instead" model. In particular those in front ends where being on a specific thread at specific times is important.
Green threads work great for server-style async, which is where go is seeing success. Then again servers are probably the last major usage of OpenJDK, so copying Go's tradeoffs here probably makes sense for it. But it's not unambiguously "the best way to do async"
That signature is awful. I never remember if the return type is first or the last in the list. Also the "generic" types don't document anything, by design. Yes it's an actual type but it's too generic to have any value over something like Kotlin or Scala declaration. It's impossible to tell from this signature what the input and output types are supposed to represent.
Almost always it's better to define a new functional interface than using BiFunction or similar.
I think golang shows that a synchronous, imperative paradigm wins the masses.
I'm not too brushed up with Kotlin suspend, but does it suffer from the classic "function coloring" problem that plagues other solutions? I've dabbled in functional effect systems in Scala, for example. I really enjoy them, but my coworkers sure don't when they realize that to perform some IO in a new place they will have to update a huge stack of type signatures to be wrapped in an IO monad. Async/await in javascript has the same issue, though the syntax is a bit friendlier.
My great hope for virtual threads in Java is that we can bring great IO performance and scalability, on par with golang, without retraining devs.
It might be but we don't know yet. I have yet to see a large scale application written with virtual threads (for the good reason that it is barely out of development!) I'm reserving judgement until I see virtual threads in use outside of toy examples.
Loom and Virtual Threads are one area I'm not as familiar with.
What I do know is that you can configure virtual threads as the coroutine dispatcher for Kotlin coroutines, and in a recent video Roman Elizarov talked about how the default "Dispatchers.IO" could theoretically leverage VT's in some future JDK.
I am not sure go shows that... Attributed the "poplarity"of go solely to its async model is kind of a leap.
> I'm not too brushed up with Kotlin suspend, but does it suffer from the classic "function coloring" problem
I never quite understand why function coloring is referred to as a problem. Including the async/non-async nature of a function in its signature is as natural as using any other monadic types as a return, such as Optional for example.
> My great hope for virtual threads in Java is that we can bring great IO performance and scalability, on par with golang, without retraining devs.
I think that's sometime a conflation that happens : virtual thread usually ease scalability at the the cost of performance.
For exactly the reason I said: colored functions are poison. Changing one type is easy. Changing a whole stack of types is tedious. And that's before you realize you tests have stopped compiling as well!
Kotlin has a few things that TS doesn't have but overall, I'd rate it very similarly. I probably enjoy writing TS a bit more than Kotlin.
EDIT: TS also has Opaque Types, sort of. They call them "brand types" or other names, it looks like this:
declare const opaque: unique symbol
declare type Opaque<T, K extends string> = T & { readonly [opaque]: K }
type Email = Opaque<string, 'Email'>
function mkEmail(email: string): Email { return email as Email }
function takesEmail(email: Email) { console.log(email) }
const email = mkEmail('foo@bar.com');
takesEmail(email);
takesEmail('wrong'); // errorEdit: didnt know about the brand types in typescript, TIL, thanks for the example.
That it's not Java. It's a huge thing with some developers not wanting to touch Java for <insert reason> but would be fine with Kotlin or Scala.
It is like using UNIX and not wanting to understand, touch, C.
At the same time I hate C and everything around it!
Both things go quite well hand in hand. You don't have to touch any C-madness most of the time. Even when you compile stuff yourself, like for example the Kernel, you almost never have to interact with C directly.
I would still prefer an OS written in Scala, but we don't have that at the moment. So I guess I will stick with Linux for the time being.
If people want to ignoring the foundations of their toolbox, that says a lot about the skillset.
Likewise, I probably need to know C to use POSIX, but it’s so unsafe that I should not be writing it.
- understanding stack traces
- writing idiomatic bindings for libraries
- debugging why Java frameworks fail consuming jar files whose bytecode was generated in a different way
- debugging why a jar misbehaves after a JVM bytecode rewriting tool messed up the bytecode sequence used to emulate other language semantics on top of the JVM
- adopting libraries whose compiler plugins and annotation procesors only understand Java semantics
- mapping debuging information and telemetry data from tooling like JFR into the original code of the guest language.
Or maybe I'm missing something in which case would you point me to a JEP or an article about destructuring in Java?
EDIT: nevermind, I should have checked JEP 440, which is about destructuring record patterns...
Aside from that, immutable / readonly collections and null safety are two big reasons for Kotlin or Scala.
- higher kinded types
- null in types
- for comprehensions
- macros
- opaque types
- implicits/type classes
- persistent immutable collections
- EDIT named & default params
Many "alot" of java programmers in the US cant code to JDK8 already. I love the JVM but these Java releases are not being adopted on any real scale for a reason. They break things. And syntax sugar is boring and unnecessary for a crew of software engineers that have no real ethos surrounding records or any of these features.
They are just being rolled out to appease devs from other ecosystems. They will not form a new or better method for building systems or improve performance.
I do a lot of serialisation. You have no idea how much I'd love to be able to just to `data.set("thing", this.valueEnum?.name())` instead of having to do `data.set("thing", this.valueEnum == null ? null : this.valueEnum.name())`.
You may be already familiar with this, but mapstruct is a godsend package that easily beats any kind of optional chaining.
Elvis operator. Drops mike.
Yet literally none of those things actually matter for any sort of real world application.
What matters is how quickly you can get code to do what you want, and Java/Kotlin lost that plot almost a decade ago.
This is the falsest statement I have read in a long time.
Maybe, if you write once and then throw it away (like a use-once shell script).
Otherwise you will have to maintain your code, and then readability and safety trump everything else. If not, it is about performance, but then a lot of time is spent on fine-tuning.
Trying to edit something means you have to dig through layers of inheritance into factories and injection frameworks, debugger is a slow piece of shit that almost needs ide wrapper to run, and as far as safety goes, people seem to have forgotten about log4shell, REALLY quick.
You can believe that strong OOP and all the theoretical bullshit in JDK21 matters, but in the future, Java developers will be the first to go as the world moves towards much smaller dev teams that are well versed in a super high level, AI powered language+compiler, where you can knock out production web apps in a day.
it's pretty quick to start with java (and presumably kotlin too - not too familiar with it).
I argue that it's even faster than with javascript. You at least don't have to toil with the build tool (java == maven).