Scala. Есть ли способ выбрать реализацию супер метода с самотипами?

#scala

#scala

Вопрос:

Когда я расширяю черты, я могу выбрать, какую реализацию метода использовать. Как здесь:

 object Main {

  def main(args: Array[String]): Unit = {
    val c = new C
    println(c.a)
    println(c.b)
  }

  trait Parent {
    def foo: String
  }

  trait A extends Parent {
    override def foo = "from A"
  }

  trait B extends Parent {
    override def foo = "from B"
  }

  class C extends A with B {
    val b = super[A].foo
    val a = super[B].foo
  }

}
 

Но если я хочу сделать то же самое с самотипами, кажется, что это невозможно:

 object Main {

  def main(args: Array[String]): Unit = {
    val c = new C with A with B
    println(c.a)
    println(c.b)
  }

  trait Parent {
    def foo: String
  }

  trait A extends Parent {
    override def foo = "from A"
  }

  trait B extends Parent {
    override def foo = "from B"
  }

  class C {
    self: A with B =>

    val b = super[A].foo
    val a = super[B].foo
  }

}
 

Это не компилируется. Я прав, и это невозможно? Если я прав, почему это так и есть ли обходной путь для этого?

ОБНОВЛЕНИЕ: Зачем мне это нужно в первую очередь? Я играл с внедрением зависимостей, используя собственные типы вместо внедрения конструктора. Итак, у меня был базовый преобразователь признаков и дочерние преобразователи признаков FooConverter и BarConverter. И я хотел написать это именно так (что, конечно, не работает).:

 object Main {

  class Foo

  class Bar

  trait Converter[A] {
    def convert(a: A): String
  }

  trait FooConverter extends Converter[Foo] {
    override def convert(a: Foo): String = ???
  }

  trait BarConverter extends Converter[Bar] {
    override def convert(a: Bar): String = ???
  }

  class Service {

    this: Converter[Foo] with Converter[Bar] =>

    def fooBar(f: Foo, b:Bar) = {
      convert(f)
      convert(b)
    }
  }

}
 

Я думал, что это из-за дженериков, но оказалось, что это не так. Итак, мне просто интересно, можно ли каким-то образом вызвать супер метод выбранного признака с самотипами. Потому что при простом наследовании это возможно. Что касается моей первоначальной проблемы, я могу написать ее так, и она будет работать:

 object Main {

  class Foo

  class Bar

  trait Converter[A] {
    def convert(a: A): String
  }

  trait FooConverter extends Converter[Foo] {
    override def convert(a: Foo): String = ???
  }

  trait BarConverter extends Converter[Bar] {
    override def convert(a: Bar): String = ???
  }

  class Service {

    this: FooConverter with BarConverter =>

    def fooBar(f: Foo, b:Bar) = {
      convert(f)
      convert(b)
    }
  }

}
 

Возможно, более жесткая абстракция, но я не уверен, плохо ли это для такой ситуации и нужна ли мне вообще такая широкая абстракция, как Converter [A] .

Комментарии:

