Path-dependent types in case class–like classes ... with less boilerplate?

265 views
Skip to first unread message

Julian Michael

unread,
Nov 15, 2016, 1:41:12 AM11/15/16
to scala-user
Hi scala-user,

I've found myself using a lot of path-dependent types for dependent-pair-like data structures, but it seems to require a lot of boilerplate. I'm interested to see if there's a more concise method.

For example, I have a type representing a type family (with upper bound):

trait ValueType {
  type getType <: Value
}

trait Value { ... }
// Value subclasses defined elsewhere

And I want to represent a "typed value" as a small data structure. Ideally this would just be a case class like:

case class ValueTerm(valType: ValueType, term: valType.getType)

Of course, this doesn't work because the path-dependent type would need to appear in a subsequent parameter list; but, path-dependent types in constructors aren't supported anyway.
So I ended up doing this:

sealed trait ValueTerm {
  val valType: ValueType
  val term: valType.getType
}
object ValueTerm {
  private[this] case class ValueTermImpl[V <: Value](
    override val valType: ValueType { type getType = V },
    override val term: V
  ) extends ValueTerm
  def apply[V <: Value](valType: ValueType { type getType = V }, term: V): ValueTerm =
    ValueTermImpl(valType, term)
  def unapply(vt: ValueTerm): Some[(ValueType { type getType = vt.valType.getType }, vt.valType.getType)] =
    Some((vt.valType: ValueType { type getType = vt.valType.getType }, vt.term))
}

Which works. It's manageable (indeed, I've been using it for a few weeks now), and don't get me wrong—I'm super thankful I have the capability to represent this at all.
But it's still 90% boilerplate. And for any data structures that use ValueTerm in an interesting way (for example, applying a typed predicate to the term under the constraints that their types match up),
I have to write similar-looking boilerplate. This is a pain when maintaining the code, though benefits of type safety still way outweigh the costs here IMO.
On top of that, I'm going to have to explain my code to non–Scala programmers. It'd be really nice if I didn't have to justify a bunch of boilerplate for what are pretty simple ideas.
And a solution using existential types similar in form to ValueTermImpl[_] of the above aren't acceptable—I need the increased flexibility of type members and I would rather not have to juggle both type parameters and type members around at once: that would introduce more boilerplate into client code which is worse.

Any ideas of how to accomplish this? Or maybe, is this sort of thing in/going to be in dotty?

Thanks,
Julian

Jasper-M

unread,
Nov 15, 2016, 4:48:22 AM11/15/16
to scala-user
This seems so do the trick more or less:

