Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting partial constructors for case classes "for free"

Consider an abstract class defining two properties

abstract class A {
  def a: Int
  def b: Int
  // real A has additional members
}

which is the base class for various case classes such as

case class Foo(a: Int, b: Int) extends A
case class Bar(a: Int, b: Int) extends A
// and many more

Goal: I would finally like to be able to create instances of the aforementioned case classes in two ways, namely

val b1 = Bar(1, 2)
val b2 = Bar(1) has 2
assert(b1 == b2) // must hold

Approach: It therefore seems reasonable to define a helper class that defines has and that allows me to partially construct As

case class PartialA(f: Int => A) {
  def has(b: Int) = f(b)
}

Problem: The current machinery doesn't allow for calls like Bar(1) because this is actually an invocation of Bar.apply(1), that is, of the method apply as defined by the compiler-generated object Bar.

It would be great if I could force the compiler to generate the Bar object as object Bar extends PartialAConstructor, where

abstract class PartialAConstructor{
  def apply(a: Int, b: Int): A // abstract, created when the compiler creates
                               // object Bar
  def apply(a: Int) = PartialA((b: Int) => apply(a, b))
}

However, it doesn't seem to be possible to influence the generation of companion objects of case classes.


Desired properties:

  • Case classes: Foo, Bar etc. should remain case classes because I would like to use the compiler-generated goodies such as structural equality, copy and automatically generated extractors.

  • "Full" structural equality: Defining the case classes as

    case class Bar(a: Int)(val b: Int)
    

    is not an option, because the compiler-generated equals method only considers the first list of arguments, and thus the following would hold erroneously:

    assert(Foo(1)(0) == Foo(1)(10))
    
  • As little code repetition as possible: For example, it is of course possible to define a

    def Bar(a: Int) = PartialA((b: Int) => Bar(a, b))
    

    but that would have to be done for every case class extending A, that, is Foo, Bar etc.

like image 500
Malte Schwerhoff Avatar asked Feb 05 '13 13:02

Malte Schwerhoff


3 Answers

You could heavily rely on currrying (and on the fact that Foo.apply, as any method, will automatically get promoted to a function) and on a little helper to enhance syntax:

object partially {
  def apply[A1,A2,R]( f: (A1, A2) => R ) = f.curried
  def apply[A1,A2,R]( f: (A1, A2) => R, a1: A1 ) = f.curried( a1 )

  def apply[A1,A2,A3,R]( f: (A1, A2, A3) => R ) = f.curried
  def apply[A1,A2,A3,R]( f: (A1, A2, A3) => R, a1: A1 ) = f.curried( a1 )
  def apply[A1,A2,A3,R]( f: (A1, A2, A3) => R, a1: A1, a2: A2 ) = f.curried( a1 )( a2 )


  def apply[A1,A2,A3,A4,R]( f: (A1, A2, A3, A4) => R ) = f.curried
  def apply[A1,A2,A3,A4,R]( f: (A1, A2, A3, A4) => R, a1: A1 ) = f.curried( a1 )
  def apply[A1,A2,A3,A4,R]( f: (A1, A2, A3, A4) => R, a1: A1, a2: A2 ) = f.curried( a1 )( a2 )
  def apply[A1,A2,A3,A4,R]( f: (A1, A2, A3, A4) => R, a1: A1, a2: A2, a3: A3 ) = f.curried( a1 )( a2 )( a3 )
  // ... and so on, potentially up to 22 args
}

Then you can do:

scala> val x = partially(Foo)(1)
x: Int => Foo = <function1>
scala> x(2)
res37: Foo = Foo(1,2)

If you really want to use your has method (instead of just directly applying the function), throw in an implicit classes on top of that:

implicit class Func1Ops[-A,+R]( val f: A => R ) extends AnyVal { 
  def has( arg: A ): R = f( arg ) 
}

and now you can do:

scala> val x = partially(Foo)(1)
x: Int => Foo = <function1>

scala> x has 2
res38: Foo = Foo(1,2)
like image 138
Régis Jean-Gilles Avatar answered Nov 09 '22 01:11

Régis Jean-Gilles


What's wrong with

val x = Foo(1, _: Int)

You could also add an apply method to the companion, that takes only 1 arg and does the partial application for you.

Other than that, there maybe is a way to do that with type macros, which are not yet released, but you can play around with them in macro paradise.

edit:

To add something to a case classes companion, simply do as you normally would:

case class Foo(x: Int, y: Int)

object Foo {
  def apply(x: Int): (Int => Foo) = Foo(x, _: Int)
}

scala> Foo(1,2)
res3: Foo = Foo(1,2)

scala> Foo(1)
res4: Int => Foo = <function1>

In the apply you could also return your PartialA or whatever you like.

like image 39
drexin Avatar answered Nov 09 '22 00:11

drexin


Assuming you really want the "has" DSL, and maybe want it extendable, the following works too:

abstract class A {
  def a: Int
  def b: Int
}    

trait PartialHas[T] { 
    self: { def apply(a: Int, b: Int): T } => 
    trait HasWord { def has(b: Int): T }
    def apply(a: Int): HasWord = new HasWord { def has(b: Int): T = apply(a, b) } 
}    

case class Bar(a: Int, b: Int) extends A
object Bar extends PartialHas[Bar]

There may be a way to use a class macro to do away with the explicit companion definition altogether.

like image 1
Jürgen Strobel Avatar answered Nov 09 '22 01:11

Jürgen Strobel