Skip to content

Top-level definitions using opaque types can bypass their opacity #8175

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
ryanberckmans opened this issue Feb 3, 2020 · 4 comments · Fixed by #8201
Closed

Top-level definitions using opaque types can bypass their opacity #8175

ryanberckmans opened this issue Feb 3, 2020 · 4 comments · Fixed by #8201

Comments

@ryanberckmans
Copy link

minimized code

https://scastie.scala-lang.org/60nHfRoiRVONgdxGJpUjZA

opaque type Foo = Int
object Foo
  def apply(i: Int): Foo = i

opaque type Bar = Int
object Bar
  def apply(i: Int): Bar = i

// ******************************
// Bug example #1, Bar incorrectly acts as subtype of Foo
// when used in a top-level definition.
object FooToString
  // Next line gives a compilation error as expected: "Found: Bar => String, Required: Foo => String"
  //val fooToString: Foo => String = (b: Bar) => "hi from Bar function"  

// This line is identical to the previous line, but it's a
// top-level definition that compiles and is used below.
val fooToString: Foo => String = (b: Bar) => "hi from Bar function" // BUG

// ******************************
// Bug example #2, same as #1 but demonstrated using
// a typeclass instance top-level definition.
trait Show[A]
  def show(a: A): String
object Show
  // Next line gives a compilation error as expected: "Found: Bar => String, Required: Show[Foo]"
  //given Show[Foo] = (f: Bar) => "hi from Show[Foo]"

// This line is identical to the previous line, but it compiles,
// creating an instance of Show[Foo] that's used below.
given Show[Foo] = (f: Bar) => "hi from Show[Foo]" // BUG

def show[A: Show](a: A): String = summon[Show[A]].show(a)

object Main extends App
  println(show(Foo(5)))
  println(fooToString(Foo(17)))

expectation

As shown in the Scastie snippet, this definition

val fooToString: Foo => String = (b: Bar) => "hi from Bar function"

Is expected to fail compilation with the error Found: Bar => String, Required: Foo => String but instead compilation succeeds if the definition is top-level.

@odersky
Copy link
Contributor

odersky commented Feb 4, 2020

In fact, opaque types are transparent in the scope they appear, not just in their companion object.
For toplevel opaques this means that the definition is visible in all other toplevel definitions of the same file, since these go into one package object together. Objects or classes in the same file do not get wrapped in the object and are consequently our of the scope where an opaque alias is visible.

I have added a section to the docs that explains this.

odersky added a commit to dotty-staging/dotty that referenced this issue Feb 5, 2020
@ryanberckmans
Copy link
Author

opaque types are transparent in the scope they appear, not just in their companion object

Got it.

Might we consider a mechanism to make this property known to users at compile-time? Something like

Warning W037:
at 12: given Show[Foo] = (f: Bar) => "hi from Show[Foo]"
                       ^
  The value of type `Bar => String`
      (f: Bar) => "hi from Show[Foo]"
  is assignable to
      given Show[Foo]
  because "opaque type Bar = Long <: opaque type Foo = Long"
  and opaque types are transparent in the scope
  they are defined, not just their companion objects.

Learn  more at www.allaboutopaquetypes.com/W037.
Disable this warning with --stop-whining-about-opaque-types

@som-snytt
Copy link
Contributor

TIL I don't understand what "scope" means. I thought it meant literally what is "seen".

So I would have said that this disposition is at odds with #7891

But scope does not mean the same thing as telescope or kaleidoscope. It means a target to aim at (skopos). The figurative sense is that the scope of one's power includes all the targets one can hit.

Shakespeare: "Desiring this mans art, and that mans skope." (Sonnet 29, in which he is depressed about his prospects, then gets a mood adjustment from his lover.)

I would still prefer that scope is what I can tell by looking.

Except, of course, for implicit scope.

"Opaque" means obscure because it's in shadow. It's too bad we can't use opaque for shadowed symbols. The sense of "not translucent" comes afterward, from optical science.

OED has another nice line from Spenser: "slipper hope Of mortal men, that swincke and sweate for nought, And shooting wide, doe misse the marked scope." There is an old word "swinch" for "toil", which someone may want to use for their latest Scala project.

@odersky
Copy link
Contributor

odersky commented Feb 6, 2020

@ryanberckmans In my experience it is really hard to do something like what you propose without producing false negatives or annoying warnings.

odersky added a commit that referenced this issue Feb 6, 2020
Fix #8175: Explain transparency of toplevel opaque types
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants