0

Is it possible in scala to have a collection of a union types. There are a few approaches to union types discussed here The top rated answer feels the most native, i have something like this:

sealed trait StringOrNumber[T]
object StringOrNumber {
    implicit object IntWitness extends StringOrNumber[Int]
    implicit object StringWitness extends StringOrNumber[String]
}

but when i try to make a map that contains both

val m: Map[String, Any] = Map("str" -> "hellp", "int" -> 32)

The scala compiler sees it as a map of [String,Any] Is there a way to tell the scala compiler this is a map [String, StringOrNumber]

Edit:

I dont think using the approach above is possible to create a collection of string or union. I think it needs to be another approach to a union type since the above is akin to overloaded methods rather than a true union type in the type system

Luke De Feo
  • 1,776
  • 2
  • 18
  • 35

6 Answers6

5

The closest emulation of runtime union types, you can do in the current version of Scala, is to wrap types of the union in case classes extending some sealed trait. It's boilerplate-y and adds an extra wrapper layer over AnyRef types, but it works, it's better than just using Any, and you can also add implicit conversions from union types:

sealed trait StringOrNumber
object StringOrNumber {
  case class IsNumber(i: Int) extends StringOrNumber
  case class IsString(s: String) extends StringOrNumber

  implicit def isNumber(i: Int): StringOrNumber = IsNumber(i)
  implicit def isString(s: String): StringOrNumber = IsString(s)
}

Now you can define your Map:

scala> val m: Map[String, StringOrNumber] = Map("str" -> "hellp", "int" -> 32)
m: Map[String,StringOrNumber] = Map(str -> IsString(hellp), int -> IsNumber(32))
Kolmar
  • 13,241
  • 1
  • 19
  • 24
  • This is such a disappointing aspect of Scala. For example, suppose I am handed 5 Enum types that are autogenerated from a code generator like Thrift. I want a function with a type parameter that must be one of those specific 5 Enum classes (all of which have the same methods). I can either omit the type parameter and add `match .. case` boilerplate everywhere, or else add the boilerplate to create a sealed trait with totally useless "FromEnum1", "FromEnum2", ...value constructor case class wrappers. Either way, Scala is oriented to increase boilerplate... – ely Aug 31 '18 at 12:55
3

Scala already has built-in case-classes, which are capable of representing arbitrary tagged disjoint unions of other types.

In your case, the simplest way to define StringOrNumber would be:

sealed trait StringOrNumber
case class Num(n: Int) extends StringOrNumber
case class Str(s: String) extends StringOrNumber

val m: Map[String, StringOrNumber] = Map(
  "str" -> Str("hellp"), 
  "int" -> Num(42)
)

for ((k, v) <- m) {
  v match {
    case Num(n) => println("It's an int: " + n)
    case Str(s) => println("A string: " + s)
  }
}

If you don't want to create an extra trait for that, and if you have only two types, just use Either:

type StringOrNum = Either[String, Int]
Andrey Tyukin
  • 38,712
  • 4
  • 38
  • 75
  • 1
    Ok yeah combined with implicits this seems to do the job – Luke De Feo Feb 19 '18 at 15:53
  • @LukeDeFeo If you really like the solution with implicit conversions, you should accept Kolmar's answer. However, I'd like to quote Welsh/Gurnell "Scala with Cats": *"implicit methods with non-implicit parameters form a [...] Scala pattern called an implicit conversion. This is an older programming pattern that is frowned upon in modern Scala code"*. I'm just saying: I did not advise you to use implicit conversions. – Andrey Tyukin Feb 19 '18 at 15:58
2

Part of answer you copied is not complete. There are another part with match. And it shows that such kind of types union works in runtime. So in general you mix two different things: compile time type union (which is also discussed in question you mentioned, originally written by Miles Sabin here) and which affects compiler checks, and runtime type check.

So, as soon as you use runtime approach, scala compiler just do not understand this union, and advice to use Any

Evgeny
  • 1,720
  • 10
  • 11
  • I hoped there would be a way to carry round the Abstract type of String or in and when i wanted to do something with it i could pattern match. It seems the only way to wrap in a case class – Luke De Feo Feb 19 '18 at 15:39
1

You should write

val m: Map[String, StringOrNumber[_]] = ...

This feature is now under development in Dotty. As I know it would be like

Class[T1 | T2] 

and

Class[T1 & T2] 

but dotty would be available next years. Now, you can use your approach, but it's a little tricky, and needs implicits. You can also try Either type (only if you'll have 2 generic types), and you can also pay attention to scalaz library. It's all about type-level programming.

Igor Yudnikov
  • 365
  • 1
  • 4
  • 13
  • I tried that and that also doesnt compile Error:(27, 62) type mismatch; found : String("hellp") required: StringOrNumber[_] val m: Map[String, StringOrNumber[_]] = Map("str" -> "hellp", "int" -> 32) – Luke De Feo Feb 19 '18 at 15:34
  • ill look into scalaz but it can be hard to get into as a beginner – Luke De Feo Feb 19 '18 at 15:35
0

Did you try:

val m: Map[String, StringOrNumber] = Map("str" -> "hellp", "int" -> 32)

You may also need to explicitly construct the StringOrNumber instances in this case to make it work.

Marcin
  • 44,601
  • 17
  • 110
  • 191
  • "StringOrNumber" are just tokens implicitly supplied by compiler. They are like entry tickets into the method `foo[T : StringOrNumber](...)`, they themselves are not Strings or Numbers. – Andrey Tyukin Feb 19 '18 at 15:23
  • Yeah I did the compiler wont accept it. On reflection, this approach is really only suited to a single method, it relies on the type parameter of method to achieve the union. I need something that can exist outside of a generic method – Luke De Feo Feb 19 '18 at 15:25
0

Let me introduce you to my solution, using contravariance and type constraints:

//Add this to your util library
trait Contra[-A]
type Union[A,B] = Contra[A] <:< Contra[B]

//And see a usage example below
@implicitNotFound("Only Int or String can be sized")
type Sizeable[T] = Union[T, Int with String]

def sizeOf[T: Sizeable](sizeable: T): Int = {
  sizeable match {
    case i: Int => i
    case s: String => s.length
  }
}

Problem with this solution is that extends of Int or String would not be accepted here. Values entered here are checked constrained to being "Contravariant" to Int with String.

There is a way around this tho, you have to circumvent the type inference, and supply the base class in the type parameter, like this:

sizeOf[String](someExtendOfString)
RoyB
  • 2,862
  • 1
  • 14
  • 31