Understanding Mixed Context Bounds of Seq[AnyVal] and Seq[String]
Asked Answered
H

3

1

Suppose I have some function that should take a sequence of Ints or a sequence of Strings.

My attempt:

object Example extends App {
  import scala.util.Random
  val rand: Random.type = scala.util.Random

  // raw data
  val x = Seq(1, 2, 3, 4, 5).map(e => e + rand.nextDouble())
  val y = Seq("chc", "asas")

  def f1[T <: AnyVal](seq: Seq[T]) = {
    println(seq(0))
  }

  // this works fine as expected
  f1(x)
  // how can i combine
  f1(y)
}

How can I add this to also work with strings?

If I change the method signature to:

def f1[T <: AnyVal:String](seq: Seq[T])

But this won't work.

Is there a way to impose my required constraint on the types elegantly?

Henrique answered 13/6, 2020 at 19:26 Comment(5)
If they are 2 different types, then you should probably write 2 different functions for them. I can't see any properties that Int and String share, other than the plus operator, which isn't quite the same thing for both of themDichotomy
You want a typeclassPrecatory
Thanks - this function would essentially just pipe these two sequences into an API. The API can only accept AnyVal or String. A user will simply just pass the data in an array to the same function endpoint.Henrique
Thanks @LuisMiguelMejíaSuárez, that looks interesting, will take a read! Was not aware of this concept.Henrique
Thanks @MarioGalic, I did not know that - I will make sure not to put tags in the question in the future.Henrique
T
3

Note the difference between an upper bound

A <: C

and a context bound

A : C

so type parameter clause [T <: AnyVal : String] does not make much sense. Also types such as String are rarely (or never) used as context bounds.


Here is a typeclass approach

trait EitherStringOrAnyVal[T]

object EitherStringOrAnyVal {
  implicit val str: EitherStringOrAnyVal[String] = new EitherStringOrAnyVal[String] {}
  implicit def aval[T <: AnyVal]: EitherStringOrAnyVal[T] = new EitherStringOrAnyVal[T] {}
}

def f1[T: EitherStringOrAnyVal](seq: Seq[T]): Unit = {
  println(seq(0))
}

f1(Seq(1))       // ok
f1(Seq("a"))     // ok
f1(Seq(Seq(1)))  // nok

or generelized type constraints approach

object Foo {
  private def impl[T](seq: Seq[T]): Unit = {
    println(seq(0))
  }
  def f1[T](seq: Seq[T])(implicit ev: T =:= String): Unit = impl(seq)
  def f1[T <: AnyVal](seq: Seq[T]): Unit = impl(seq)
}

import Foo._

f1(Seq(1))       // ok
f1(Seq("a"))     // ok
f1(Seq(Seq(1)))  // nok
Tobar answered 13/6, 2020 at 19:44 Comment(0)
D
3

I feel like you should write a separate function for both, but there are other ways to do it:

In Dotty, you can just use union types, which is what I'd recommend here:

Edit: As per Alexey Romanov’s suggestion, replaced Seq[AnyVal | String] with Seq[AnyVal | String], which was wrong,

def foo(seq: Seq[AnyVal] | Seq[String]): Unit = {
  println(seq)
}

Scastie


If you're not using Dotty, you can still do this in Scala 2 with a couple reusable implicit defs


class Or[A, B]

implicit def orA[A, B](implicit ev: A): Or[A, B] = new Or
implicit def orB[A, B](implicit ev: B): Or[A, B] = new Or

def foo[T](seq: Seq[T])(implicit ev: Or[T <:< AnyVal, T =:= String]): Unit = 
  println(seq)

foo(Seq("baz", "waldo"))
foo(Seq(23, 34))
foo(Seq(List(), List())) //This last one fails

Scastie


You can also do it with a typeclass, as Luis Miguel Mejía Suárez suggested, but I wouldn't recommend it for such a trivial task since you still have to define 2 functions for each type, along with a trait, 2 implicit objects, and one function that can use instances of that trait. You probably don't want this pattern to handle just 2 different types.

sealed trait DoSomethingToIntOrString[T] {
  def doSomething(t: Seq[T]): Unit
}

implicit object DoSomethingToAnyVal extends DoSomethingToAnyValOrString[AnyVal] {
  def doSomething(is: Seq[AnyVal]): Unit = println(is)
}

implicit object DoSomethingToString extends DoSomethingToIntOrString[String] {
  def doSomething(ss: Seq[String]): Unit = println(ss)
}

def foo[T](t: Seq[T])(implicit dsis: DoSomethingToIntOrString[T]): Unit = {
  dsis.doSomething(t)
}

foo(Seq("foo", "bar"))
foo(Seq(1, 2))

In Scastie

Dichotomy answered 13/6, 2020 at 19:44 Comment(4)
ah, yes, but in that case, just use an Either[Int, String], which is Scala's way of representing sum typesInferential
@JoostPapendorp I suppose that's probably a more practical way to do it, but whatever :)Dichotomy
Thanks the second one looks very elegantHenrique
"a sequence of Ints or a sequence of Strings" is Seq[Int] | Seq[String], not Seq[Int | String]; the second allows e.g. Seq(0, "").Andante
T
3

Note the difference between an upper bound

A <: C

and a context bound

A : C

so type parameter clause [T <: AnyVal : String] does not make much sense. Also types such as String are rarely (or never) used as context bounds.


Here is a typeclass approach

trait EitherStringOrAnyVal[T]

object EitherStringOrAnyVal {
  implicit val str: EitherStringOrAnyVal[String] = new EitherStringOrAnyVal[String] {}
  implicit def aval[T <: AnyVal]: EitherStringOrAnyVal[T] = new EitherStringOrAnyVal[T] {}
}

def f1[T: EitherStringOrAnyVal](seq: Seq[T]): Unit = {
  println(seq(0))
}

f1(Seq(1))       // ok
f1(Seq("a"))     // ok
f1(Seq(Seq(1)))  // nok

or generelized type constraints approach

object Foo {
  private def impl[T](seq: Seq[T]): Unit = {
    println(seq(0))
  }
  def f1[T](seq: Seq[T])(implicit ev: T =:= String): Unit = impl(seq)
  def f1[T <: AnyVal](seq: Seq[T]): Unit = impl(seq)
}

import Foo._

f1(Seq(1))       // ok
f1(Seq("a"))     // ok
f1(Seq(Seq(1)))  // nok
Tobar answered 13/6, 2020 at 19:44 Comment(0)
I
1
def f1( seq: Seq[Any] ): Unit = 
  println( seq( 0 ) )

This works, because the least common supertype of Int and String (the 'lowest' type that is a supertype of both) would be Any, since Int is a subtype of AnyVal (i.e. 'the primitives) and String is a subtype of AnyRef (i.e. `the heap objects). f1 does not depends in any way on the contents of the list and is not polymorphic in its return type (Unit), so we don't need a type parameter at all.

Inferential answered 13/6, 2020 at 19:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.