Hacker News new | past | comments | ask | show | jobs | submit login

Statically typed languages without Sum Types (aka tagged unions aka enums) which includes Java, C# and C++ amongst others drive me crazy: they have no ergonomic way to express "or" types. This is a massive expressive hole which (along with lacking type inference) I believe is responsible for a lot of the hate towards statically typed languages.



This is one of the joys of TypeScript.

Some of my coworkers complain about being required to use TS instead of JS, and I just wonder why in the world you would want to use JS in a massive codebase.


It is, although the syntax for proper sum types (it calls them "discriminated unions") is really verbose in Typescript. I wish they'd make it more terse so people didn't use untagged sum types and hacks like `typeof` all the time.


Sealed classes in Kotlin largely solve that problem for me, and Java is getting those. It's not quite the same, but most of the time I find that if I'm trying to do Int|String, it's primitive obsession and there is actually a better sealed type hierarchy I'm missing.


If you care about the exact underlying memory layout such high-level types are way too blackbox-y. I doubt that historically this specific feature was responsible for any "static typing hate" (because languages with such high-level type systems were quite obscure 10 or 20 years ago). I have the impression that this hate was specifically a web-dev thing because many Javascript developers never experienced what it's like to work with a statically typed language until Dart and Typescript showed up (and then it suddenly was the best thing since sliced bread).


> Statically typed languages without Sum Types (aka tagged unions aka enums) which includes Java, C# and C++ amongst others drive me crazy: they have no ergonomic way to express "or" types. This is a massive expressive hole which (along with lacking type inference) I believe is responsible for a lot of the hate towards statically typed languages.

Union types coming in Scala 3


Yes, totally agree. It's the lack of statical type systems like this one that makes people hate them - for good reasons.


These languages have far better type systems than languages like python or javascript, that's not a reason to hate them.


We are talking about user experience here. In that regard, comparing python/javascript with Java/C++ is comparing apples with oranges.


We are talking about Python and JavaScript from the top of this comment chain, and the 'user experience' of writing Java/C# is closer to Python and JavaScript than to, say, Haskell.


Yes, but I find that the user experience of writing Rust or Swift is closer to that of JS than C# and Java.


Why? Rust's type system is basically a more sophisticated version of Java's, JS is in the opposite direction - a much simpler dynamic type system. Rust's lifetimes and borrow checker is additional complexity that JS doesn't have. Rust has longer compilation times than JS, longer than Java. Etc.


>Why? Rust's type system is basically a more sophisticated version of Java's, JS is in the opposite direction - a much simpler dynamic type system.

I would argue that a sophisticated type system is closer to dynamic typing than a simple type system. A type system is like guard rails that prevent you from doing certain things. A sophisticated type system gives you more freedom and possibilities than a simple type system, and hence is closer to a dynamic type system without the guard rails at all.


Java gives you a decent way to build guard rails, Rust gives you a somewhat better way, JS doesn't give you any way at all.

A more sophisticated type system gives you more elegant ways of expressing constraints, with increased language complexity. Java falls in the middle with a fairly simple language and type system. Any code which you do not know how to structure within Java's type system can be written with some Objects/Maps/Collections and a bit of runtime logic - basically giving you what you'd do in JavaScript, though Java's type system is sufficiently powerful for 99% of real-world use cases.

The main issue I see Python and JS programmers face when coming to a statically typed language like Java is the additional complexity of a type system. Saying that a more complex type system would somehow be more familiar is just backwards.


> Any code which you do not know how to structure within Java's type system can be written with some Objects/Maps/Collections and a bit of runtime logic

Yeah, but that means jumping through hoops because of the lack of the typesystem. And that's what many people don't like, hence the whole thread. For you that might be fine, but please understand that there are other people out that who are not okay with it so easily.

> Java's type system is sufficiently powerful for 99% of real-world use cases

Rather the opposite. Every big project that uses reflection/introspection or annotations or some kind of code generation tooling shows that the typesystem is not sufficient. Yeah, there are some cases where the above techniques were used and could have been avoided (while keeping typesafety), but often they are just required.

And then Java does not even have proper sumtypes or union types (enums only work when the structure is identical and I mean... we could count some strange workarounds with static classes and private constructors that pretty much no one uses due to horrible ergonomics). And these literally appear everywhere.

I diagnose that you are suffering from the famous http://wiki.c2.com/?BlubParadox


