4

Looking at scalaz.Tree[A], it is invariant in A. I'm looking for a multi-way tree which I can dump values of a hierarchy in

E.g. if I have an ADT of

trait MyThing
case object Thing1 extends MyThing
case object Thing2 extends MyThing

And I want a Tree of MyThings, I cant use this without doing a cast to MyThing in scalaz;

import scalaz.{Scalaz, Tree}
import Scalaz._
val tree = Thing1.asInstanceOf[MyThing].
               node(Thing2.asInstanceOf[MyThing].leaf)

Which is a bit of a pain.

  • Is there are covariant [+A] version of Tree?
  • Why is Tree invariant in the first place?
NightWolf
  • 7,343
  • 9
  • 64
  • 115
  • 4
    As an aside, it's safer to use a type ascription `(Thing1: MyThing)` than it is a cast; if changes are made such that there is no longer a relationship between `Thing1` and `MyThing`, the case will compile but fail at runtime whereas the ascription will not even compile. – Hugh Feb 16 '15 at 03:11
  • https://github.com/scalaz/scalaz/pull/383 is the pull request to look at regarding the removal of variance annotations in Scalaz 7.1; in particular, see the linked PR #328. And I haven't check it's appropriate/possible, but the same technique as `scalaz.IList#widen` uses may be applicable here. – Hugh Feb 16 '15 at 03:22
  • 1
    I'd suggest tackling the problem from the opposite direction—using constructors for your ADT that are statically typed as `MyThing`. – Travis Brown Feb 16 '15 at 03:44
  • Thanks for the comments. @Huw - how could I use `widen` on `Tree`? I'm guessing pimping with widen ala IList style `def widen[B](implicit ev: A – NightWolf Feb 16 '15 at 03:51
  • Yeah, that's the sort of thing. Highly recommend Travis's suggestion! – Hugh Feb 16 '15 at 03:57
  • @TravisBrown How could I do this with a `case object`? – NightWolf Feb 16 '15 at 04:05
  • People often point to [Argonaut's `Json`](https://github.com/argonaut-io/argonaut/blob/v6.0.4/src/main/scala/argonaut/Json.scala#L468) as a good example of Scala ADT design that downplays subtyping. – Travis Brown Feb 16 '15 at 04:33

1 Answers1

2

First of all, I want to second Huw's recommendation that you use a type ascription instead of downcasting with asInstanceOf. As Huw says, using a type ascription will fail at compile time instead of runtime if something changes in your type hierarchy that makes the cast invalid. It's also just a good practice to avoid asInstanceOf for any upcasts. You can use asInstanceOf for either upcasting or downcasting, but only using it for downcasting makes it easy to identify unsafe casting in your code.

To answer your two questions—no, there's not a covariant tree type in Scalaz, for reasons discussed in detail in the pull request linked by Huw above. At first this may seem like a huge inconvenience, but the decision to avoid non-invariant structures in Scalaz is related to a similar design decision—avoiding the use of subtyping in ADTs—that makes invariant trees, etc. less painful.

In other languages with good support for ADTs (like Haskell and OCaml, for example), the leaves of an ADT aren't subtypes of the ADT type, and Scala's somewhat unusual subtyping-based implementation can make type inference messy. The following is a common example of this problem:

scala> List(1, 2, 3).foldLeft(None)((_, i) => Some(i))
<console>:14: error: type mismatch;
 found   : Some[Int]
 required: None.type
              List(1, 2, 3).foldLeft(None)((_, i) => Some(i))
                                                     ^

Because the type of the accumulator is inferred from the first argument to foldLeft, it ends up as None.type, which is pretty much useless. You have to provide a type ascription (or explicit type parameters for foldLeft), which can be pretty inconvenient.

Scalaz attempts to address this issue by promoting the use of ADT constructors that don't return the most specific subtype for the ADT leaf. For example, it includes none[A] and some[A](a: A) constructors for Option that return an Option[A].

(For more discussion of these issues, see my answer here and this related question).

In your case, implementing this approach could be as simple as writing the following:

val thing1: MyThing = Thing1
val thing2: MyThing = Thing2

Which allows you to write thing1.node(thing2.leaf). If you want to go further down this path, I'd highly recommend looking at Argonaut's Json ADT as a good example of ADT design that downplays the role of subtyping.

Community
  • 1
  • 1
Travis Brown
  • 135,682
  • 12
  • 352
  • 654