case class ValueTerm[T <: ValueType, V <: T#getType](valType: T, term: V)

But I might be missing something. And then you do need 2 type parameters... Although I'm pretty sure you'd need at least one, even if you could use `valtype.getType`.


Kind regards,
Jasper

Op dinsdag 15 november 2016 07:41:12 UTC+1 schreef Julian Michael:

Julian Michael

unread,
Nov 15, 2016, 2:37:22 PM11/15/16
to scala-user


This seems so do the trick more or less:

case class ValueTerm[T <: ValueType, V <: T#getType](valType: T, term: V)

Thanks Jasper! Great suggestion. Reading it at first I thought we would lose some path-dependence on the valType parameter since we're accessing getType with #.
But it seems to me that in order to ever construct such a ValueTerm you would need a specific-enough type on valType to give you back that information anyway.
For example, if I have a vt: ValueType and a term: vt.getType,
I can use this constructor with the type parameter T = ValueType { type getType = vt.getType }, which is a correct more-specific type for vt.
Indeed, I tried this and the correct type parameters were inferred by the compiler. Nice!

At the end of the day, though, I really need the extra flexibility of type members and would rather not juggle the type parameters around.
So it seems the simplest thing I could do would be to use this case class to extend a trait that has those type members—but that gives me two different names
for only one type that I care about, which is more confusing—again, I'd like case class–like behavior.
I suspect that since we can't have path-dependent types in constructors, my current approach is the best I'll get.
Maybe your case class definition for the -Impl class can make it slightly more concise though.

Anyway though, for situations where type parameters suffice, I'll use your suggestion.

Thanks!
Julian

Alec Zorab

unread,
Nov 15, 2016, 3:02:06 PM11/15/16
to Julian Michael, scala-user

If you're willing to give up the case class-ness, you can have multiple parameter blocks, which allows you to refer back.


--
You received this message because you are subscribed to the Google Groups "scala-user" group.
To unsubscribe from this group and stop receiving emails from it, send an email to scala-user+...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Julian Michael

unread,
Nov 15, 2016, 3:11:18 PM11/15/16
to scala-user, julianjo...@gmail.com
Thanks Alec—actually, when I try that:

scala> class ValueTerm2(val valType: ValueType)(val term: valType.getType)
<console>:13: error: not found: value valType
       class ValueTerm2(val valType: ValueType)(val term: valType.getType)
                                                          ^
It doesn't work; see SI-5712. If it did, using such a class with a custom equals, apply, and unapply would probably be the best solution.

On another note, it seems like this boilerplate wouldn't be too hard to generate automatically. I don't know much about how Scala macros work, but is there any reason it a macro wouldn't be possible that simulates case classes with parameters whose types depend on other parameters?

Matthew Pocock

unread,
Nov 16, 2016, 8:39:21 AM11/16/16
to Julian Michael, scala-user

Hi Julian,

Sometimes you can work around these sorts of things by introducing an extra type (in the companion object) that flips between the extra type as a type member and as a type parameter. There are good examples of this in shapeless. Look for Aux types in that code Base. I've used that trick in the Eval class of my shortbol project on github. On my phone so can't post code examples.

Matthew


To unsubscribe from this group and stop receiving emails from it, send an email to scala-user+unsubscribe@googlegroups.com.

Julian Michael

unread,
Nov 16, 2016, 3:32:04 PM11/16/16
to Matthew Pocock, scala-user
Matthew,

That's a great point. I have used the Aux pattern but didn't think about how it relates to my problem.

It seems to me that I can think of the -Impl class's type as an Aux type in the Aux pattern. But at the end of the day the Aux pattern is useful where type refinements won't work but type parameters will; my understanding from using shapeless is that the primary use case here is when we have dependencies between implicit parameters. My data structures aren't so much meant to serve as implicits so I feel like the Aux pattern may not be of much use to me. My difficulty is somewhat the opposite, that I have something I can only construct using type parameters, but I want to use it only with type members. And the boilerplate I'm complaining about is essentially just the plumbing that's necessary to do that. And it requires a lot more plumbing than the Aux pattern because I can't just use a type alias to remove type parameters the way I can easily use it to add them.

I've concluded that there are two main barriers to doing this nicely, each sufficient to cause the problem.
1. SI-5172—constructors don't support path-dependent types.
2. Case classes don't care about subsequent parameter lists.

If (1) was resolved (which it may be soon!) then you could imagine:

case class ValueTerm(valType: ValueType)(val term: valType.getType)

But this isn't what I want, because its equals, hashCode, and unapply will ignore the second parameter list. (apply will be fine though.) I could instead make it a normal class and define those methods myself—but actually I think my original approach is still less error prone and less hassle than having to define my own equals and hashCode every time I want one of these.

So it seems to me that solving this nicely would require pretty big changes to the language (i.e., changing what case class means or allowing path-dependence within the same parameter list). But I feel like the ability to easily construct a "dependent pair" is a compelling reason. Any ideas on if this is an ideal sort of thing for Scala 3?

On that note, are there compelling reasons for restricting path-dependence to previous parameter lists? Why not just require them to appear in order in a single parameter list?

Thanks,
Julian
Reply all
Reply to author
Forward
0 new messages