> Any code which you do not know how to structure within Java's type system can be written with some Objects/Maps/Collections and a bit of runtime logic Yeah, but that means jumping through hoops because of the lack of the typesystem. And that's what many people don't like, hence the whole thread. For you that might be fine, but please understand that there are other people out that who are not okay with it so easily.

Not jumping through hoops... the point is that you can write untyped code in Java similar to JS with similar complexity. If you really think JS is somehow better in this regard then writing horrible poorly-typed Java is not very different.

> Rather the opposite.

The opposite as in Java is arguably the most relied-upon language for enterprise-grade backend code, because it lacks 99% of features people would want? Okay.

> Every big project that uses reflection/introspection or annotations or some kind of code generation tooling shows that the typesystem is not sufficient.

Annotations and reflection are a feature of Java, they are not external to the language. Annotation and code generation are separate features from the type system - Rust's code generation and annotations are very commonly used. Reflection is equivalent to runtime typechecking that is common in JS. How can you say Java is worse then JS in this regard when the poor parts you point out are basically what JS does?

> And then Java does not even have proper sumtypes or union types (enums only work when the structure is identical and I mean... we could count some strange workarounds with static classes and private constructors that pretty much no one uses due to horrible ergonomics). And these literally appear everywhere.

First of all JS and Python do not have these either, so saying that Java is somehow worse in this regard is ridiculous. Furthermore the usefulness of sum types is fairly limited - what problem are you trying to solve with sum types in Java? Implementing an Either<A,B> is trivial in Java.

> I diagnose that you are suffering from the famous http://wiki.c2.com/?BlubParadox

I think that applies to you a lot more. You're criticising Java for giving you a whole bunch of features which don't exist in Python or JS, while also saying the language sucks compared to Python/JS because it doesn't have features of Haskell. The fact that you think JS is somehow closer to Rust than Java makes me think you have very limited experience with these languages.


> Not jumping through hoops... the point is that you can write untyped code in Java

Sure, but that already means you are jumping through hoops.

> > > Java's type system is sufficiently powerful for 99% of real-world use cases

> > Every big project that uses reflection/introspection or annotations or some kind of code generation tooling shows that the typesystem is not sufficient.

> Annotations and reflection are a feature of Java, they are not external to the language. Annotation and code generation are separate features from the type system - Rust's code generation and annotations are very commonly used. Reflection is equivalent to runtime typechecking that is common in JS. How can you say Java is worse then JS in this regard when the poor parts you point out are basically what JS does?

Well, maybe I misunderstood you. And with "sufficiently powerful" you just meant "someone can kinda use it to build something". Well then, yes. Saying it just doesn't make much sense to me in a discussion about ergonomics where people complain about type system limitations.

> Implementing an Either<A,B> is trivial in Java.

Okay, let me copy&paste how this can be defined in F#:

    type Result<'TSuccess,'TFailure> = 
        | Success of 'TSuccess
        | Failure of 'TFailure
or maybe a language closer to Java, here it is in Scala3:

    enum Either[A, B] {
      case Left[A](a: A)  extends Either[A, Nothing]
      case Right[B](b: B) extends Either[Nothing, B]
    }
I'm curious to see the "trivial" implementation in Java that equals the ones from F# and Scala. Mind that both solutions I gave allow to add a "fold(left -> handleLeft(...), right -> handleRight(...))" function which allows to manipulate the content, depending on what it is _without using any casts or reflection_. This is possible in Java, but I don't know any "trivial" solution.


> Sure, but that already means you are jumping through hoops.

Map<String,Object>, Object, a few other things you may need are standard Java, so not sure how this is 'jumping through hoops'. It's not necessarily more complicated, just not idiomatic java - the point is you CAN write shitty JS-style code if you want, how is that an argument for why JS is somehow better than Java?

> Well, maybe I misunderstood you. And with "sufficiently powerful" you just meant "someone can kinda use it to build something". Well then, yes.

Are you not aware that many prominent tech companies have a significant Java stack? Google, Amazon, Uber, Airbnb, Netflix, etc? Are you not aware of major open source Java projects such as kafka, elasticsearch, hadoop, android sdk, etc? What point are you even trying to make?

> Saying it just doesn't make much sense to me in a discussion about ergonomics where people complain about type system limitations.

What doesn't make sense is saying that Java's type system makes it a worse language than JS or Python, or that JS or Python are closer to Rust/Haskell.

