Synthesize constructor for opaque types

At my company, we use https://github.com/estatico/scala-newtype extensively.
We thought opaque types would be the language support we needed to get rid of this macro-based solution, but the boiler plate looks likely to be too much.

To emulate newtype, we need the following mouthful:

opaque type Name = String
object Name {
    def apply(x:String):Name = x
}

Seeing as we have hundreds of these in each code base, this will become tiresome.

I would like for the compiler to synthesize this, maybe based on a keyword or some kind of hint. What do people think about this?

There was a short discussion on the dotty gitter here

11 Likes

how do you extract the String from the Name in an external scope?

I’d say something like this:

opaque type Name = String
object Name {
  def apply(s:String): Name = s
  extension (n:Name)
    def value: String = n
}

Seems like opaque types already have the apply method by default:

But only available in the same scope, right?

Yes only in the same scope. That’s confusing …

I also encountered the same problem. I have literally hundreds of of “newtypes”.
Although I use some homebrewed solution, which also defines a typeclass for every such newtype so that later on you can write some very clever, good, and ultimately safe code related to these “newtypes” at the boundaries of your application.

Unfortunately giving such a default implementation for the typeclass with no boilerplate will be impossible now, and it’s also not possible to write a macro that does this, since there’s no way to declare that “this macro is useable only in the context of the opaque type definition, and has access to it”.

Example of this homebrewed solution. It saves literally thousands of lines of boilerplate down the line, not only at the definition of the opaque types, but also in the derivation of typeclasses like json codecs, DB driver codecs, etc.

Looks like it’s just some magical incantation and there’s no actual apply method. Probably just syntactic sugar for "Hello World" : Name.

I think the assumption was that most of the time you want to do some transformations or checks before lifting the value into the opaque type. Like PositiveInt, NonEmptyString, Logarithm. Maybe there should be a opaque case type construct or something like that, to indicate that it’s basically a case class without the boxing.

From a user point of view, opaque types replace value classes which I use currently for type safety without boxing. E. g: when passing arguments to functions.

I would love to have a default apply method but opaque case type looks really nice if we can have that instead.

1 Like

Edit: this only works because String has its own copy constructor from another String

4 Likes

I note that all issues in this thread would be solved if support for macro annotations (or something similar) was ported to Dotty. It would avoid having to make the language a kitchen-sink of everyone’s feature wishlist.

8 Likes

For reference, using opaque type in place of new type was also one of my expected use case (the main one), and the boilerplate is likely to kill it, as explained, especially since it seems hard to be able to write macro to take care of it.

3 Likes

I’d be nice if you could get apply/unapply “for free” for the common case. But in my view it would have to require a keyword, because in many cases you do not want an unguarded constructor or deconstructor for an opaque type.

Maybe something like:

case opaque type Name = String

But can wait till after 3.0 IMO.

3 Likes

As I suggested on gitter, I think this is better to have them always be available, but optionally declared private like private constructors for classes, so it would be:

opaque type Name private = String
1 Like

Agreed, automatic constructor / deconstructor in all case breaks the idea of newtype a bit :slight_smile:

  1. You can write def Name(x: String): Name = x, it’s not terribly long.
  2. An opaque type member is still just a type member, not a class. If we start adding more class-like features to it, where does it end? Eventually we’re just back to existing value classes with their shortcomings. If you want something that behaves like a class, use a class. If you’re uncomfortable with the allocation cost of a class (it’s really not a big deal most of the time), you’ll have to wait for proper value types on the JVM: https://openjdk.java.net/projects/valhalla/.
8 Likes

Agree with this. I guess people have the annotation fear from Java.

1 Like

I have strong opinions about your 2nd argument.

The whole use case for opaque types (IMHO) is to have a compiler enforced alias that doesn’t suffer the performance penalties of e.g. case classes.

The features I want are:

  1. No subtype relationship with the original or any other type
  2. Possibility to coerce typeclass instances from the underlying type on a per typeclass basis (see coercible trick even though it’s not recommended in general). This allows me to use my special opaque type Name = String in the context of e.g. doobie, circe, etc. without extra work.
  3. Ability to construct the opaque type outside of scope in order to use said type in e.g. interfaces.

I agree that case classes gives me most of these features, but that leaves little to no use cases in my own code for opaque types and none of the performance benefits.

2 Likes

There has been quite a long discussion about the syntax of opaques in the original proposal topic. I expressed strong opinions against the proposed syntax in favor of combination of these two syntax constructs:

// #1 for adding extension methods to no-runtime-overhead types
opaque Permission(i: Int) {
  def |(other: Permission): Permission = i | other.i
}
object Permission {
  val NoPermission =  Permission(0)
}

// #2 for defining type aliases that cannot be mixed in (`nominal` might not be the right keyword)
nominal type Password = String
nominal type GUID = String

I’d rather not repeat the discussion as I believe I’ve made my points clear in the previous one. Here’s the gist of it.

2 Likes

I ran into the supertagged library recently that uses an encoding for newtypes that is at least as good as opaque, and in some ways better (particularly when used alongside top-level type definitions).

It might be a useful thing to see if this encoding can be used to improve opaque's rough edges.

2 Likes