Skip to content

Refine types according to their constructor val’s singleton types #1262

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

Closed
julienrf opened this issue May 18, 2016 · 8 comments
Closed

Refine types according to their constructor val’s singleton types #1262

julienrf opened this issue May 18, 2016 · 8 comments

Comments

@julienrf
Copy link
Contributor

Being able to have constructor parameters for traits is great, however using constructor parameters in the following situation is not very useful because the type Quux will not be refined according to the value’s singleton type of the foo constructor parameter:

trait Foo {
  type Bar
}

trait Quux(val foo: Foo)

And then:

object FooInt extends Foo {
  type Bar = Int
}

val quux: Quux { val foo: FooInt.type } = new Quux(FooInt)
// error: type mismatch:
//  found   : Quux
//  required: Quux{foo: FooInt.type}

It works if I replace the constructor’s val parameter foo with an abstract member and refine its type in the instantiated anonymous class:

trait Quux2 {
  val foo: Foo
}

val quux2: Quux2 { val foo: FooInt.type } =
  new Quux2 { val foo: FooInt.type = FooInt } // OK

A workaround proposed in SI-5700 consists of the following:

val quux: Quux { val foo: FooInt.type } = {
  class RefinedQuux(override val foo: FooInt.type) extends Quux(foo)
  new RefinedQuux(FooInt)
}

But I find it a bit cumbersome to use, and anyway this workaround is limited because we can not anymore mix a refined Quux in some other class or trait (ie. we can not write new Something with newQuux(…)).

Do you think it would be possible to refine traits type that have val constructor parameters according to the singleton type of the actual value being passed during the constructor invocation? That would make dependently-typed programming and existential types a lot more convenient.

@smarter
Copy link
Member

smarter commented May 18, 2016

  • Why should traits behave differently from classes here?
  • What's wrong with:
trait Quux[T <: Foo](val foo: T)

Since this seems to be what you're trying to express.

@julienrf
Copy link
Contributor Author

julienrf commented May 18, 2016

As a workaround, it seems that we can also use the same trick as for quux2: refining the Quux type in an anonymous subclass:

val quux: Quux { val foo: FooInt.type } = new Quux { val foo: FooInt.type = FooInt }

@julienrf
Copy link
Contributor Author

julienrf commented May 18, 2016

Why should traits behave differently from classes here?

You’re right, it should apply to class too. I only mentioned traits because that’s what I currently use in Scala to model existential types (because of SI-5700 and SI-5712).

Your version works of Quux works (it was also listed in the SI-5700 comments), but I remember to have had issues because T was not always inferred to the singleton type of the constructor parameter, or to a type as narrow as possible. And also, this approach does not scale:

trait OneMore[Q <: Quux[F], F <: Foo](val quux: Q)
trait AnotherOne[O <: OneMore[Q], Q <: Quux[F], F <: Foo](val oneMore: O)
…

Sometimes it is more convenient to work with type members rather than type parameters…

@smarter
Copy link
Member

smarter commented May 18, 2016

class Quux(val foo: Foo) means that Quux has a field foo with type Foo, we can't overriding the type of foo without subclassing Quux, and we don't want to create a new subclass for every constructor call, so I don't see how we could do what you propose.

@julienrf
Copy link
Contributor Author

julienrf commented May 19, 2016

I’m wondering if we can have something similar to what we currently have with dependent methods. Consider the following:

def f(foo: Foo): foo.Bar =

We define a method that takes a Foo as parameter. However, when we invoke f(FooInt) the compiler tells us that we get an Int, not an abstract foo.Bar.

And we can use that feature as follows:

object Quux {
  def apply(_foo: Foo): Quux { val foo: _foo.type } =
    new Quux { override val foo: _foo.type = foo }
}

So that Quux(FooInt) effectively returns a Quux { val foo: FooInt.type }.

It seems to me that it would be great to just write new Quux(FooInt) instead of having to define this apply method in the companion object.

@smarter
Copy link
Member

smarter commented May 19, 2016

It seems to me that it would be great to just write new Quux(FooInt) instead of having to define this apply method in the companion object.

I see how this could be useful, but inferring types which are "too precise" should not be the default because it can force the user to write a lot more type signatures, imagine:

val foo1 = new Foo { ... }
val foo2 = new Foo { ... }
var x = new Quux(foo1) // x is inferred to have type `Quux { val foo: foo1.type }`
x = new Quux(foo2) // error: foo2.type does match foo1.type
// To solve this, write "var x: Quux = ..."

For similar reasons Dotty does not infer union types (A | B), most of the time they're not needed and some of the time they lead to potentially confusing type errors which can only be solved by explicitly writing types.

@julienrf
Copy link
Contributor Author

OK, I understand your arguments.

@smarter
Copy link
Member

smarter commented May 19, 2016

I'm not saying we couldn't provide some nicer syntaxic sugar to do what you want, but you'll have to make a more detailed proposal :).

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

No branches or pull requests

2 participants