> I'm curious to see the "trivial" implementation in Java that equals the ones from F# and Scala. Mind that both solutions I gave allow to add a "fold(left -> handleLeft(...), right -> handleRight(...))"

Here you go:

  class Either<L,R>
  {
      public static <L,R> Either<L,R> left(L value) {
          return new Either<>(Optional.of(value), Optional.empty());
      }

      public static <L,R> Either<L,R> right(R value) {
          return new Either<>(Optional.empty(), Optional.of(value));
      }

      private final Optional<L> left;
      private final Optional<R> right;

      private Either(Optional<L> l, Optional<R> r) {
        left=l;
        right=r;
      }
  }

Yes, it's longer and slightly more complicated, mainly because Java doesn't have pattern matching, and yes you can add typesafe fold and map functions to it without reflection. That being said, you gave me examples in languages with more sophisticated type systems than Java - these say absolutely nothing about why Java is worse than Python or JS.


> Map<String,Object>, Object, a few other things you may need are standard Java, so not sure how this is 'jumping through hoops'

When you put various things into this map and then later get them out and want to work with them, you will have to cast them to be able to do anything useful with them.

> the point is you CAN write shitty JS-style code if you want, how is that an argument for why JS is somehow better than Java

For the sake of our discussion: I have never said that JS were somehow better than Java. I much prefer statical type systems and would always pick Java over JS for a personal non-browser projects. But that's not the point of this discussion, so I'm playing "devil's advocate" here. It's important to understand and accept the shortcomings of statical type-systems - that's what I try to explain here.

> What doesn't make sense is saying that Java's type system makes it a worse language than JS or Python

You need to re-read what I (and the others in this subtread) have written. It is completely valid to criticize one part of language X compared to language Y without implying that this language X is worse than another language Y overall.

> [Java Either implementation]

> Yes, it's longer and slightly more complicated

And not only that, it is also not equivalent to the F# / Java examples. Or if it tries to be equivalent, it is buggy.

E.g.:

    Either.left(null)
Now I have an Either that is neither left nor right. Compared to the Scala example (because Scala also has to deal with the existence of null):

    Left(null)
This will create an instance of the type Left which contains a null-value. As I said, if I add a `.fold` method, then it will fold over the null. E.g.:

    Left(null).fold(left => "Left value is " + left, right => "Right value is" + right) 
This would return the String "Left value is null". You can't do this with your example implementation in Java, because the information is lost.

It is _not_ trivial to do that in Java, even when relying on already similar functionality like the built-in Optional type.


> When you put various things into this map and then later get them out and want to work with them, you will have to cast them to be able to do anything useful with them.

Considering this is entire hypothetical is a edge case, that's a minor inconvenience.

> But that's not the point of this discussion, so I'm playing "devil's advocate" here. It's important to understand and accept the shortcomings of statical type-systems - that's what I try to explain here.

That is the point of the discussion, the original claim I was objecting to was:

'This is a massive expressive hole which (along with lacking type inference) I believe is responsible for a lot of the hate towards statically typed languages.'

You're pointing out weaknesses in a subset of statically typed languages, and these are only weaknesses when compared to better type systems - not when compared to dynamically typed languages. I never claimed that Java had a perfect type system - I prefer Haskell and Rust.

> You need to re-read what I (and the others in this subtread) have written. It is completely valid to criticize one part of language X compared to language Y without implying that this language X is worse than another language Y overall.

It's not valid when you're using Rust or Haskell to show weaknesses in Java relative to JS. The original context was Java/C#/C++ vs Python/JS.

> Now I have an Either that is neither left nor right.

You're right. Here's a simple example without this problem:

  class Either<L,R>
    {
        public static <L,R> Either<L,R> left(L value) {
            return new Either<>(value, null, true);
        }

        public static <L,R> Either<L,R> right(R value) {
            return new Either<>(null, value, false);
        }

        private final L left;
        private final R right;
        private final boolean isLeft;

        private Either(L l, R r, boolean isLeft) {
          left = l;
          right = r;
          isLeft = isLeft;
        }
    } 
> It is _not_ trivial to do that in Java, even when relying on already similar functionality like the built-in Optional type.

It is trivial, it's just more awkward and lengthy but not complex at all - also no Optional. Plus there are stable libraries providing types like Either<A,B>, and other functional language features. Anyway, I'm not here to defend Java type system against Haskell, my point is that Java type system is a huge feature when compared to JS or Python.


> Considering this is entire hypothetical is a edge case, that's a minor inconvenience.

