Pre-SIP: warn against unidiomatic features (e.g., null value and unsafe casts)

Agreed. Seriously, I think the community has been maturing to the point of recognizing that Scala is not The One True Language For All Purposes. It happens to be my favorite “general heavy-lifting language”, and I’m a huge Scala advocate, but there are plenty of problems for which I’d reach for Rust, Haskell, C#/F# or lots of other possibilities – most popular languages are a good answer for certain problems. (I was quite happy to see the ScalaDays keynote last year being on Rust – it’s healthy to recognize that there are other good languages out there, with lessons for us to learn from.)

Years of experience has taught the community a lot about what works well and what doesn’t in Scala, and the Scala 3 project largely reflects that – it’s adding a bunch of stuff that the consensus says will help, but also removes several things that have proven to be problematic over the years. It’s becoming a little more focused and opinionated, far as I can tell, and I’d say that’s a generally good thing – it’ll help teams work together a little better, and help folks better understand what Scala is good at.

But I don’t think it’s terrible if we acknowledge that it isn’t the perfect solution for some use cases, and that some language features should be used with considerable caution. null is very high on that list, IMO – I have rarely used it in any Scala code except when strictly required by APIs, teach students to never use it except when strictly necessary, and generally consider it to be a ticking timebomb to be avoided. That’s not the only possible attitude, but I think it’s a pretty common one…

3 Likes

I have used it a lot last time I was working in Scala (a 2D game, lot of GUI stuff, it was in every screen class and majority of widgets, so like 2/3 of whole code-base). Mainly in enforcing initialization order in traits with self-type with many dependencies on base class and even other traits base class mixes in.

Simple example demonstrating what I mean which will crash on NPE:

trait B {
  this: A =>
  val b = Seq(2, 3)
  println(a.length)
  println(b.length)
}

class A extends B {
  val a = Seq(1)
  println(a.length)
  println(b.length)
}

Generic solution I adopted was to convert val x = ... to var x: X = _ and add an initialization method to each trait, so base class can initialize everything in a correct order.

It is a good advice.

The definition of more suited is wrong.

I heavely use implicits and more often null when I write domain extention.
But we forbid using implicits and we do not recomend using null in high level business logic. Because usualy implicits in high level cause even more errors than null.

I think logic like :

  • I do not like it,
  • I do not use it.
    lets forbid it if it is posible.

It is probably witch hunt.

I don’t understand what you mean by that. Where is this definition of more suited coming from?

Lets forbid it if it is posible.

Let’s get back down to earth. This thread is a pre-SIP about giving a warning when a language import is not present, like what happens now for existentials or implicit conversions. Not about forbidding anything.

1 Like

It is wrong assertion for me.

Ok.

Your example seems like a legitimate use case for (the now deprecated) DelayedInit or the hypothetical OnCreate. You could add your use case to the discussion there.

1 Like

Just some examples of base libraries on java with heavy usage of null

I do not want to make additional works in such cases. And the arguments like:

  • null is unidiomatic because it’s widely considered a code smell to make an API that relies on it in Scala.

Don’t convince me

I am sure that people which makes api are reasonable enough to understand when they should use it.
The error of that sip that it suggests difficulties for library users. If it were for library authors I would be able to think it is good.

You can use lazy val to avoid this problem completely. Its actually recommended to use lazy val if you use cake pattern/trait mixin which you appear to be dong.

There is some performance overhead on lay val though, so I guess it depends on how much this effects your game.

I quickly reviewed that API, and the only requirement for null there seems to be in the constructor, which is easily replaced (instead of new File(null, child) simply use new File(child))

What use case do you see in this API where you want to use null that I’m missing?

Yes because there isn’t an alternative in Java up until Java 8 (which introduced option types but they are very hard to use)

Well you should because its not considered idiomatic. There are already plenty of tools in Scala to interact with Java if you happen to use Java libraries (i.e. the Option constructor which returns None if the value is an option. There is also scala.util.Try which catches thrown NullPointerExceptions)

