¿Cómo evitan los Rasgos en Scala el "error de diamante"?

16

(Nota: utilicé 'error' en lugar de 'problema' en el título por razones obvias ...;)).

Hice algunas lecturas básicas sobre Rasgos en Scala. Son similares a las interfaces en Java o C #, pero permiten la implementación predeterminada de un método.

Me preguntaba: ¿no puede esto causar un caso del "problema del diamante", razón por la cual muchos idiomas evitan la herencia múltiple en primer lugar?

Si es así, ¿cómo maneja Scala esto?

Aviv Cohn
fuente
Compartir su investigación ayuda a todos . Cuéntanos qué has probado y por qué no satisfizo tus necesidades. Esto demuestra que te has tomado el tiempo para tratar de ayudarte a ti mismo, nos salva de reiterar respuestas obvias y, sobre todo, te ayuda a obtener una respuesta más específica y relevante. También vea Cómo preguntar
mosquito
2
@gnat: esta es una pregunta conceptual, no una pregunta problemática concreta. Si él preguntaba "Tengo esta clase en Scala y me está dando problemas que creo que podrían estar relacionados con el Problema del Diamante, ¿cómo lo soluciono?" entonces su comentario sería apropiado, pero la pregunta pertenecería a SO. : P
Mason Wheeler
@MasonWheeler También hice algunas lecturas básicas sobre Scala. Y la primera búsqueda de "diamante" en lo que he leído me dio la respuesta: "Un rasgo tiene todas las características de la construcción de la interfaz Java. Pero los rasgos pueden haber implementado métodos en ellos. Si está familiarizado con Ruby, los rasgos son similares a los mixins de Ruby. Puedes mezclar muchos rasgos en una sola clase. Los rasgos no pueden tomar parámetros de constructor, pero aparte de eso se comportan como clases. Esto te da la capacidad de tener algo que se acerca a la herencia múltiple sin el problema del diamante ". La falta de esfuerzo en esta pregunta se siente bastante evidente
mosquito
77
Leer esa declaración no te dice CÓMO solo eso.
Michael Brown

Respuestas:

22

El problema del diamante es la incapacidad de decidir qué implementación del método elegir. Scala resuelve esto definiendo qué implementación elegir como parte de las especificaciones del lenguaje ( lea la parte sobre Scala en este artículo de Wikipedia ).

Por supuesto, la definición del mismo orden también podría usarse en la herencia múltiple de clase, entonces, ¿por qué molestarse con los rasgos?

La razón IMO es constructores. Los constructores tienen varias limitaciones que los métodos normales no tienen: solo se pueden invocar una vez por objeto, se deben invocar para cada nuevo objeto, y el constructor de una clase secundaria debe llamar al constructor de sus padres como su primera instrucción (la mayoría de los idiomas serán hágalo implícitamente si no necesita pasar parámetros).

Si B y C heredan A y D heredan B y C, y los constructores de B y C llaman al constructor de A, entonces el constructor de D llamará al constructor de A dos veces. Definir qué implementaciones elegir como hizo Scala con los métodos no funcionará aquí porque se debe llamar tanto a los constructores de B como de los de C.

Los rasgos evitan este problema ya que no tienen constructores.

Idan Arye
fuente
1
Es posible usar la linealización C3 para llamar a los constructores una vez y solo una vez, así es como Python realiza la herencia múltiple. Fuera de mi cabeza, la linealización de D <B | C <Un diamante es D -> B -> C -> A. Además, una búsqueda en Google me mostró que los rasgos de Scala pueden tener variables mutables, por lo que seguramente hay un constructor en algún lugar allí? Pero si está usando composición debajo del capó (no sé, nunca usé Scala) no es difícil ver que B y C podrían compartir en la instancia de A ...
Doval
... Los rasgos parecen una forma muy concisa de expresar todo lo que implica combinar la herencia de la interfaz y la composición + delegación, que es la forma correcta de reutilizar el comportamiento.
Doval
@Doval Mi experiencia con los constructores en Python con herencia múltiple es que son un verdadero dolor. Cada constructor no puede saber en qué orden se llamará, por lo que no sabe cuál es la firma de su constructor principal. La solución habitual es que cada constructor tome un montón de argumentos de palabras clave y pase los no utilizados a su súper constructor, pero si necesita trabajar con una clase existente que no sigue esa convención, no puede heredar de forma segura de eso.
James_pic
Otra pregunta es ¿por qué C ++ no eligió una política sensata para el problema del diamante?
usuario
20

Scala evita el problema del diamante mediante algo llamado "linealización de rasgos". Básicamente, busca la implementación del método en los rasgos que extiende de derecha a izquierda. Ejemplo simple:

trait Base {
   def op: String
}

trait Foo extends Base {
   override def op = "foo"
}

trait Bar extends Base {
   override def op = "bar"
}

class A extends Foo with Bar
class B extends Bar with Foo

(new A).op
// res0: String = bar

(new B).op
// res1: String = foo

Dicho esto, la lista de rasgos que busca puede contener más que los que usted proporcionó explícitamente, ya que podrían extender otros rasgos. Aquí se da una explicación detallada: los rasgos como modificaciones apilables y un ejemplo más completo de la linealización aquí: ¿Por qué no herencia múltiple?

Creo que en otros lenguajes de programación este comportamiento se denomina a veces "Orden de resolución de método" o "MRO".

Lutzh
fuente