41

I just wanted to know if it is possible to iterate over a sealed trait in Scala? If not, why is it not possible? Since the trait is sealed it should be possible no?

What I want to do is something like that:

sealed trait ResizedImageKey {

  /**
   * Get the dimensions to use on the resized image associated with this key
   */
  def getDimension(originalDimension: Dimension): Dimension

}

case class Dimension(width: Int,  height: Int)

case object Large extends ResizedImageKey {
  def getDimension(originalDimension: Dimension) = Dimension(1000,1000)
}

case object Medium extends ResizedImageKey{
  def getDimension(originalDimension: Dimension) = Dimension(500,500)
}

case object Small extends ResizedImageKey{
  def getDimension(originalDimension: Dimension) = Dimension(100,100)
}

What I want can be done in Java by giving an implementation to the enum values. Is there an equivalent in Scala?

Schleichardt
  • 7,502
  • 1
  • 27
  • 37
Sebastien Lorber
  • 89,644
  • 67
  • 288
  • 419

6 Answers6

66

This is actually in my opinion an appropriate use case for 2.10 macros: you want access to information that you know the compiler has, but isn't exposing, and macros give you a (reasonably) easy way to peek inside. See my answer here for a related (but now slightly out-of-date) example, or just use something like this:

import language.experimental.macros
import scala.reflect.macros.Context

object SealedExample {
  def values[A]: Set[A] = macro values_impl[A]

  def values_impl[A: c.WeakTypeTag](c: Context) = {
    import c.universe._

    val symbol = weakTypeOf[A].typeSymbol

    if (!symbol.isClass) c.abort(
      c.enclosingPosition,
      "Can only enumerate values of a sealed trait or class."
    ) else if (!symbol.asClass.isSealed) c.abort(
      c.enclosingPosition,
      "Can only enumerate values of a sealed trait or class."
    ) else {
      val children = symbol.asClass.knownDirectSubclasses.toList

      if (!children.forall(_.isModuleClass)) c.abort(
        c.enclosingPosition,
        "All children must be objects."
      ) else c.Expr[Set[A]] {
        def sourceModuleRef(sym: Symbol) = Ident(
          sym.asInstanceOf[
            scala.reflect.internal.Symbols#Symbol
          ].sourceModule.asInstanceOf[Symbol]
        )

        Apply(
          Select(
            reify(Set).tree,
            newTermName("apply")
          ),
          children.map(sourceModuleRef(_))
        )
      }
    }
  }
}

Now we can write the following:

scala> val keys: Set[ResizedImageKey] = SealedExample.values[ResizedImageKey]
keys: Set[ResizedImageKey] = Set(Large, Medium, Small)

And this is all perfectly safe—you'll get a compile-time error if you ask for values of a type that isn't sealed, has non-object children, etc.

