Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to disambiguate case class creation with multiple parameter lists?

Tags:

scala

currying

I have a case class that looks about like this:

case class A(first: B*)(second: C*)

Both first and second are repeated, so I put the in separate parameter lists. However, I expect that second might be empty in a substantial number of cases, so being able to use the class like A(???, ???) without trailing empty parentheses would be nice. I tried the following:

case class A(first: B*)(second: C*) {
  def this(first: B*) = this(first: _*)()
}

Which gives me ambiguous reference to overloaded definition.

Is there a way to unambiguously write this constructor call? (and would I be able to call the overloaded constructor without cluttering up the syntax again?) My guess is no, with some arguments about how such syntactic sugar would break currying or some such, but I'd prefer to hear it from someone with more Scala knowledge than me ;)

like image 351
Silly Freak Avatar asked Feb 20 '15 18:02

Silly Freak


2 Answers

The following may accomplish your goal:

case class Foo private(first: List[Int], second: List[Int])

object Foo {
    def apply(first: Int*) = new Foo(first.toList, List.empty[Int]) { 
        def apply(second: Int*) = new Foo(first.toList, second.toList)
    }
}

And then you can do:

Foo(1, 2, 3)
Foo(1, 2, 3)(4, 5, 6)

Edit by @SillyFreak: This variant doesn't give a "reflective access of structural type member" warning, so I think it should be a little better performance-wise:

case class Foo private (first: List[Int], second: List[Int])

object Foo {
  def apply(first: Int*) = new Foo(first.toList, List.empty[Int]) with NoSecond

  trait NoSecond {
    self: Foo =>
    def apply(second: Int*) = new Foo(first.toList, second.toList)
  }
}

Foo(1, 2, 3)
Foo(1, 2, 3)(4, 5, 6)
like image 146
Ben Reich Avatar answered Oct 10 '22 01:10

Ben Reich


Edit Ben Reich has broken through the barrier and proved that this is, in fact possible. Here's a slight improvement over what he's got, that doesn't rely on the reflective calls feature:

case class Foo private(first: List[Int], second: List[Int]) {
  def apply(newSecond: Int*) = new Foo(first.toList, newSecond.toList)
}
object Foo {
  def apply(first: Int*) = new Foo(first.toList, List.empty[Int])
}

The only possible disadvantage versus his example is that you could keep calling a Foo object multiple times, whereas in his example, you can only do so for a Foo constructed with only first provided.


In some sense, I don't think the two options should clash. This is because you can't directly call a multiple parameter list method with only one of its parameter lists. Both parameter lists are an integral part of the method call, and technically, you have to use the post-fix _ operator to partially apply the first parameter list and get a function back that takes the second parameter list (taking a sequence, instead of varargs, you might note). This happens automagically in some cases, but not always. So the compiler should be able to tell between the two options simply by always assuming your executing a complete method call, unless you explicitly use _.

But overloading is a little iffy in Scala, often due to implementation details of the translation to JVM representation. Or maybe it's just because there's no syntactic difference between calling a method vs. a function, and the analysis that would permit this simply doesn't exist.

However, this doesn't work when you try to call the overloaded method. Here are several variations:

Overloaded apply method

scala> :paste
// Entering paste mode (ctrl-D to finish)

case class A(first: Int*)(second: Int*)
object A { def apply(first: Int*) = new A(first: _*)() }

// Exiting paste mode, now interpreting.

defined class A
defined object A

scala> A(1,2,3)
<console>:12: error: ambiguous reference to overloaded definition,
both method apply in object A of type (first: Int*)(second: Int*)A
and  method apply in object A of type (first: Int*)A
match argument types (Int,Int,Int)
              A(1,2,3)
              ^

Overloaded constructor

(original question's example)

Split constructor and apply

scala> :paste
// Entering paste mode (ctrl-D to finish)

class A(first: Int*)(second: Int*)
object A { def apply(first: Int*) = new A(first: _*)() }


// Exiting paste mode, now interpreting.

defined class A
defined object A

scala> A(5, 6, 7)
res5: A = A@47a36ea0

scala> A(5, 6, 7)(4, 5)
<console>:12: error: A does not take parameters
              A(5, 6, 7)(4, 5)
                        ^

scala> new A(5, 6, 7)(4, 5)
res7: A = A@62a75ec

scala> new A(5, 6, 7)
<console>:10: error: missing arguments for constructor A in class A
              new A(5, 6, 7)
              ^

So whether you try to get this overloaded behavior with apply or with the constructor, you're going to have the same issue with ambiguity. As you can see, making it a regular class (to not define the default apply) and splitting the methods works, but I'm pretty sure it doesn't achieve the elegance you're looking for.

like image 40
acjay Avatar answered Oct 10 '22 02:10

acjay