1. Вы правы. Это невозможно. И я не думаю, что есть обходной путь (кроме добавления определенных «foo» к признакам: def aFoo = "from A"; override def foo = aFoo

2. Зачем вам это нужно?

3. @m-z Я обновил свой вопрос.

Ответ №1:

Вызов super методов из уже сконструированного типа невозможен (вы можете сделать это только изнутри). В вашем примере вы пытаетесь вызвать foo экземпляр self , который создается во время выполнения, поэтому foo является виртуальным и может быть переопределен — компилятор не знает, какая фактическая реализация будет вызвана (проблема с формальным или реальным типом). Итак, технически невозможно делать то, что вы хотите (вызывать виртуальный метод как статический).

Наивный взлом :

 trait CC extends A with B {
  val b = super[A].foo
  val a = super[B].foo
}

class C {
  self: CC =>

}
 

Он в основном обеспечивает инкапсуляцию, которую вы хотите — возможно, вы захотите переопределить a и b в классе C, поскольку они не будут доступны (в самом типе C ), пока вы C не смешаете CC с.

Обратите внимание, что в каждом примере, который вы предоставляете (включая мое наивное решение), результирующий val c имеет доступ в foo любом случае, и какой именно foo будет вызываться, зависит от того, как вы смешиваете A и B ( A with B или B with A ). Итак, единственная инкапсуляция, которую вы получаете, заключается в том, что C сам тип не будет иметь foo метода. Это означает, что самотипирование дает вам своего рода способ временно закрыть (сделать приватным) метод в «подклассе» без нарушения LSP — но это не единственный способ (см. Ниже).


Помимо всего этого, некоторые авторы считают непрактичным внедрение cake-injection, которое вы пытаетесь реализовать. Возможно, вы захотите взглянуть на шаблон Thin Cake — в качестве замечания, я успешно использовал нечто подобное в реальном проекте (в сочетании с внедрением конструктора).

Я бы реализовал ваши сервисы конвертера таким образом:

 class Foo

class Bar

trait Converter[A] {
  def convert(a: A): String
}

object FooConverter1 extends Converter[Foo] {
  override def convert(a: Foo): String = ???
}

object BarConverter1 extends Converter[Bar] {
  override def convert(a: Bar): String = ???
}


trait FooBarConvertService {
  def fooConverter: Converter[Foo]
  def barConverter: Converter[Bar]

  def fooBar(f: Foo, b: Bar) = {
    fooConverter(f)
    barConverter(b)

  }

}

trait Converters {
  def fooConverter: Converter[Foo] = FooConverter1 
  def barConverter: Converter[Bar] = BarConverter1 
}

object App extends FooBarConvertService with Converters with ... 
 

Это позволяет вам изменять / имитировать реализацию конвертера при объединении всего этого.

Я бы также заметил, что Converter[Bar] это не что иное, как Function1[Bar, String] or just Bar => String , так что на самом деле вам не нужен отдельный интерфейс для этого:

 sealed trait FooBar //introduced it just to make types stronger, you can omit it if you prefer
class Foo extends FooBar
class Bar extends FooBar

trait FooBarConvertService {

  type Converter[T <: FooBar] = T => String

  def fooConverter: Converter[Foo]
  def barConverter: Converter[Bar]
  def fooBar(f: Foo, b: Bar) = {
    fooConverter(f)
    barConverter(b)
  }

}

trait FooConverterProvider {
  def fooConverter: Foo => String = ??? 
}

trait BarConverterProvider {
  def barConverter: Bar => String = ??? 
}

object App 
  extends FooBarConvertService 
  with FooConverterProvider 
  with BarConverterProvider
 

Вы также можете использовать def fooConverter(f: Foo): String = ??? вместо def fooConverter: Foo => String = ??? этого.

Говоря об инкапсуляции — здесь она более слабая, поскольку вы можете получить доступ к транзитивным зависимостям, поэтому, если вам это действительно нужно — используйте private[package] modifier .

Модуль конвертеров:

 package converters

trait FooBarConvertService {

  type Converter[T <: FooBar] = T => String

  private[converters] def fooConverter: Converter[Foo]
  private[converters] def barConverter: Converter[Bar]

  def fooBar(f: Foo, b: Bar) = {
    fooConverter(f)
    barConverter(b)
  }

}

trait FooConverterProvider {
  private[converters] def fooConverter: Foo => String = ??? 
}

trait BarConverterProvider {
  private[converters] def barConverter: Bar => String = ??? 
}
 

Основной модуль:

 package client 
import converters._  

object App 
  extends FooBarConvertService 
  with FooConverterProvider 
  with BarConverterProvider
 

Вы можете использовать объекты object converters {...}; object client {...} вместо пакетов, если предпочитаете.

Эта инкапсуляция даже сильнее, чем инкапсуляция на основе самотипа, поскольку вы не можете получить доступ fooConverter к / barConverter из App объекта (в вашем примере foo все еще доступен из val c = new C with A with B ):

  client.App.fooBar(new Foo, new Bar) //OK
 client.App.fooConverter
 <console>:13: error: method fooConverter in trait FooConverterProvider cannot be accessed in object client.App
          client.App.fooConverter
                     ^
 

Комментарии:

1. Большое вам спасибо. Это именно то, что я искал.

Ответ №2:

Имейте в виду, что самотипы предназначены для того, чтобы вы могли требовать, чтобы любой клиентский код, который использует признак, который вы смешиваете, также должен смешиваться с другим признаком. Другими словами, это способ объявления зависимостей. Но это не классическое наследование. Итак, когда вы говорите, что класс C { self: A с B => } A и B на самом деле не существует в данный момент. Вы только что определили, что клиентский код должен смешиваться с A и B, чтобы затем смешиваться с C.

Но для вашего конкретного варианта использования кажется, что вы можете достичь той же цели с помощью чего-то вроде этого кода. Другими словами, сначала создайте третью черту, а затем расширьте ее до определенного класса.

 object DoubleSelfType extends App {
      val c = new DoubleFoo
      println(c.a)
      println(c.b)

      trait Parent {
        def foo: String
      }

      trait A extends Parent {
        override def foo = "from A"
      }

      trait B extends Parent {
        override def foo = "from B"
      }

      trait C {
        self: A with B =>

        val a = ""
        val b = ""

      }

      class DoubleFoo extends C with A with B {
        override val b = super[A].foo
        override val a = super[B].foo
      }

    }
 

Комментарии:

1. лучше никогда не переопределять значения val в чертах scalapuzzlers.com/#pzzlr-005

2. Спасибо за ваш ответ. Это здорово, но это не совсем то, что я хотел, потому что я не хочу, чтобы у типа DoubleFoo были общедоступные методы из признаков A и B. Я обновил свой вопрос на случай, если вам интересно, что я пытался сделать.