c++scalatypesgeneric-programmingf-bounded-polymorphism

Scala variable number of parameters that are sub-classes of an F-Bounded type


In C++, I can create a variadic function template as follows:

#include <tuple>

// helper to loop over tuple
template <std::size_t I = 0, typename FuncT, typename... Args>
void for_each(std::tuple<Args...>& tuple, FuncT func) {
  func(std::get<I>(tuple));
  if constexpr (I + 1 < sizeof...(Args)) {
    for_each<I + 1, FuncT, Args...>(tuple, func);
  }
}

template <class A, class B, class Derived>
struct FBounded {
  auto foo() { return static_cast<Derived *>(this); }
  auto baz() { return static_cast<Derived *>(this); }
};

class Child1 : public FBounded<const char*, const char*, Child1> {};
class Child2 : public FBounded<bool, int, Child2> {};
class Child3 : public FBounded<double, const char*, Child3> {};

template <class... A, class... B, class... SubTypes>
static auto func(FBounded<A, B, SubTypes>... elems) {
  auto args = std::tuple(elems...);
  for_each(args, [](auto x) { x.foo()->baz(); });
}

int main() {
  auto c1 = Child1();
  auto c2 = Child2();
  auto c3 = Child3();

  func(c1, c2, c3);
}

I want to re-create this behavior in Scala. Here is what I have so far:

class FBounded[A, B, T <: FBounded[A, B, T]] {
  def foo(): T = this.asInstanceOf[T]
  def baz(): T = this.asInstanceOf[T]
}

class Child1 extends FBounded[Int, Double, Child1] {}
class Child2 extends FBounded[String, String, Child2] {}
class Child3 extends FBounded[Int, String, Child3] {}

def func(elems: Seq[FBounded[_, _, _]]) = {
    elems.foreach(_.foo.baz)
}


val c1 = new Child1()
val c2 = new Child2()
val c3 = new Child3()

func(c1, c2, c3)

I receive the error:

error: value baz is not a member of _$3
elems.foreach(_.foo.baz)
              ^

I believe this has something to do with when Scala fills out the placeholder types, but I am not sure.


Solution

  • The FBounded[_, _, _]-type is a shortcut for an existential type that looks somewhat like

    FBounded[A, B, T] forSome { type A; type B; type T <: FBounded[A, B, T] }
    

    and for whatever reason, the compiler refuses to infer the correct f-bounds for type parameter T (at least I couldn't get it to do that).

    I guess that this could be connected in some way to the reason why existential types are dropped in Dotty.

    Here is a workaround that simply avoids existential types altogether:

    class FBounded[A, B, T <: FBounded[A, B, T]] {
      self: T =>
      def foo: T = self
      def baz: T = self
      def wrap: FBE = new FBE {
        type a = A
        type b = B
        type t = T
        val value: t = self
      }
    }
    
    class Child1 extends FBounded[Int, Double, Child1] {}
    class Child2 extends FBounded[String, String, Child2] {}
    class Child3 extends FBounded[Int, String, Child3] {}
    
    /** Wrapper for FBounded existential types */
    abstract class FBE {
      type a
      type b
      type t <: FBounded[a, b, t]
      val value: t
    }
    
    def func(elems: FBE*) = {
      elems.map(_.value.foo.baz)
    }
    
    val c1 = new Child1()
    val c2 = new Child2()
    val c3 = new Child3()
    
    func(c1.wrap, c2.wrap, c3.wrap)
    

    Instead of relying on the existential FBounded[_, _, _], it uses a wrapper class FBE that holds a long lists with all types and all the constraints. With FBE, the func seems to work just fine:

    def func(elems: FBE*) = {
      elems.map(_.value.foo.baz)
    }
    

    because it can be written out more explicitly as:

    def funcMoreExplicit(elems: FBE*) = {
      elems.map(e => {
        val v: e.t = e.value
        val fooRes: e.t = v.foo
        val bazRes: e.t = fooRes.baz
        bazRes
      })
    }
    

    where we can use the explicit path dependent type e.t provided by FBE.t for intermediate results.