0

A quite typical use case: an object (or class) declares several public vals of related types, and it would like to declare an accessor returning a collection containing all of them:

case class Ball(dia :Int)
object Balls {
    val tennis = Ball(7)
    val football = Ball(22)
    val basketball = Ball(24)
    val balls = Seq(tennis, football, basketball)
}

This example obviously violates DRY and is error prone. It can be quite easily solved using mutable state (such as adding an implicit Builder[Ball, Seq[Ball]] parameter to the Ball constructor. That solution however isn't without issues, either. Particularily once we try to generalize the solution and/or have a class hierarchy where every class declares some values, the moment when we should switch from mutable partial summary to final immutable value is not clear.

More as an intellectual exercise and out of curiosity I was trying to come up with a purely functional variant, without much success. The best I came up with is

object Balls {
    import shapeless.{::, HNil}

    val (balls @
            tennis ::football::basketball::HNil
        ) =
            Ball(7)::Ball(22)::Ball(24)::HNil
}

Which is quite neat, but becomes unmanagable once the number of balls or their initializers aren't tiny. A working alternative would be to change everything into a HMap, but I generally try to avoid shapeless dependencies in public API. It seems like it could be perhaps doable with scala continuations, but I have no idea how to make the declarations non-local to the reset block.

EDIT: What I haven't stressed before, and the reason why scala.Enumeration doesn't do the job for me, is that in the real case the objects aren't identical, but are in fact composite structures, which constructors take several lines or more. So, while the final type may be the same (or at least I'm not interested in the details), it isn't a simple enumeration, and for readability reasons it is important that the name of the declared member/key can be easily tied visually with its definition. So my shapeless solution here, as well as the shapelessless Seq-based proposed, are very susceptible to off-by-one errors, where a modification is made to the wrong value, by mistaking its real identifier.

Of course, the real case is currently implemented similarly to scala.Enumeration, by maintaining a sequence of values produced by the inherited constructor method. It suffers however from all problems that Enumeration does, and amplifies the probability of errors from calling the constructor outside of actual object Balls initializer, or discarding a value in a conditional block. Besides, I was quite interested how this is solved in purely functional languages.

Any ideas how to have a cake and eat it?

Turin
  • 2,208
  • 15
  • 23

2 Answers2

1

Not sure what you look for, but As per what you tried using Shapeless, I believe you can achieve it without it, and quite similar of what you just did:

case class Ball(dia :Int)
object Balls {
    val balls@Seq( tennis, football, basketball ): Seq[Ball] = Ball(7)::Ball(22)::Ball(24)::Nil
}

On the other side, as answered already this would be a kind of of enumeration, which you could actually use here.

EDIT

Have you considered to model the data in a more descriptive way. Let´s see:

sealed trait Balls {
    def dia: Int
}
case object Football { val dia: Int = 22 }
case object Tennis { val dia: Int = 7 }
case object Basketball { val dia: Int = 24 }

object Balls {
    val values: Seq[Ball] = Football :: Tennis :: Basketball :: Nil
}

Advantages of an approach like this, will be the use of pattern matching, you will still pattern matched the Ball to extract the diameter while being able to refine your pattern matching to the subtypes.

def kickBall( in: Ball ): Boolean = {
    in match {
        case f: Football => 
            true
        case b: Basketball =>
            // You shouldn't do this
            false
        case _ =>
            // Anything else
            false
    }
}

Using sealed will force you to define all the types on the same file and the compiler will let you know when you forget cases on a pattern matching.

Still having some boilerplate, but it is a typical approach to model your solutions in a Functional Way.

FerranJr
  • 332
  • 1
  • 8
  • Haven't thought about it, but it has one huge disadvantage compared to the shapeless variant: the compiler doesn't compare the lengths of the sequences, so with long lists this quickly becomes unmanagable.. – Turin Oct 05 '16 at 19:44
0

Not a functional solution, so not a proper answer to my question, but the minor issue of 'losing' values created by the constructor method responsible also for their collection can be addressed by moving the collector to an unapply method:

class Collector[T] {
    private[this] var seq :Seq[T]=Nil
    def items = seq
    def unapply(item :T) = synchronized { seq = item+:seq; Some(item) }
}

class Ball private (val dia :Int)

object Ball {
    val Ball = new Collector[Ball]
    implicit private def ball(dia :Int) = new Ball(dia)

    val Ball(basket) = 24
    val Ball(tennis) = 7
    val Ball(football) = 22
}

While I prefer this solution syntactically, I don't think the benefit is large enough to offset the confusion factor comparing to the simplest factory method.

Turin
  • 2,208
  • 15
  • 23