Sure, but using null is not considered idiomatic in Scala code, that is a fact. There are exceptions (i.e. performance, Java interopt) but they are just that, exceptions.

Are you joking?
Instead of writing:

 val a = file.getParent()
 if (a!=null){
 .... 
 }

I must use plenty of tools in Scala?

Thank you. I hope I will never have to use it.
:slight_smile:

I think If I try to explain why it can be comfortable I will recive many arguments like

  • you are wrong it is not scala way plenty of tools in Scala to interact with Java.

At the and I will recive somethink like

  • Let’s get back down to earth

Sorry I am not ready. I think who want to understand my position already can do it.

You’re wrong about that. I for one am trying to understand your position, but can’t see it.

May be, but I do not know what to say else.
I can only repeat
We use null when

  • use java library
  • performance (also in everyday business logic)

am not ready to argue that I can write something like

val o = option(file.getParent)
 o match {
    case Some(p) =>  
       ...
}
//instead of 
 val p= file.getParent()
 if (p!=null){
 .... 
 }

I think warring is bad because:

  • there are no better alternative in such cases
  • It add much more complexity for library user than for library authors. Authors of library can just ignore such warning. The main additional works will fall on users.

I do not like a suggestion which does not suggest better alternative, instead it suggests more difficulties.

At the end people can just disagree :slight_smile:

Sure, you can do

Option(file.getParent()) match {
    case Some(file) => ....
    case None => ....
}

That way you handle all cases properly

The point of the SIP is not that you should never use the feature because there is no better way to do it (whether there is in your example depends on the rest of the code, maybe there is, maybe there isn’t, maybe it’s a matter of opinion), the point is that you get a warning if you don’t explicitly enable the feature with either an import or a compiler option.

I don’t believe adding a language import adds much complexity for the library user at all. To me, it seems entirely straightforward, especially since the warning points towards the language import.

You import the language feature to tell scala - hey, I’m about to do something unidiomatic with the language, and I’m going to do so anyway because I need to interop with Java.

You add the compiler option because you do it all the time.

The same as it’s now with existentials, higher kinded types (though I believe that’s a mistake), dynamic objects and reflective calls.

They’re part of the language, there are cases where they are the right choice, and you should totally use them in those cases. But there are many cases where they are not, and you should be warned that you’re doing something mildly funky, unless you turn off the warning by import or compiler flag.

2 Likes

That’s a good point. I am pretty sure I tried it, but under some conditions I still managed to break it - not sure if I got null or I wasn’t able to order actions as I needed (sometimes construction was branched and I don’t think one can check lazy val if it is already initialized without triggering initialization). Right side of val was calling a method which constructed a widget and added that widget to the current widget (to this, order of adding is important). Maybe converting everything to lazy would have solved my issue, I am not sure. The problem is that it is not that easy to reason about and debug code where everything is lazy compared to explicitly calling init/create methods in traits from a base class. Plus, as you mentioned, the performance cost could be too high.

My point was that while = _ is not considered idiomatic, it can still be used heavily in some codebases and since Scala tries hard to be compatible with Java world we should not presume only beginners use null or = _ (in my case I was using a Java library, but I think there are also types of projects which cannot afford to use other approaches because of performance cost even if they are in pure Scala).

Honestly, I would prefer having both. And for the project I was mentioning, it would be incredibly useful having also something package scoped (e.g. disable it for all UI stuff, but keep it enabled in game logic), but I don’t know if many people would use it.

Does importing the language import in the package object work for that? I don’t know if it does, or how I feel about whether it should. It’s a separate discussion I suppose, but good to take along.

TBH, I think this entire pre-SIP is moot. Not arguing about the idiomaticity of such constructs, or whether they’re sometimes useful, or whatever. But we already have experimental evidence that language imports have solved nothing. They are not the deterrent their authors intended them to be. People just import them, or add codebase-wide compiler options, and never stop to ask themselves “hey, maybe if I have to import something, it’s because that feature might not be what I’m looking for”.

Putting more stuff under language imports will accomplish nothing.

4 Likes