I believe this is not an edgecase. I have to deal with that almost everyday and I'm working with a language that has a much more advanced typesystem than Java. But I guess there is no hard data for that, so everyone can believe what they what. :)

> You're right. Here's a simple example without this problem:

If it's so trivial, then why do you even have to fix something in your first approach. Also, you second approach still has flaws and is not equivalent. Maybe you want to figure it out yourself this time? :)

Anyways, I guess we are talking different points. Have a nice day!


First of all, of course Rust hast some additional complexity because it is close to bare metal. But if you think this complexity away (to make it comparable to e.g. javascript), here are some reasons:

1) Better type-inference. In Java this has improved but is still much more clunky and boilerplatey. Good type-inference is important to not annoy the user.

2) Traits / type-classes. They enable a way of programming that comes much closer to duck-typing and avoid wrapping your objects in wrapper-classes to support interfaces like you are forced to do it in Java.

3) Better and less noisy error handling (looking at you Java, Go, C++ and most other languages)


You can't 'think away' the additional complexity of lifetimes and the borrow checker though. It's something you have to understand and keep in mind.

> Better type-inference. In Java this has improved but is still much more clunky and boilerplatey. Good type-inference is important to not annoy the user.

In my opinion, Java without type inference is fine - it's very minor issue, and there is a fairly limited scope of code that would actually benefit from type inference in terms of quality/readability. If you use a decent editor most of the redundant typing is auto-completed anyway.

> Traits / type-classes. They enable a way of programming that comes much closer to duck-typing and avoid wrapping your objects in wrapper-classes to support interfaces like you are forced to do it in Java.

Eh, Rust traits are better than Java's interfaces, but you can implement multiple interfaces for your own objects in Java without any wrappers. The issue is extending external objects to support new interfaces. Plus, the point is to have correct code defined and checked at interface/trait boundaries, something JS doesn't do at all.

> Better and less noisy error handling (looking at you Java, Go, C++ and most other languages)

The error messages for Rust can be much more complex than Java, and are probably more complex on average, simply because it's a more complex language and type system.

I would say the benefits of Java's type system far outweigh the imperfections and tiny costs when compared to a language like JS.


> You can't 'think away' the additional complexity of lifetimes and the borrow checker though

Of course you can't. But the problem of lifetimes does not go away, not matter if you use statical or dynamic typing. However, in javascript this problem does not exist, so obviously that can't be compared to Rust. If you use Rust, then because you _need_ this for performance.

> In my opinion, Java without type inference is fine

Fair enough, but most people see that very different, hence the unhappiness.

> Eh, Rust traits are better than Java's interfaces, but you can implement multiple interfaces for your own objects in Java without any wrappers. The issue is extending external objects to support new interfaces.

That's exactly what I said or at least meant. :)

> The error messages for Rust can be much more complex than Java

No no, not the error messages that the rust compiler gives you. I'm talking about error handling that the developer does.

> I would say the benefits of Java's type system far outweigh the imperfections and tiny costs when compared to a language like JS.

I agree, but just because the benefits outweight the problems, that doesn't mean people will be frustrated by these problematic parts. And let's not call it imperfections. Java is _so_ far away from perfection, that just gives your post a sarcastic touch.


> However, in javascript this problem does not exist, so obviously that can't be compared to Rust.

Static typing and a whole bunch of other things don't exist in JS as well. The point of a comparison is to highlight the differences and similarities. You were the one who said Rust is more similar to JS than Java, you can't just reduce the language to 'type inference and traits' - things JS doesn't have at all and say it's somehow similar to JS.

> Fair enough, but most people see that very different, hence the unhappiness.

You have data supporting this 'most people see it...' argument? Or you just made it up on the spot?

> No no, not the error messages that the rust compiler gives you. I'm talking about error handling that the developer does.

Error handling in Java is very straight forward and it's far more similar to JS than Rust is.

> And let's not call it imperfections. Java is _so_ far away from perfection, that just gives your post a sarcastic touch.

Java is a great language. It's not overly complex, it's fast, it has great tooling and IDE support, it has one of the largest library ecosystems. It has a huge developer community, many high profile projects, many high profile companies use it. It's easy to find decent Java developers for your project. It has a decent type system - far better than JS or python. From a pragmatic point of view Java is one of the best languages in existence.


Okay, I think it was not a good idea to compare these languages from the beginning. I don't think this discussions leads anywhere.


