Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

StdLib Extensions #22006

Open
4 tasks
odersky opened this issue Nov 22, 2024 · 12 comments
Open
4 tasks

StdLib Extensions #22006

odersky opened this issue Nov 22, 2024 · 12 comments
Labels

Comments

@odersky
Copy link
Contributor

odersky commented Nov 22, 2024

This is a mantle issue to collect areas where the standard library should be retro-fitted to work better with Scala 3. Please add more requests in comments, or, if you have access rights, edit this PR description.

@odersky odersky added stat:needs triage Every issue needs to have an "area" and "itype" label area:library Standard library itype:enhancement and removed stat:needs triage Every issue needs to have an "area" and "itype" label labels Nov 22, 2024
@som-snytt
Copy link
Contributor

inline tap and pipe extensions, with tap result this.type. (Arguably, anyone wanted it has already written their own, shout out to Ichoran.)

@bjornregnell
Copy link
Contributor

Beginner students often struggle with versatile patch on Iterable (IndexedSeq) and it would be nice if there are simpler versions for the simpler use cases such as insert and delete, or perhaps insterOne, insertAll etc. that could delegate to patch.

@MateuszKubuszok
Copy link

If we are allowed to modify the code (to break bincompat, but keep sourcecompat, kinda), then:

  • replace each implicit class in scala.Predef with extension
  • make implicitly inline as well
  • update conversions in jdk to use given Conversion instead of implicit def.

@odersky
Copy link
Contributor Author

odersky commented Nov 22, 2024

We will definitely not break backwards binary compatibility at this point.

@odersky
Copy link
Contributor Author

odersky commented Nov 22, 2024

inline tap and pipe extensions, with tap result this.type. (Arguably, anyone wanted it has already written their own, shout out to Ichoran.)

Why is inline important? And where should these extensions go? We already have tap and pipe in the library. It's somewhat annoying to add new ones elsewhere since we believe that the old signatures are broken. Looks like a mess, not sure how to address it.

@MateuszKubuszok
Copy link

If we are not breaking any compatibility (understandable) then there is very little that can be fixed this way: mostly adding some implicit evidence in such a way that evidence would be erased and not become a part of binary signature (didn't know it was even possible? is it?). Maybe OOTB witness here and there. What other changes are allowed?

@olhotak
Copy link
Contributor

olhotak commented Nov 22, 2024

Option.fromNullable

@bishabosha
Copy link
Member

Why is inline important? And where should these extensions go? We already have tap and pipe in the library. It's somewhat annoying to add new ones elsewhere since we believe that the old signatures are broken. Looks like a mess, not sure how to address it.

possibly replace the signature in the scala2-stdlib-tasty? otherwise the trick like with stdLibPatches.Predef

@JD557
Copy link
Contributor

JD557 commented Nov 22, 2024

Inlined override of foreach in Range so that we finally get fast C-style for loops

I'm not sure if this viable now in Scala 3, but I think it might be viable to also consider changing foreach[U](f: T => U) to foreach(f: T => Unit), which would help with specialization in cases like range.

For example, consider the following very simple example:

  trait Collection[T] {
    def foreachOld[U](f: T => U): Unit
    def foreachNew(f: T => Unit): Unit
  }

  final case class BoxedInt(x: Int) extends Collection[Int] {
    def foreachOld[U](f: Int => U): Unit = f(x)
    def foreachNew(f: Int => Unit): Unit = f(x)
  }

Here's how the BoxedInt bytecode looks like:

  public <U> void foreachOld(scala.Function1<java.lang.Object, U>);
    Code:
       0: aload_1
       1: aload_0
       2: invokevirtual #75                 // Method x:()I
       5: invokestatic  #110                // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
       8: invokeinterface #129,  2          // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object;
      13: pop
      14: return

  public void foreachNew(scala.Function1<java.lang.Object, scala.runtime.BoxedUnit>);
    Code:
       0: aload_1
       1: aload_0
       2: invokevirtual #75                 // Method x:()I
       5: invokeinterface #135,  2          // InterfaceMethod scala/Function1.apply$mcVI$sp:(I)V
      10: return

I believe this is the reason Range uses final override def foreach[@specialized(Unit) U](f: Int => U): Unit but, AFAIK, Scala 3 doesn't use @specialized anymore, so the optimization is lost.

Note that it's also possible to just do something like:

def foreach[U](f: T => U): Unit
@scala.annotation.targetName("foreachUnboxed")
def foreach(f: T => Unit): Unit

Which I think would not break binary compatibility.

Range does have other methods with manual specialization (like indexOf and lastIndexOf). From my tests in apache/pekko#1247 I believe that removing the generic parameter from such methods can also lead to a huge performance boost in some situations, but I imagine that the variance there can also be helpful.

@soronpo
Copy link
Contributor

soronpo commented Nov 22, 2024

  • replace each implicit class in scala.Predef with extension

Since extension methods are not as flexible as implicit class methods, I'm fully against this. We first need to try and solve https://contributors.scala-lang.org/t/relaxed-extension-methods-sip-54-are-not-relaxed-enough/6585

@OndrejSpanel
Copy link
Member

Why is inline important?

My reasons:

  • nicer, more natural callstacks when debugging or analyzing exceptions
  • it is possible to use return from inside of inline functions

And if you mentioned fast C-style loops, I guess if anyone uses pipe / tap in inner performance critical loops, I guess there might be some performance concerns as well, but I did not meet such situation yet (or if I did, avoiding pipe is usually much simpler than avoiding for).

@som-snytt
Copy link
Contributor

The original pushback from Ichoran on performance of tap: scala/scala#7007 (comment)

That is followed by the pushback from Miles on self.type result resulting in existential crisis (in that encoding). But it's absolutely essential that tap yield the value upon which it operates, so that a lint can detect that it does not produce a value which must not be discarded.

Also, the heightened dislike for "symbolic operators", peculiar to that period of time, deterred the definition of operators which everyone has asked for ever since.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

9 participants