Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Fair points. I haven't worked with Go in a few years, and I remember hating it when I did, but I feel like I remember hating Java more. It's possible that part of the Java hate is not from the language itself, but from the ecosystem.

Can you elaborate on Java streams vs Go's containers? I assume you mean things like List and Heap in Go? I'm not sure why you'd compare those to Java's stream API rather than Java's collections. In any case, I do agree that Java's standard library has WAY better collections than Go does, and Go doesn't have the excuse of wanting a minimal standard library.

However, I'll push back a bit on the complaint that working with Go's containers/collections/whatever requires imperative code for everything. Now, I'll remind myself that one of your original points was that Go was "actively hostile toward functional programming" and I retorted to imply that Java was just as bad at all of the things you mentioned. I'll concede that Java isn't actually quite as hostile toward functional programming as Go. But, I'll move the goalposts a bit and claim that supporting some few functional programming patterns isn't inherently good and doesn't automatically make a language better.

> And endless `if err != nil return err` every time you want to call a function - which actually destroys useful stack information.

I agree and disagree. I'm one of the few people who still thinks that checked exceptions are a good idea for a language. I have my complaints about how they're implemented in Java, but I think the concept is still a good one and I honestly think that even the Java implementation of checked exceptions is mostly fine. The issue, IMO, is with training and explaining when to use checked vs. unchecked exceptions and how do design good error type hierarchies.

Go's idiomatic error handling is mostly stupid because Go doesn't have sum types. But, I'd argue that if you are wanting stack information, it means that you shouldn't be returning error values at all- you should be panicking. Error values are for expected failures, a.k.a. domain errors. You can and should attach domain-relevant information to error values when possible, but generally, there shouldn't be a need for call-stack information. A bug should be a panic.



Here's a Java example that sums the populations of a list of Countries:

    int population = countries.stream().mapToInt(Country::getPopulation).sum();
The Go implementation:

    var population = 0
    for _, country := range countries {
        population += country.Population
    }
It gets more perverse if you need to flatMap, or transmute components of map types, etc. If you want even more power, take a look at https://github.com/amaembo/streamex. This sort of container manipulation is bread and butter for business processing. I use it every day, sometimes with a dozen operations. This (with liberal use of `final` values) makes for some pretty functional-looking code.

I'll grant you the Kotlin or Scala version is slightly more compact. But not fundamentally different, like the Go version.

I (and the pretty much every language designer in the post-Java era) disagree with you about checked exceptions, but that's a whole different thread...


The go version looks perfectly fine to me (saying this as someone who uses clojure every day) ;)

Something else to consider is performance, in most implementations the for loop is going to be more efficient.


That's exactly my complaint- most languages have eager, mutable, non-persistent, collections because they were not designed with functional programming in mind.

Then FP became the hot new shit, so they all added some of the lowest hanging fruit so that people can say absolutely weird things like "I do FP in C#". The problem is that the majority of these implementations just eagerly iterate the collection and make full copies every time. So, you're much better off with a for-loop.

To be fair to GP, though, Java has legit engineering behind it, and the way they did it was to introduce the Stream API, which is lazy sequences, and they made the compiler smart enough to avoid actually allocating a new Stream object per method call (which is what the code nominally does, IIRC- each method wraps the original Stream in a new Stream object that holds on to the closure argument and applies on each iteration).


If you really want to go wild, take a look at https://www.vavr.io/ (formerly jslang). You can make programming in Java as functional as you want.


I have to admit, that looks pretty slick.

Have you used it? I'd be curious to hear how well it works in practice.

It seems like the only "big" things Scala has over this is its implicits (which so many people hate, but have been really improved in version 3) and its for-comprehension syntax.

It's so interesting to see a bunch of projects converge on really similar things. You look at Scala, at this Vavr stuff, and at Kotlin + Arrow.kt, and they're implementing all of the same stuff over Java.


Ah. You know what? I forgot that the Java implementation of these concepts isn't stupid like it is in some other languages (except what the heck is mapToInt? Some optimized version that makes a primitive array, I guess? Yucky- I wish the compiler could just figure that out).

So, I concede that Java's addition of the stream API is a legitimately good example of adding an aspect of functional programming to an otherwise very non-FP language.

But, let me go off on my tangent, anyway. ;)

It's not that you need to convince me that functional programming is great. It's just that I find that consistent and coherent designs tend to work well and that kitchen-sink or be-everything-to-everybody approaches tend to be good at nothing and mediocre-to-bad at everything.

MOST languages that have tacked on the low-hanging fruit of FP (map, filter, etc combinators on collections) have done it in a really sub-optimal way.

JavaScript, for example. JavaScript has eager, mutable, non-persistent, arrays as the default collection data structure. When they added map, reduce, filter, etc to Array, they added them in the most naive possible way, which means that doing something like your example above (map-then-sum), would create an entire extra array with the same number of elements as the original, and would end up looping both arrays once. So we have ~2N memory usage and 2N iterations where we really should just have an extra 8 bytes to hold the sum and iterate over the array once (N iterations).

Same thing with other languages like Swift and Kotlin.

Kotlin maybe should have an asterisk because it has Sequence, which will mostly work like Java's streams. However, there are two issues: it still offers them on eager iterables, instead of forcing us to use a sequence/stream to access them, and with suspend functions you have to be careful with Sequences. In you Java example, we're theoretically allocating a new Stream object with every combinator call, BUT we "know" that the compiler is smart enough to avoid those allocations and the result code will be about as fast as writing a for-loop. With Kotlin's suspend functions, we can very easily thwart the compiler's ability to do that. If you use a Sequence chain inside a suspend function and call another suspend function as part of that chain, then that's a yield point and the compiler can no longer optimize away the allocation of the intermediate Sequence object(s).

So, my point is that designing a language with some initial philosophy and then trying to borrow from, frankly, incompatible other philosophies usually leads to sub-optimal implementations and/or APIs. Again, though, Java's streams are a good counter example to my claim.

> I (and the pretty much every language designer in the post-Java era) disagree with you about checked exceptions, but that's a whole different thread...

Indeed it is! :) I'm willing to be the black sheep, and die on that hill, though (too many metaphors?). And, honestly, I don't think it's as unanimous as some people claim. I see returning monadic error values as isomorphic to checked exceptions, and several languages have gone that route since Java: Scala, Swift, and Rust, to name a few. Kotlin's lead dude, Roman, simultaneously claims that checked exceptions were a terrible mistake, but then also advocates for using sealed classes for return values when failure is expected or in the domain, which sounds a lot like what checked exceptions are supposed to be used for. TypeScript can't have monadic error handling because of its design philosophy of being a thin layer over JavaScript, but many in that community have embraced using union types for return values instead of throwing Errors.

Cheers!


Yeah, mapToInt is annoying because the primitive/object dichotomy in Java is annoying. No question about it, it's a wart on the language. Though it does offer some optimization abilities, so the dichotomy is not completely meritless - it's easy to understand why the language designers did it this way. Maybe project valhalla will fix this someday, I don't know. In the mean time, it's not a fatal flaw.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: