Hacker News new | ask | show | jobs
by brabel 702 days ago
The Ceylon language allowed you to do that sort of thing with types. If you "added" two maps together of types `Map<String, int>` and `Map<String, String>`, you would get back a `Map<String, String | int>` (or, equivalently, `Map<String, String> | Map<String, int>`).

But for some slightly complex reasons, most language designers find adhoc union types (which are required for this to work) a bad idea. See the Kotlin work related to that, they explicitly want to keep that out of the language (and I've seen that in other language discussions - notice how tricky it can be to decide if two types are "the same" or whether a type is a subtype of another in the presence of generalized unions) except for the case of errors: https://youtrack.jetbrains.com/issue/KT-68296/Union-Types-fo...

2 comments

> If you "added" two maps together of types `Map<String, int>` and `Map<String, String>`, you would get back a `Map<String, String | int>` (or, equivalently, `Map<String, String> | Map<String, int>`).

But these are obviously not equivalent: the first type is a map where all values are either strings or ints, and the second one is either a map where all values are strings, or all values are ints.

If that's confusing, consider: {foo: 1, bar: "2"}. It satisfies `Map<String, String | int>` but not `Map<String, String> | Map<String, int>`.

(In fact, the latter is a subtype of the former.)

P.S. You also seem to have misunderstood the toplevel comment about modeling record types as maps, as being about typing maps that exist in the language.

These types are equivalent if you consider a value of both of these types have the exact same read operations. Calling `get(String)` will return `String | int` in both cases. You're right that you could build a value of one of these types that does NOT conform to the union, however. I am not sure what's the technical name for what I am trying to say... are they "covariantly equivalent"???

EDIT: ok, I just wanted to say that one type, `Map<String, String | int>`, is a supertype of `Map<String, int> | Map<String, String>`, so if a function accepts the former, it also accepts the latter. They're not equivalent but you can substitute one for the other (one way only) and still perform the same operations (always assuming read-only types, if you introduce mutation everything becomes horrible).

I was trying to emphasize how having type "combinations" ends up causing the type system to become undecidable as you end up with infinite combinations possible, but as I haven't really gone too deeply into why, I am having trouble to articulate the argument.

> you would get back a `Map<String, String | int>` (or, equivalently, `Map<String, String> | Map<String, int>`)

How are these equivalent? Wouldn't the latter result in {foo:1, foo:"two"}, where the former wouldn't?