Skip to content
This repository has been archived by the owner on Jun 5, 2023. It is now read-only.

Add support for lazy (explicit|implicit) parameters #11

Open
milessabin opened this issue Aug 22, 2017 · 15 comments
Open

Add support for lazy (explicit|implicit) parameters #11

milessabin opened this issue Aug 22, 2017 · 15 comments

Comments

@milessabin
Copy link

#1998 makes implicit parameters consistent with explicit parameters by allowing implicit parameters to also be by-name. This change was motivated in Dotty (and work in progress on Scala 2) to support the same sort of type level programming use cases which are currently enabled by shapeless's Lazy type.

However, the bynamity of a parameter prevents it from being stable which means that the following,

trait Foo {
  type Out
  def out: Out
}

object Test {
  implicit def bar(implicit foo: => Foo): foo.Out = foo.out
}

doesn't compile (either in Dotty or Scala 2 with by-name implicit support) because the by-name argument foo is not stable,

lazy-implicits-7.scala:12: error: stable identifier required, but foo found.
  implicit def bar(implicit foo: => Foo): foo.Out = foo.out
                                          ^
one error found

This pattern is very important in implicit-based type level programming, so this would be a disappointing limitation. By contrast, shapeless's Lazy captures the value in a stable context and hence supports the following with vanilla Scala 2,

implicit def bar(implicit foo: Lazy[Foo]): foo.value.Out = foo.value.out

Currently the following is a workaround with by-name implicits which compiles both on my Scala 2 branch and with Dotty,

trait Foo {
  type Out
  def out: Out
}

object Foo {
  type Aux[Out0] = Foo { type Out = Out0 }
}

object Test {
  implicit def bar[T](implicit foo: => Foo.Aux[T]): T = foo.value
}

ie. we use the Aux pattern to avoid the need for a stable value. This is clumsy however, and the additional type parameter (which typically must be inferred) can make it hard to express methods which also require explicit type arguments.

So, whilst by name implicits get us close be being able to replace shapeless's Lazy macro, it's not quite there.

It was proposed in the discussion on #1998 to use the lazy modifier on method parameters to express by-need (ie. at most once) evaluation semantics. If such parameters could participate in stable paths (at least modulo the restrictions described here and the issues enumerated in the Scala 2 ticket) then a lazy implicit parameter would more or less exactly replicate the semantics of shapeless's Lazy.

@ghost
Copy link

ghost commented Oct 18, 2017

Hey, Miles, what is the status of this issue?

@milessabin
Copy link
Author

No recent progress as far as I'm aware. I'll come back to it when I have time.

@ghost
Copy link

ghost commented Oct 18, 2017

I may have some time to contribute (actually, I am writing library code where the feature would be very useful). But I'm not sure if I'm the right kind of person to contribute (it has been some time since I've been involved in similar stuff).

odersky referenced this issue in dotty-staging/dotty Jan 7, 2018
Two main changes:

 - values with type => T are stable if T is stable
 - types deriving from scala.Singleton are considered stable

The two changes together allow to declare by-name-parameters to be stable
by declaring them to have a Singleton type.
@odersky
Copy link

odersky commented Jan 7, 2018

I have a potential fix for this. The program can be compiled if we change the definition of bar to

implicit def bar(implicit foo: => Foo & Singleton): foo.Out = foo.out

See #3773. WDYT?

@ghost
Copy link

ghost commented Jan 7, 2018

does "foo: => Foo & Singleton" indeed mean that foo is evaluated by need and at most once?
that would be great indeed

@odersky
Copy link

odersky commented Jan 7, 2018

does "foo: => Foo & Singleton" indeed mean that foo is evaluated by need and at most once?
that would be great indeed

No, it just means that the actual argument to foo is required to be stable. E.g. you can't pass a var. Note that a notion of "eval at most once" would not help with dependent types. #50 and #1050 show that taking dependent types of lazy vals is in general unsound - we need to guarantee that the argument is evaluated.

So it seems this would not in general solve the problem of lazy implicits because the inferred arguments to lazy implicits are most often not stable. It actually looks quite hard to find a good solution that maintains soundness.

@odersky
Copy link

odersky commented Jan 8, 2018

In fact #3005 does not seem to be a useful fix for the problem. And I don't know what a sound solution would look like. I am leaving this open for a while in case others have ideas, but if not we will close with stat:revisit.

@milessabin
Copy link
Author

@odersky I can't quite tell from the discussion on #50 what the current consensus on lazy vals in paths is. Are they ever allowed? If so, under what circumstances?

@odersky
Copy link

odersky commented Jan 8, 2018

@milessabin Lazy vals are allowed in paths, but they must be final and their type must be a concrete class. Here are some test cases. The code in dotc is in class CheckRealizable.

class C { type T }

class Test {

  type D <: C

  lazy val a: C = ???
  final lazy val b: C = ???
  val c: D = ???
  final lazy val d: D = ???

  val x1: a.T = ???  // error: not a legal path, since a is lazy & non-final
  val x2: b.T = ???  // OK, b is lazy but concrete
  val x3: c.T = ???  // OK, c is abstract but strict
  val x4: d.T = ???  // error: not a legal path since d is abstract and lazy

}

So on third thought, yes, this could actually still allow many important use cases.

@milessabin
Copy link
Author

Suppose we used the same criteria for lazy parameters? They can be viewed as effectively final and we can restrict types to those that are concrete in the same sense as class C.

@Blaisorblade
Copy link

@milessabin Was there any change here? I'm studying realizability right now, so this is good time to discuss it.

Suppose we used the same criteria for lazy parameters? They can be viewed as effectively final and we can restrict types to those that are concrete in the same sense as class C.

That currently makes sense to me.

@Blaisorblade Blaisorblade self-assigned this Apr 13, 2018
@Blaisorblade
Copy link

BTW, I think there are two questions here:

  • support lazy params at all (add lazy parameters scala/bug#240), which has also other use cases;
  • make them stable. At first sight, it seems this is done by not changing SymDenotations.isEffectivelyFinal or anything in CheckRealizable, though this needs testing — it's felicitous that the proposed logic is so close to the existing one that it needs no changes.

Beware I need to talk again to somebody who understands realizability here (that is, @odersky).

@milessabin
Copy link
Author

I've not done any more on this recently. I'd be very happy to pick this up in Scala if Dotty decides in favour of supporting lazy parameters.

@liufengyun liufengyun transferred this issue from scala/scala3 May 28, 2019
@dcsobral
Copy link

Is there some sort of SIP for this in Dotty?

@milessabin
Copy link
Author

The way we left it was that @odersky needed to rule on the stability issue.

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

No branches or pull requests

4 participants