Community
  • 1
  • 1
Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • Nice :) It is not as easy as we do in Java but it will get the job done (when i'll use 2.10...) – Sebastien Lorber Dec 02 '12 at 19:47
  • 6
    Yes, Java's `enum` is arguably less of a train wreck than Scala's `Enumeration`, and is more convenient than sealed traits in this particular respect, but I'd still pick the Scala ADT-like approach over `enum` any day of the week. – Travis Brown Dec 02 '12 at 20:16
  • 5
    Actually there's a bug in this macro as highlighted by http://stackoverflow.com/questions/18732362/issue-with-using-macros-in-sbt. Its last lines should be replaced with https://gist.github.com/xeno-by/6573434. – Eugene Burmako Sep 15 '13 at 19:00
  • 3
    Fixed (finally—my apologies for taking so long to notice the comment). Thanks, Eugene! – Travis Brown Mar 11 '14 at 22:09
  • I'm fairly sure the fix didn't need to be so complex. You can just use children.map(c => Ident(c.asClass.module)) – Chris Mar 12 '14 at 16:57
  • 2
    This macro doesn't work for me. I use scala 2.11.2 SealedExample.values[Rank] I get: type mismatch; found : scala.collection.immutable.Set[Product with Serializable with Rank] required: Set[Rank] Note: Product with Serializable with Rank <: Rank, but trait Set is invariant in type A. You may wish to investigate a wildcard type such as `_ <: Rank`. (SLS 3.2.10) sheet.sc /playground/src line 8 Scala Problem – Anton Kuzmin Oct 04 '14 at 06:47
  • 8
    With approval from @TravisBrown , I've just packed this answer into a tiny library and published to Bintray. Source: https://github.com/mrvisser/sealerate Bintray: https://bintray.com/pellucid/maven/sealerate/view/general – mrvisser Jan 20 '15 at 14:09
  • I know it's been a while since your answer, but is there a possibility to get them in definition order? eg. List(Large, Medium, Small). Changing the result type to List does not cut it. – Ákos Vandra-Meyer Sep 22 '15 at 15:59
  • @ÁkosVandra my lib mentioned [below](http://stackoverflow.com/a/28569939/1814775) supports returning the implementations in definition order. – lloydmeta Apr 23 '16 at 06:59
  • Very very curious about the `sym.asInstanceOf[ scala.reflect.internal.Symbols#Symbol ].sourceModule.asInstanceOf[Symbol]` part? where is it documented? – caeus Sep 29 '20 at 14:02
  • @AlejandroNavas This was a long time ago but given the `internal` part I'm fairly confident that the answer to that question is "nowhere". – Travis Brown Sep 29 '20 at 17:13
  • @TravisBrown I guess `sym.asInstanceOf[scala.reflect.internal.Symbols#Symbol].sourceModule.asInstanceOf[Symbol]` can be replaced with `sym.owner.info.decl(sym.name.toTermName)`. – Dmytro Mitin Sep 30 '20 at 20:06
8

The above mentioned solution based on Scala Macros works great. However it does not cases like :

sealed trait ImageSize                            
object ImageSize {                                
    case object Small extends ImageSize             
    case object Medium extends ImageSize            
    case object Large extends ImageSize             
    val values = SealedTraitValues.values[ImageSize]
}                                                 

To allow this, one can use this code:

import language.experimental.macros
import scala.reflect.macros.Context

object SealedExample {
    def values[A]: Set[A] = macro values_impl[A]

    def values_impl[A: c.WeakTypeTag](c: Context) = {
        import c.universe._

        val symbol = weakTypeOf[A].typeSymbol

        if (!symbol.isClass) c.abort(
            c.enclosingPosition,
            "Can only enumerate values of a sealed trait or class."
        ) else if (!symbol.asClass.isSealed) c.abort(
            c.enclosingPosition,
            "Can only enumerate values of a sealed trait or class."
        ) else {
            val siblingSubclasses: List[Symbol] = scala.util.Try {
                val enclosingModule = c.enclosingClass.asInstanceOf[ModuleDef]
                enclosingModule.impl.body.filter { x =>
                    scala.util.Try(x.symbol.asModule.moduleClass.asClass.baseClasses.contains(symbol))
                        .getOrElse(false)
                }.map(_.symbol)
            } getOrElse {
                Nil
            }

            val children = symbol.asClass.knownDirectSubclasses.toList ::: siblingSubclasses
            if (!children.forall(x => x.isModuleClass || x.isModule)) c.abort(
                c.enclosingPosition,
                "All children must be objects."
            ) else c.Expr[Set[A]] {
                def sourceModuleRef(sym: Symbol) = Ident(
                    if (sym.isModule) sym else
                        sym.asInstanceOf[
                            scala.reflect.internal.Symbols#Symbol
                            ].sourceModule.asInstanceOf[Symbol]
                )

                Apply(
                    Select(
                        reify(Set).tree,
                        newTermName("apply")
                    ),
                    children.map(sourceModuleRef(_))
                )
            }
        }
    }
}
user673551
  • 101
  • 1
  • 4
5

Take a look at @TravisBrown's question As of shapeless 2.1.0-SNAPSHOT the code posted in his question works and produces a Set of the enumerated ADT elements which can then be traversed. I will recap his solution here for ease of reference (fetchAll is sort of mine :-))

import shapeless._

  trait AllSingletons[A, C <: Coproduct] {
    def values: List[A]
  }

  object AllSingletons {
    implicit def cnilSingletons[A]: AllSingletons[A, CNil] =
      new AllSingletons[A, CNil] {
        def values = Nil
      }

    implicit def coproductSingletons[A, H <: A, T <: Coproduct](implicit
                                                                tsc: AllSingletons[A, T],
                                                                witness: Witness.Aux[H]
                                                               ): AllSingletons[A, H :+: T] =
      new AllSingletons[A, H :+: T] {
        def values: List[A] = witness.value :: tsc.values
      }
  }

  trait EnumerableAdt[A] {
    def values: Set[A]
  }

  object EnumerableAdt {
    implicit def fromAllSingletons[A, C <: Coproduct](implicit
                                                      gen: Generic.Aux[A, C],
                                                      singletons: AllSingletons[A, C]
                                                     ): EnumerableAdt[A] =
      new EnumerableAdt[A] {
        def values: Set[A] = singletons.values.toSet
      }
  }

  def fetchAll[T](implicit ev: EnumerableAdt[T]):Set[T] = ev.values
Community
  • 1
  • 1
Yaneeve
  • 4,751
  • 10
  • 49
  • 87
  • I would like to add to your answer that we don't need to implement `EnumerableAdt[T]` for some trait `T` using it implicitly. Everything will work only due to macros generation. I think it's unobvious here. – Boris Azanov Feb 21 '22 at 11:46
3

There's no capability for this natively. It wouldn't make sense in the more common case, where instead of case objects you had actual classes as subclass of your sealed trait. It looks like your case might be better handled by an enumeration

object ResizedImageKey extends Enumeration {
  type ResizedImageKey = Value
  val Small, Medium, Large = Value
  def getDimension(value:ResizedImageKey):Dimension = 
      value match{
         case Small => Dimension(100, 100)
         case Medium => Dimension(500, 500)
         case Large => Dimension(1000, 1000)

}

println(ResizedImageKey.values.mkString(",") //prints Small,Medium,Large

Alternatively, you could create an enumeration on your own, possibly placing it in the companion object for convenience

object ResizedImageKey{
  val values = Vector(Small, Medium, Large)
}

println(ResizedImageKey.values.mkString(",") //prints Small,Medium,Large
Dave Griffith
  • 20,435
  • 3
  • 55
  • 76
1

See this answer in another thread. The Lloydmetas Enumeratum library provides java Enum like features in an easily available package with relatively little boilerplate.

Community
  • 1
  • 1
Peter Lamberg
  • 8,151
  • 3
  • 55
  • 69
0

Something that can also solve the problem is the possibility to add an implicit convertion to add methods to the enum, instead of iteraring over the sealed trait.

object SharingPermission extends Enumeration {
  val READ = Value("READ")
  val WRITE = Value("WRITE")
  val MANAGE = Value("MANAGE")
}


/**
 * Permits to extend the enum definition and provide a mapping betweet SharingPermission and ActionType
 * @param permission
 */
class SharingPermissionExtended(permission: SharingPermission.Value) {

  val allowRead: Boolean = permission match {
    case SharingPermission.READ => true
    case SharingPermission.WRITE => true
    case SharingPermission.MANAGE => true
  }
  val allowWrite: Boolean = permission match {
    case SharingPermission.READ => false
    case SharingPermission.WRITE => true
    case SharingPermission.MANAGE => true
  }
  val allowManage: Boolean = permission match {
    case SharingPermission.READ => false
    case SharingPermission.WRITE => false
    case SharingPermission.MANAGE => true
  }

  def allowAction(actionType: ActionType.Value): Boolean = actionType match {
    case ActionType.READ => allowRead
    case ActionType.WRITE => allowWrite
    case ActionType.MANAGE => allowManage
  }

}

object SharingPermissionExtended {
  implicit def conversion(perm: SharingPermission.Value): SharingPermissionExtended = new SharingPermissionExtended(perm)
}
Sebastien Lorber
  • 89,644
  • 67
  • 288
  • 419