Преимущества F-ограниченного полиморфизма над классом типов для задачи с типом возврата-ток - PullRequest
5 голосов
/ 19 января 2020

Возвращает текущий тип часто задаваемых вопросов в StackOverflow. Здесь является одним из таких примеров. Обычные ответы кажутся либо F-ограниченным полиморфизмом , либо типом решения шаблонным решением. Одерский предлагает в Полезен ли F-связанный полиморфизм?

F-границы действительно добавляют значительную сложность. Я хотел бы иметь возможность избавиться от них и заменить их подтипом более высокого рода

, в то время как tpolecat (автор ссылки post ) предлагает

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

, где выявлен следующий недостаток

F-ограниченный полиморфизм параметризует тип над собственными подтипами, что является более слабым ограничением, чем то, что обычно хочет пользователь, - это способ сказать «мой тип», который вы не можете express точно с помощью подтипов. Однако классы типов могут express напрямую придумать эту идею, поэтому я бы научил новичков

Мой вопрос, может ли кто-нибудь продемонстрировать ситуацию, когда F-ограниченный полиморфизм благоприятен, или мы должны указать на решение класса типов как канонический ответ для решения проблемы return-current-type ?

F-связанный полиморфизм по параметру типа

trait Semigroup[A <: Semigroup[A]] { this: A =>
  def combine(that: A): A
}

final case class Foo(v: Int) extends Semigroup[Foo] {
  override def combine(that: Foo): Foo = Foo(this.v + that.v)
}

final case class Bar(v: String) extends Semigroup[Bar] {
  override def combine(that: Bar): Bar = Bar(this.v concat that.v)
}

def reduce[A <: Semigroup[A]](as: List[A]): A = as.reduce(_ combine _)

reduce(List(Foo(1), Foo(41)))        // res0: Foo = Foo(42)
reduce(List(Bar("Sca"), Bar("la")))  // res1: Bar = Bar(Scala)

F-ограниченный полиморфизм по типу элемента

trait Semigroup {
  type A <: Semigroup
  def combine(that: A): A
}

final case class Foo(v: Int) extends Semigroup {
  override type A = Foo
  override def combine(that: Foo): Foo = Foo(this.v + that.v)
}

final case class Bar(v: String) extends Semigroup {
  override type A = Bar
  override def combine(that: Bar): Bar = Bar(this.v concat that.v)
}

def reduce[B <: Semigroup { type A = B }](as: List[B]) =
  as.reduce(_ combine _)

reduce(List(Foo(1), Foo(41)))        // res0: Foo = Foo(42)
reduce(List(Bar("Sca"), Bar("la")))  // res1: Bar = Bar(Scala)

Класс типов

trait Semigroup[A] {
  def combine(x: A, y: A): A
}

final case class Foo(v: Int)
object Foo {
  implicit final val FooSemigroup: Semigroup[Foo] = 
    new Semigroup[Foo] {
      override def combine(x: Foo, y: Foo): Foo = Foo(x.v + y.v)
    }
}

final case class Bar(v: String)
object Bar {
  implicit final val BarSemigroup: Semigroup[Bar] = 
    new Semigroup[Bar] {
      override def combine(x: Bar, y: Bar): Bar = Bar(x.v concat y.v)
    }
}

def reduce[A](as: List[A])(implicit ev: Semigroup[A]): A = as.reduce(ev.combine)

reduce(List(Foo(1), Foo(41)))        // res0: Foo = Foo(42)
reduce(List(Bar("Sca"), Bar("la")))  // res1: Bar = Bar(Scala)

Ответы [ 2 ]

1 голос
/ 20 января 2020

F-Bounded является отличным примером того, на что способна система типов express, даже более простые, такие как Java. Но класс типов всегда будет более безопасной и лучшей альтернативой.

Что мы имеем в виду под безопаснее ? Просто мы не можем разорвать договор о возврате точно такого же типа. Что можно сделать для двух форм F-ограниченного полиморфизма (довольно легко) .

F-ограниченного полиморфизма по типу member

Этот довольно легко сломать, так как нам нужно только l ie относительно члена типа .

trait Pet {
  type P <: Pet
  def name: String 
  def renamed(newName: String): P
}

final case class Dog(name: String) extends Pet {
  override type P = Dog
  override def renamed(newName: String): Dog = Dog(newName)
}

final case class Cat(name: String) extends Pet {
  override type P = Dog // Here we break it.
  override def renamed(newName: String): Dog = Dog(newName)
}

Cat("Luis").renamed(newName = "Mario")
// res: Dog = Dog("Mario")

F-ограниченный полиморфизм по параметру типа

Его немного сложнее взломать, так как this: A требует, чтобы расширяющий класс был одним и тем же. Однако нам нужно всего лишь добавить дополнительный уровень наследования .

trait Pet[P <: Pet[P]] { this: P =>
  def name: String 
  def renamed(newName: String): P
}

class Dog(override val name: String) extends Pet[Dog] {
  override def renamed(newName: String): Dog = new Dog(newName)

  override def toString: String = s"Dog(${name})"
}

class Cat(name: String) extends Dog(name) // Here we break it.

new Cat("Luis").renamed(newName = "Mario")
// res: Dog = Dog(Mario)

. Тем не менее, ясно, что подход typeclass является более сложным и имеет более шаблонный характер; Также можно утверждать, что чтобы сломать F-Bounded , нужно сделать это намеренно. Таким образом, если вы в порядке с проблемами F-Bounded и не любите иметь дело со сложностью класса типов , то это все еще верное решение.

Кроме того, мы должны отметить, что даже подход typeclass может быть нарушен с помощью таких вещей, как asInstanceOf или рефлексия.


Кстати, стоит упомянуть, что если вместо возврата модифицированная копия, вы хотите изменить текущий объект и вернуть себя, чтобы разрешить цепочку вызовов (как традиционный Java строитель) , вы можете (следует) использовать this.type .

trait Pet {
  def name: String

  def renamed(newName: String): this.type
}

final class Dog(private var _name: String) extends Pet {
  override def name: String = _name

  override def renamed(newName: String): this.type = {
    this._name = newName
    this
  }

  override def toString: String = s"Dog(${name})"
}

val d1 = Dog("Luis")
// d1: Dog = Dog(Luis)

val d2 = d1.renamed(newName = "Mario")
// d2: Dog = Dog(Mario)

d1 eq d2
// true

d1
// d1: Dog = Dog(Mario)
1 голос
/ 19 января 2020

Я бы предположил, что классы типов действительно являются лучшим шаблоном, и любое F-связанное полиморфное c решение вопроса о «возвращении текущего типа» имеет одинаково хорошую, если не лучшую параллель классов типов.

Подход F-связанного полиморфа c на самом деле не express концепция «текущего типа» очень хорошо, в то время как класс типов может. Классы типов также создают в целом хороший код по принципу, что композиция лучше, чем наследование . Этот ответ предлагает аналогичные логики c со ссылкой, в частности, на scala классов типов.

Примечание: я не авторитет; просто кажется, что это вероятно правильный ответ (как подсказано в вопросе), и его нужно представить.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...