ClassTags are there, of course, but they are only useful to create Arrays of genetic types. If you’re trying to use a ClassTag for anything else, it’s wrong and it will blow up when you least expect it. There’s a reason we created TypeTest and Typeable.
Might be worth looking into why TypeTest
’s page isn’t accessible in the docs (I had to come into the url directly from Google to access it), and possibly update it to explain this? Google returns almost nothing immediately useful on the subject (Searching in the Dotty context returns more results, however most is on the older side and the danger you’re referring to isn’t apparent in the docs).
They’re different things but they have a method in common
In the thread about multi-level enums there is now a post by @michel-steuwer with an interesting use case for unions with shared members:
Trust me, nobody dislikes null
more than me, but you have to consider why null
is bad. It’s not the mere existence of null
which is the problem, it’s the fact that it inhibits every other type. In the presence of -Yexplicit-nulls
this is not a problem since null
has been lifted out to its own type.
The only benefit you get from your Empty
class over null
is that it allows you to call toString
. I don’t have any specific pitfalls in mind, it’s just that Option
and Null
has better support.
Specifically, the flow typing support for Explicit Null, makes a lot of what you’re trying to do easier. For example you can do this works without casts:
If you need to be able to call toString
etc, without checking for null
, then Option
is better supported and more importantly will be more recognizable for people reading your code.
Of course, Union Types in general would probably benefit a lot from wider support of flow typing, but I imagine implementing that is easier said than done.
Many thanks for your input and nice examples!! Esp. interesting that the proposed “Enhancement 1” above would require, or at least benefit from, flow typing. It would be a cool feature in a future Scala 3 version.
I enabled -Yexplicit-nulls
in my build and I got 26 errors related to java integration with javax.sound.midi
. So I should start digging into all those null-unsafe places… It is a nice bonus that Scala 3 now gives me a list of all those UncheckedNull
.
One goodie with the toString of Empty
is that my library, intended to be used as a “chord laboritory” with the REPL as main user interface, is that structures are prettyprinted nicely in the REPL. The type signatures also read nice with Empty
without type param. And if I introduce a similar concept of Silence
then it is also motivated from a domain modelling perspective.
Has the story around explicit null improved? back in version 0_28 (before the milestones and this RC), it was super broken, like, big time. Flow typing would randomly not work for trivial if (a != null) ...
, extending a java class like JPanel would simply make the compiler crash, java methods would return Array[Thing | Null] | Null
, and using .nn
would only remove the outer layer, etc etc
Edit: As of RC1, extending JPanel with explicit null on doesn’t crash the compiler, it still fails to compile with a weird error that seems to be it’s failing to parse the members of JPanel.
Using nn
still only removes the outer layer last I checked. The rest has generally improved significantly.
Yes, .nn should only remove the outer layer, what I meant to say and poorly articulated is that, the times of such collection coming from java is huge, and there’s no default .nn
variant to cast it away, like a .nnn
or something. To make things worse, while the outer array itself will be UncheckedNull, letting you do things immediately with it, the inner type wouldn’t (IIRC).
It’s still a -Y option so should be considered as experimental and some more changes are in the pipeline:
Update: The changes to nullability have now been merged.
While on the topic of explicit nulls…
I remember, while reading through one of the original drafts I believe, there being some discussion about flow types and how they were needed to strip away the nullability in a scenario like def len(str: String|Null) = if str == null then -1 else str.length
.
I find the concept really intriguing in a less specific setting, like for union types. Interestingly, it seems the mechanism is halfway there with pattern matching: an exhaustive warning for Y
is emitted if the type X|Y
is only matched on the X
branch. However, despite the compiler telling us that the Y branch is unchecked, it still infers the union for a “naked” match:
(xy: X|Y) match
case x: X =>
case y => // y is of type X|Y even though the compiler complains specifically about Y if this case is removed.
While the above example not being very useful on its own, the required foundation for these “as-you-go” refinement types seems to be in place (singleton types + intersections/unions). It does also solve the problem of stripping away part of a union in an elegant manner, which I remember toying with at some point.
I agree in principle. Now the only hurdles are algorithmic complexity, compilation speed, and whether the community at large cares enough about union types (which might depend on the outcome of the experiment, so it’s a chicken and egg problem). Somebody could go all in with union types and flow typing to try that experiment.
This happens because exhaustivity checking is run at a later phase than typechecking. Trying to move some of the logic from exhaustivity checking into the typer to give more precise types to the default case branch would be an interesting thing to try but no on has attempted that experiment so far.
What you are asking for makes sense, and I think any user of the system would expect the behaviour and would be surprised about this error. After all x
is definitely a member of A | B
. I.e., no element of the type lacks the method.
I realize that from an implementation perspective it might be difficult.
To generate a set of methods or members applicable to A|B
is probably hard. But that’s not what’s being asked for here. Instead, what is being asked is whether a user named method/member is applicable.
It could be implemented as
def f(x: A | B) = x.y
==
def f(x: A | B) = x match {
case x_A: A => x_A.y
case x_B: B => x_B.y
}
and the compiler knows if A
and B
have y
I really respect that the algorithmic complexity and compilation speed are of utmost importance for community acceptance. However are union types really a bit of a stab in the back. Do they give just enough abstraction to help the people developing dotty itself, but not really an abstraction level to the mortal programmer to make them elegantly usable?
I admit my perspective is naive. When Scala 3 is used in-force, we’ll see whether user’s love them or find them almost usable.
I have to weigh in here: I would consider the missing pattern matching exhaustivity to be a bug. As others have said, if union types are a real (new) aspect of Scala 3 language, they should be properly checked in pattern matching. (My strong opinion comes from the fact that I’m having to work around the lack of union types in my own work these days, and am eager to be able to write cleaner, more succinct code.)
IIUC then exhaustivity checking itself works fine. It’s only that the type of the fall-through case is not inferred based on the earlier cases. That’s not specific to union types. It’s the same for enums or custom ADTs:
scala> sealed trait Foo; case object A extends Foo; final case class B(x: Int) extends Foo
// defined trait Foo
// defined case object A
// defined case class B
scala> (A: Foo) match {
| case _: A.type => -1
| case b => b.x
| }
3 | case b => b.x
| ^^^
| value x is not a member of Foo
Interesting! Is not that a bug? I’d think that would work because of sealed
.
I answered that in Making union types even more useful - #53 by smarter.