Modern versions of these languages have these features to a varying degree.


std::variant or std::any don't count? Overloading doesn't count? Templates don't count?

Seems like you should play with C++ a bit more.


Not a C++ developer, but from what I read, it says: "A variant is not permitted to hold references, arrays, or the type void". These are quite some limitations and don't really give a "smooth" experience.


None of those types make sense in variants. This is obvious to C++ programmers.

With all due respect, if you're not qualified to make assertions about something, perhaps refrain from labeling it as "quite the limitation".


Can you explain why it makes no sense for it to hold an array or void? I'm really curious and will take back my claim.


You cannot trivially compare or move an array. Note that `std::array` is still allowed in `std::variant` - just not C-style arrays.

As for references, you cannot re-bind references. Rationale here: https://stackoverflow.com/a/27037871/510036

As for void, apparently the reasons I had in my head are not the reasons in real life. I thought it might be because of a destructible requirement on the type, but it turns out there really isn't a good reason why they disallowed it, and that a future standard might allow it.

In any event, there are a multitude of variant implementations that allow all sorts of things depending on the behavior you want. Nothing is forcing you to use the standard library.


Thank you for the explanation!

I just wonder why it is hard to make a variant-type that works with everything. Well, if the language prevents e.g. reference rebinding, there can't be done much.

But not being able to put _anything_ into a variant severely limits the way it can be used for abstraction. Especially for library authors, because they might not no what their users will pass them. So when they write a generic method, that uses variants under the hood, they would have to "pass down" the restrictions to the user and tell them not to pass e.g. void. Same for the interaction of two libraries.

Or am I misunderstanding the constraints here?


> I just wonder why it is hard to make a variant-type that works with everything.

Because standard C++ has to work for the general case. It's specifically designed so that more pointed or specific implementations that have other concerns (e.g. supporting C-style arrays or void) can do so, accepting the runtime penalties if desired.

> reference rebinding, there can't be done much.

References are syntactic sugar over pointers at worst, and a means of optimization at best. C++ is a pass-by-value language first and foremost, and goes to great lengths to keep things as optimizable as possible when it comes to standardization.

Again, variant supports pointers just fine. It also supports smart-pointers just fine. There's nothing preventing you from using those.

Remember that C++ has to work across all architectures, platforms, etc. Not everything handles references the exact same. Compilers are afforded many liberties when it comes to them in order to optimize for the target machine.

> But not being able to put _anything_ into a variant severely limits the way it can be used for abstraction.

Aside from `void`, I disagree. Like I said before, you can implement your own variant quite easily if you want those things. There are decent reasons (except for `void`) not to include them in the standard.

> Especially for library authors, because they might not no what their users will pass them.

I'm not so sure I understand what you mean here. Templates tell library authors /exactly/ what will be passed to them.

> So when they write a generic method, that uses variants under the hood, they would have to "pass down" the restrictions to the user and tell them not to pass e.g. void.

They don't have to tell the user anything. The compiler will inform them void is not allowed if a type cannot be compiled.

> Or am I misunderstanding the constraints here?

Yes. Most of what std::variant does in terms of type checking happens at compile time. Unless a program has been modified after compilation (which should never be the case), there's no possible way for the "wrong type" to be passed to a variant at runtime, because the assortment of possible types have been checked at compile time.

---

EDIT: I just realized why `void` may not be included, though I admit it's speculation.

`void` is not allowed as a function argument type; it is not equivalent to e.g. `decltype(nullptr)` and simply is the lack of type.

Therefore, there is no valid specialization of `operator=(const T&)` that would accept a "void type" because there is no way to implicitly invoke `operator=(void)` (you'd have to call, literally, `some_variant_object.operator=()`, which is very un-C++).

The alternative would be to have a method akin to `.set_void()`, and it could only be enabled if `void` was one of the types passed to the template parameter pack - and, if it is the only type passed to the parameter pack, all other `operator=()` overloads would have to be disabled.

This is an incredibly confusing API specification that I can understand if never included in the standard.

Note that, in this case, there'd be a difference between "no value" (null) and a "void value" (not-null), which is overly confusing and, again, very un-C++ (or un-C for that matter).

If this is the rationale, it makes a lot of sense and I agree with it. If I need a variant that supports `void`, I'd probably write my own anyway because there's probably a domain-specific use case.


std::variant is basically a parody of everything wrong with modern C++.


Please elaborate because I strongly disagree with you.




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

Search: