Получение частичных конструкторов для case-классов бесплатно

Рассмотрим абстрактный класс, определяющий два свойства

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

который является базовым классом для различных классов случаев, таких как

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

Цель: наконец-то я хотел бы иметь возможность создавать экземпляры вышеупомянутых классов case двумя способами, а именно

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

Подход: поэтому кажется разумным определить вспомогательный класс, который определяет has и позволяет мне частично создавать As

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

Проблема. Существующий механизм не поддерживает такие вызовы, как Bar(1), поскольку на самом деле это вызов Bar.apply(1), то есть метода apply, как определено сгенерированным компилятором объектом Bar.

Было бы здорово, если бы я мог заставить компилятор генерировать объект Bar как object Bar extends PartialAConstructor, где

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))
}

Однако не представляется возможным повлиять на создание сопутствующих объектов классов case.


Желаемые свойства:

  • Классы case: Foo, Bar и т. д. должны оставаться классами case, потому что я хотел бы использовать сгенерированные компилятором вкусности, такие как структурное равенство, copy и автоматически генерируемые экстракторы.

  • «Полное» структурное равенство: определение классов прецедентов как

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

    не является вариантом, потому что метод equals, сгенерированный компилятором, рассматривает только первый список аргументов, и, следовательно, следующее будет ошибочным:

    assert(Foo(1)(0) == Foo(1)(10))
    
  • Как можно меньше повторений кода: например, конечно, можно определить

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

    но это нужно было бы сделать для каждого класса case, расширяющего A, то есть Foo, Bar и т. д.


person Malte Schwerhoff    schedule 05.02.2013    source источник


Ответы (3)


Вы можете в значительной степени полагаться на каррирование (и на тот факт, что Foo.apply, как и любой метод, автоматически становится функцией) и на небольшой помощник для улучшения синтаксиса:

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
}

Затем вы можете сделать:

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

Если вы действительно хотите использовать свой метод has (вместо прямого применения функции), добавьте неявные классы поверх этого:

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

и теперь вы можете сделать:

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

scala> x has 2
res38: Foo = Foo(1,2)
person Régis Jean-Gilles    schedule 05.02.2013

Что случилось с

val x = Foo(1, _: Int)

Вы также можете добавить к компаньону метод apply, который принимает только 1 аргумент и выполняет частичное применение за вас.

Помимо этого, возможно, есть способ сделать это с помощью макросов типов, которые еще не выпущены, но вы можете поиграть с ними в макро-рай.

изменить:

Чтобы добавить что-то к компаньону по кейс-классам, просто сделайте как обычно:

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>

В приложении вы также можете вернуть свой PartialA или что угодно.

person drexin    schedule 05.02.2013
comment
val x = Foo(1, _: Int) не позволяет исправить второй аргумент с помощью такого метода, как has (знаю, я привередлив). Не могли бы вы уточнить, как бы вы добавили метод apply в объект-компаньон, созданный компилятором? - person Malte Schwerhoff; 05.02.2013
comment
Если это единственное, что вас беспокоит, вы можете определить implicit class Func1Ops[-A,+R]( val f: A => R ) extends AnyVal { def has( arg: A ): R = f( arg ) }, а затем выполнить x has 2 (в scala 2.10 было бы очень похоже на 2.9) - person Régis Jean-Gilles; 05.02.2013
comment
@drexin Ваше предложение не СУХОЕ, поскольку мне пришлось бы сделать это для Foo, Bar и любого другого класса случаев. - person Malte Schwerhoff; 05.02.2013
comment
@ RégisJean-Gilles Это действительно должно сработать, спасибо. Foo(1, _: Int) has 2 немного менее лаконичен, чем Foo(1) has 2, на который я надеялся, но поскольку исправление первого аргумента (val f = Foo(1)) выполняется значительно реже, чем исправление второго (f has 2), в конце концов, это может быть не так уж плохо. Однако то, что неявное применимо очень широко, немного нервирует меня. Было бы неплохо, если бы можно было сузить применимость до частичных экземпляров A. - person Malte Schwerhoff; 05.02.2013
comment
Что ж, взгляните на мой ответ, чтобы найти, возможно, более приятный синтаксис. Что касается беспокойства по поводу слишком общего характера, я не вижу способа обойти это, сохраняя при этом принцип DRY. - person Régis Jean-Gilles; 06.02.2013

Предполагая, что вы действительно хотите иметь DSL и, возможно, хотите, чтобы он расширялся, следующее также работает:

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]

Может быть способ использовать макрос класса, чтобы полностью отказаться от явного определения компаньона.

person Jürgen Strobel    schedule 20.03.2014