Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #1771 - inconsistencias en list.asSet() #1786

Merged
merged 28 commits into from
Sep 23, 2019

Conversation

nicovio
Copy link
Contributor

@nicovio nicovio commented Sep 11, 2019

Cambios:

  • asSet() deja de ser native y withoutDuplicates() ya no lo usa.

  • Agrego tests para asSet() y withoutDuplicates()

Después de arreglar las inconsistencias relativas al issue, note este caso que seguía pasando:

set-with-duplicated-bug

Cuando el set era de distintos tipos, cada tanto se "comía" un duplicado (siempre eran Strings y Dates).

Lo pude solucionar con un cambio en el método compare() de WollokObjectComparator. Hay que revisarlo.

Fixes #1771 and #1783

@nicovio nicovio added this to the Wollok v1.8.6 milestone Sep 11, 2019
@coveralls
Copy link

coveralls commented Sep 11, 2019

Coverage Status

Coverage increased (+0.3%) to 89.789% when pulling 68a628f on fix-#1771-inconsistencias-en-list.asSet into 694f874 on dev.

@nicovio nicovio self-assigned this Sep 11, 2019
@fdodino
Copy link
Collaborator

fdodino commented Sep 11, 2019

@nicovio en general suena bien, yo te pido que hagas un chequeo extra porque me preocupa que no se degrade la performance. Fijate este proyecto

https://github.com/wollok/test-performance-set--1370

Bajátelo y corré los tests, que generan sets y lists bastante voluminosos, los métodos no deberían tardar mucho (en general < 1seg, algunos sí tardan y están ok, fijate este PR que dice más o menos lo que se esperaría, por más que solo tengas el total de lo que corrió es fácil ver si se fue al pasto o no)

@nicovio
Copy link
Contributor Author

nicovio commented Sep 11, 2019

@nicovio en general suena bien, yo te pido que hagas un chequeo extra porque me preocupa que no se degrade la performance. Fijate este proyecto

https://github.com/wollok/test-performance-set--1370

Bajátelo y corré los tests, que generan sets y lists bastante voluminosos, los métodos no deberían tardar mucho (en general < 1seg, algunos sí tardan y están ok, fijate este PR que dice más o menos lo que se esperaría, por más que solo tengas el total de lo que corrió es fácil ver si se fue al pasto o no)

Ok lo voy a ver. Gracias Dodain! ✌️

@nicovio
Copy link
Contributor Author

nicovio commented Sep 12, 2019

@fdodino estuve corriendo los tests de Set que estan acá https://github.com/wollok/test-performance-set--1370 y compare los tiempos con los que pusiste en #1370.

Algunos tardaron bastante más, así que me fije como daban antes de los cambios de este PR y las diferencias fueron las mismas.

Dejo los tiempos totales que se esperaban vs los que dieron.

Pruebas hechas con Set

Números

Esperado: 4502 ms
Resultado: 4361 ms

Objetos definidos por el usuario sin == ni >

Esperado: 1740 ms
Resultado: 2697 ms

Objetos definidos por el usuario (redefiniendo == y >)

Esperado: 6692 ms
Resultado: 6812 ms

Strings

Esperado: 1548 ms
Resultado: 2328 ms

Dates

Esperado: 1036 ms
Resultado: 2209 ms

Booleans

Esperado: 53 ms
Resultado: 791 ms

@fdodino
Copy link
Collaborator

fdodino commented Sep 13, 2019

Pongo acá lo que hablamos recién, puede ser una diferencia en la potencia de las máquinas, no me preocuparía por la performance.

Copy link
Member

@npasserini npasserini left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Impresionante laburo!!!
Pido disculpas por los muchos comentarios pero es un lugar delicado y ya tuvimos algunas idas y vueltas, propongo que nos tomemos la paciencia de dejarlo bien de una vez por todas.

@@ -922,13 +922,13 @@ class Set inherits Collection {
}

/**
* Returns a new copy of current Set.
* Converts an object to a Set. No effect on Sets.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Es un poco quisquilloso pero deberíamos evitar la palabra effect que tiene un significado bastante específico en nuestro discurso. De paso, no creo que convierta an object to a set, vale sólo para colecciones, de hecho convierte this.

Entonces propongo "Converts this collection into a set. If this collection is already a set, it returns a copy of it."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 a tu propuesta.

Otra cosa: Esto estaba puesto en el asSet() de Set, pero creo que debería estar en el de Collection.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suena lógico. @fdodino tenemos una política sobre wollokdoc en métodos que son overrides?

*/
override method asSet() native
override method asSet() = self
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acá cambió el comportamiento y no coincide con el comentario de arriba. Esta implementación no está bien en Collection sólo valdría para Set. Francamente me gustaba la idea de que el asSet devolviera una copia, pero si prefieren evitarlo no está mal, pero deberíamos:

  • mover esta implementación a Set
  • dejar este método abstracto (y chequear que otras colecciones lo implementen).
  • corregir el wollokdoc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Esta implementación es la de Set y en Collection es abstracto.

Me parece que se genera una confusión porque esta mal la documentación y en el asSet() de Setdice lo que debería decir en el de Collection.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, mala mía. Igualmente me gustaba de la implementación nativa que devolvía un set nuevo en lugar de self. @fdodino @PalumboN @lspigariol qué opinan?

@@ -1402,7 +1406,7 @@ class List inherits Collection {
* [1, 3, 1, 5, 1, 3, 2, 5].withoutDuplicates() => Answers [1, 2, 3, 5]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Este ejemplo está mal, está cambiando el orden y es justamente lo que no queremos.
De hecho estaría bueno agregar ese detalle en el comentario.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Estoy de acuerdo con vos.

org.uqbar.project.wollok.lib/src/wollok/lang/WList.xtend Outdated Show resolved Hide resolved
@@ -34,7 +34,7 @@ class WollokObjectComparator implements Comparator<WollokObject> {
val comparator = comparisonsStrategy.get(o1.kind.fqn) ?: new WollokObjectEqualsComparator
return comparator.compare(o1, o2)
} catch (RuntimeException e) {
return o1.hashCode.compareTo(o2.hashCode)
return (o1.kind.hashCode).compareTo(o2.kind.hashCode)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No me convence lo de atrapar RuntimeException aca. ¿No podríamos estar capturando cualquier excepción inesperada y ocultándola?

Igual entiendo que este no es un problema de este PR, y de hecho esta línea me parece mejor que la anterior, al menos respeta la relación de orden que el comparator debería definir. Si quieren levantamos otro ticket para revisar esto.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahora no recuerdo por qué puse el catch, @nicovio , te acordás por qué pusiste el o1.kind.hashCode en lugar de o1.hashCode, ¿en qué casos salta el RuntimeException? Es lo único que me da cosita sacar. Y sí, si esto traba el PR generemos otro ticket.

Copy link
Contributor Author

@nicovio nicovio Sep 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

El RuntimeException salta cuando o1 y o2 son de distintos tipos.

Puse el o1.kind.hashCode porque estaban andando mal los sets con elementos de distintos tipos, a veces aceptaba duplicados (pasaba con strings y dates).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, cierto!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O sea, o1 y o2 siempre son WollokObject, pero vos me entendiste jaja (creo).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yo entiendo dos cosas:

  1. Que el error que se espera atrapar es algún DNU producto de que los objetos se intentan comparar entre ellos por igualdad y si son de distinto tipo y ellos mismos no lo contemplan, posiblemente uno le termina mandando un mensaje a otro que no entiende. Ejemplo:

class Pair {
var x,y
method equals(other) = x == other.x() and y == other.y()
}

Si hacés new Pair(x=1,y=2).equals( "ola") => DNU "hola no entiende x".

A mí me gusta que se intente capturar ese caso. Capturarlo con una excepción podría ser materia de discusión, pero la alternativa es preguntar por la clase y puede tener sus propios problemas, da para una discusión larga.

El tema acá es que también estamos atrapando otros errores e ignorándolos, eso me parece más peligroso. Pero no lo discutamos acá, creo un issue para ver eso.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. La segunda es que usar el hash para el orden es peligroso porque no establece una relación de orden, ergo, podría pasar que a > b > c > a si b y c pueden compararse entre sí, pero no con a.

Entiendo que eso se evita si se usa el hash de la clase, asumiendo que si b y c son comparables, entonces son de la misma clase y entonces el kind.hash es el mismo.

Siendo estricto, esto funciona si

para todo b y c comparables (es decir, comparator.compare(o1, o2) no tira excepción), b.kind == c.kind

Creo que a la larga esto medio define la cuestión anterior y debemos agregar:
a. Un if o1.kind == o2.kind antes de intentar el compare, y eso nos exime de atrapar excepciones.
b. Definir la regla "si dos objetos tienen mismo kind deben ser comparables sin tirar excepción" y documentarla.
c. De yapa, no usar kind.hashCode y en cambio comparar los nombres de los kinds entre sí. Esto es un chiche pero nos daría un ordenamiento predecible entre objetos de distinto kind. Que es un ordenamiento arbitrario (alfabético, bah) pero menos caótico que ordenar por hash y que sería consistente entre diferentes ejecuciones.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Esto también obliga a tener claro cuándo es que puede pasar que dos objetos tengan el mismo kind, que asumo que no es lo mismo que tener "la misma clase", porque para eso se inventó el concepto, no?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No sé, si les parece podríamos meterle el fix que decía y dar por resuelto el #1791

@Test
def void testListOfStringAsSetConversion() {
'''
assert.equals(1, ["hola","hola"].asSet().size())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Me gustaría probar con dos formas de construir el mismo string, onda "ho" + "la" porque si lo construis las dos veces de la misma manera podría terminar siendo efectivamente "el mismo" y no estás testeando lo que queríamos verificar.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lo mismo vale para los demás tipos de datos.

def void testListOfDictionaryAsSetConversion() {
'''
const dictionary = new Dictionary()
assert.equals(1, [dictionary,dictionary].asSet().size())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acá efectivamente es el mismo diccionario, tenemos que probarlo creando dos dictionaries.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hay un problema. Si creo dos dictionaries los considera distintos y esto [dictionary,otherDictionary].asSet().size() da 2.

¿Hay que redefinir el equals de Dictionary?

Copy link
Member

@npasserini npasserini Sep 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Es posible!
Igual antes de llegar a esa conclusión haría el test más unitario posible assert.equals(new Dictionary(), new Dictionary()) y vería qué pasa.

def void testListOfPairAsSetConversion() {
'''
const pair = new Pair(1,2)
assert.equals(1, [pair,pair].asSet().size())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idem, hay que hacer el new dos veces.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pasa lo mismo que con los dictionaries.

class MiClase {}
program a {
const miClase = new MiClase()
assert.equals(1, [miClase,miClase].asSet().size())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hacer new dos veces!
Para que esto tenga sentido MiClase debería implementar equals.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acá hay otro tema para ver.

Este test falla:

	class MiClase {
		override method equals(other) = return true
	}
	program a {
		const a = new MiClase()
		const b = new MiClase()
		assert.equals(1, [a,b].asSet().size())
	}
org.uqbar.project.wollok.tests.interpreter.WollokComparisonFailure: Expected [1] but found [2]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahora que veo todo, creo que este es el test más fácil y más representativo. Esto debería andar y si no anda es que la implementación de asSet está mal y debemos cambiarla.

Al hacerlo, deberíamos escuchar los comentarios de @fdodino sobre las cuestiones de performance en las que estuvo trabajando, pero creo que vos ya estás al tanto de eso más que yo.

list.add(true)
list.add(1)
list.add("hola")
assert.equals(3, list.withoutDuplicates().size())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Falta acá hacer el assert del orden.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

De acuerdo.

@nicovio
Copy link
Contributor Author

nicovio commented Sep 13, 2019

Impresionante laburo!!!
Pido disculpas por los muchos comentarios pero es un lugar delicado y ya tuvimos algunas idas y vueltas, propongo que nos tomemos la paciencia de dejarlo bien de una vez por todas.

No me molestan tantos comentarios. Al contrario, me sirven para aprender.

Yo pido disculpas si me mando muchas cagadas.

@fdodino
Copy link
Collaborator

fdodino commented Sep 14, 2019

@npasserini @nicovio

Ahí estuvimos con los muchachos cambiando la lógica, básicamente:

  • chequeamos que en la jerarquía del objeto no haya un equals o un == (que las definiciones sean únicamente de wollok.lang.Object). Esto está acoplado a la biblioteca, habrá que ver si el día de mañana redefinimos un == o un equals
  • en caso de tener un == o un equals en la jerarquía (ya sea en la clase/wko donde estoy parado o bien en alguna de las clases padres antes de Object), llamo al equals primero que a su vez por defecto delega en el == en Object. Eso nos asegura que si redefinen el equals, se use el equals y si redefinen el == llegue por rebote.

Agregué 4 tests.

Lo que sí pasa es que si definís esto:

		class Animal {
			override method ==(other) = true
		}
		
		class Perro inherits Animal {
		}
		
		class Gato inherits Animal {
			
		}

cuando tenés

#{ new Gato(), new Perro(), "hola" }.size()  // me da 2
#{ "hola", new Gato(), new Perro() }.size()  // me da 1

pero como dice @nscarcella , estamos definiendo un equals que no es conmutativo, con lo cual recontrahipersupermegaarchiexcede el alcance de este issue.

@nicovio
Copy link
Contributor Author

nicovio commented Sep 14, 2019

Buenísimo, más tarde subo las correcciones del review de @npasserini.

@npasserini
Copy link
Member

npasserini commented Sep 14, 2019

@npasserini @nicovio

Ahí estuvimos con los muchachos cambiando la lógica, básicamente:

  • chequeamos que en la jerarquía del objeto no haya un equals o un == (que las definiciones sean únicamente de wollok.lang.Object). Esto está acoplado a la biblioteca, habrá que ver si el día de mañana redefinimos un == o un equals
  • en caso de tener un == o un equals en la jerarquía (ya sea en la clase/wko donde estoy parado o bien en alguna de las clases padres antes de Object), llamo al equals primero que a su vez por defecto delega en el == en Object. Eso nos asegura que si redefinen el equals, se use el equals y si redefinen el == llegue por rebote.

Agregué 4 tests.

Lo que sí pasa es que si definís esto:

		class Animal {
			override method ==(other) = true
		}
		
		class Perro inherits Animal {
		}
		
		class Gato inherits Animal {
			
		}

cuando tenés

#{ new Gato(), new Perro(), "hola" }.size()  // me da 2
#{ "hola", new Gato(), new Perro() }.size()  // me da 1

pero como dice @nscarcella , estamos definiendo un equals que no es conmutativo, con lo cual recontrahipersupermegaarchiexcede el alcance de este issue.

Tenés razón, esa implementación de equals es incorrecta, al menos en una colección que tiene cosas que no son animales. Entiendo que si restringís la colección solo a animales ahí sí es conmutativa y todo debería funcionar de acuerdo a lo esperado.

Estaría bueno agregar esa regla en el comentario del equals, onda "Si redefinís esto asegurate de hacerlo conmutativo.".

Ahora... no sé cómo lo implementarías bien si no es preguntando el nombre de la clase, pero bueno, eso ya es otro tema.

@fdodino
Copy link
Collaborator

fdodino commented Sep 14, 2019

Jejeje, lo mismo dijo NicoS

@fdodino
Copy link
Collaborator

fdodino commented Sep 14, 2019

tiré kind pero es isAssignableFrom lo que necesitaríamos

@clombardi
Copy link
Collaborator

Gente, socorro. Mañana toca (en UNAHur) hablar del asSet() y justo el ejemplo es con una lista de Strings.
Y auch, en el set aparece dos veces el mismo String. Imagino que es porque no es el mismo String, obsérvese lo siguiente:

>>> var a = "hola"
>>> var strs = [a, "que", a, "tal"]
>>> strs.asSet()
#{"hola", "que", "tal"}
>>> var strs2 = ["hola", "que", "hola", "tal"]
>>> strs2.asSet()
#{"hola", "que", "hola", "tal"}

Para mayor felicidad, da la clase un docente nuevo, y yo no estoy en Buenos Aires.
Yo estoy usando Wollok 1.8.5. ¿Hay alguna versión posterior en la que esto esté arreglado?

Graciaaaas por cualquier cosa que me puedan decir.

@lspigariol
Copy link
Contributor

lspigariol commented Sep 16, 2019 via email

@nicovio
Copy link
Contributor Author

nicovio commented Sep 21, 2019

Hola @fdodino, tengo una mala noticia... parecía que estaba solucionado el problema con sets de listas y sets de sets, pero no.

el problema para mi está en este método:

def compareGreaterThan(WollokObject o1, WollokObject o2) {
	if (o1.hasGreaterThanMethod) {
		 if (o1.wollokGreaterThan(o2)) 1 else -1
	} else 1
}

como Collection no tiene greaterThan, acá devuelve siempre 1

Ahí cambie los tests para que fallen.

@fdodino
Copy link
Collaborator

fdodino commented Sep 21, 2019

hola @nicovio, ok, mañana lo miro. Una cosa que yo haría por si querés probar:

def compareGreaterThan(WollokObject o1, WollokObject o2) {
	if (o1.hasGreaterThanMethod) {
		 if (o1.wollokGreaterThan(o2)) 1 else -1
	} else {
                 // --->
                 if (o1.hashCode > o2.hashCode) 1 else -1
                 // <----
        }
}

@nicovio
Copy link
Contributor Author

nicovio commented Sep 21, 2019

esto no anduvo:

def compareGreaterThan(WollokObject o1, WollokObject o2) {
	if (o1.hasGreaterThanMethod) {
		 if (o1.wollokGreaterThan(o2)) 1 else -1
	} else {
                 // --->
                 if (o1.hashCode > o2.hashCode) 1 else -1
                 // <----
        }
}

esto si:

def compareGreaterThan(WollokObject o1, WollokObject o2) {
	if (o1.hasGreaterThanMethod) {
		 if (o1.wollokGreaterThan(o2)) 1 else -1
	} else {
                 // --->
                 if (o1.toString > o2.toString) 1 else -1
                 // <----
        }
}

@npasserini
Copy link
Member

npasserini commented Sep 21, 2019 via email

@fdodino
Copy link
Collaborator

fdodino commented Sep 21, 2019

No tiene sentido, por eso mi propuesta inicial era devolver 1 como el mínimo cálculo posible. Pero seguramente se me pasó alguna letra chica del TreeSet y por eso el test que falla. Si corrige los tests, mi propuesta es pushearlo: ahora tenemos un montón de tests verdes más, solucionamos el problema original, no hay duplicados triviales para números, strings, booleanos, fechas, que es lo que el 96,89% de las veces vamos a usar, y démosle para adelante...

@npasserini
Copy link
Member

npasserini commented Sep 21, 2019 via email

@fdodino
Copy link
Collaborator

fdodino commented Sep 21, 2019

Dale, perfecto!!

@npasserini
Copy link
Member

npasserini commented Sep 22, 2019 via email

@npasserini
Copy link
Member

Bueno, tengo algo que da tests verdes. Y además tengo una definición precisa de lo que soportamos y lo que no. Las reglas para que un set funcione son:

  1. Si una clase define equals debe respetar las reglas de una relación de equivalencia: reflexiva, simétrica y transitiva.
  2. Si una clase define <, debe definir también el equals, respetar las reglas de una relación de orden (reflexiva, antisimétrica y transitiva) y debe ser consistente con la relación de igualdad, es decir sólo una de estas puede ser válida: a<b, b<a o a=b; y las transitividades entre ambas relaciones deben respetarse, o sea si a<b y b=c entonces a<c.
  3. Definir relaciones de igualdad u orden entre objetos de diferentes kind's sólo puede funcionar si en la misma colección sólo hay objetos comparables entre ellos.

Es decir, esto permite dos features interesantes pero que no se pueden usar al mismo tiempo, usando mi ejemplo anterior:

  • puedo tener una colección de triángulos y cuadrados, que definen una igualdad cruzada entre ellos (un triángulo puede ser igual a un cuadrado), mientras en esa colección haya sólo triángulos y cuadrados (o cosas comparables entre sí).
  • si no uso la igualdad cruzada (es decir a=b => a.kind = b.kind), entonces puedo tener coleciones mixtas de objetos cualesquiera.
  1. Objetos que definen igualdad pero no son comparables tienen que tener un toString que produzca un ordenamiento compatible con la igualdad. Es decir:

Si a = b, a != c y a.toString < c.toString, entonces b.toString < c.toString (o bien a.toString > c.toString y b.toString > c.toString.

@npasserini
Copy link
Member

Entiendo que estas reglas son violentamente complicadas, pero surgen de la motivación de usar un tree set y no forzar a que todos los objetos sean comparables dentro del tree set. Habría que considerar la posibilidad de usar hashsets, tal vez las reglas serían más simples. La pregunta es si podemos hacerlo sin pedirle al estudiante que implemente eventualmente un hashCode.

Finalmente, la última opción sería evitar tanto treesets como hashsets y comparar por fuerza bruta, pero esa estrategia la hemos intentado reemplazar por problemas de performance.

Una variante para analizar sería que se usen colecciones inteligentes tipo treeset cuando tenemos colecciones homogéneas y migrar el wrapped a algo menos eficiente pero más simple de entender cuando se detecta la necesidad (es decir al ingresar un elemento heterogéneo en la colección).

@fdodino faltaría verificar que los cambios que propongo no introducen problemas de performance. ¿Cómo puedo verificar eso?

@npasserini
Copy link
Member

Mi test del ejemplo anterior anda, pero acá tengo un ejemplo que lo rompe:

#{1,2,new Cuadrado(lado=3),3,"hola",1, new Pair(1,2), 6, "hola", 2323, new Pair(1,2), [], 1, #{}, [], new Triangulo(base=4, altura=4), new Triangulo(base=6, altura=3), new Cuadrado(lado=3), new Triangulo(base=3, altura=6),new Dictionary(), #{}, new Date(), new Dictionary()}
#{a Cuadrado[lado=3], a Triangulo[altura=4, base=4], a Triangulo[altura=6, base=3], 9/22/2019, a Dictionary[], [], 1, 2, 3, 6, 2323, a Pair[x=1, y=2], #{}, "hola"}

Esto no es un bug, si compramos la especificación que escribí antes. Los triángulos y cuadrados no están respetando mis reglas. :)

@fdodino
Copy link
Collaborator

fdodino commented Sep 22, 2019

@npasserini @nicovio

El jueves a la noche NicoV vino con un test "peor" que el que tiraste anoche, juntando en una colección números, booleanos, fechas, pares, dictionaries, sets, lists, strings, y no se cuántas cosas más. Yo lo miré y le dije: "y qué puede hacer un alumno con ésto", y medio que lo convencí de tirar para atrás ese test. Después de ver la seguidilla, me encanta ver que tenemos una justificación teórica de por qué eso no es algo que me interese testear: si tenemos objetos de una jerarquía (ej. Figura) que redefinen el igual y funciona, es lo que yo quiero. Después si tengo objetos polimórficos de diferentes jerarquías, no querría bajo ningún punto que redefinan el equals, y si lo hacen, creo que habría que advertirles que no usen un set.

  1. O sea, un issue probable sería agregar un warning en el validator cuando eso ocurra.
  2. Otra cosa que me gustaría es agregar esta definición en Set.
  3. Y una tercera propuesta que tiro es: escribamos una clase BasicSet, que herede de Set y tenga una implementación full hecha en Wollok. El literal #{} te devuelve un Set inocente, si querés usar BasicSet tenés que instanciarlo con new.

@npasserini
Copy link
Member

npasserini commented Sep 22, 2019 via email

@fdodino
Copy link
Collaborator

fdodino commented Sep 22, 2019

Me gusta mucho esa línea de pensamiento! No se me había ocurrido que el tener equals/greater than podría hacer cambiar la implementación de Set que usemos... +1 a mergear y a permitir que NicoV siga jugando con otras cosillas

@nicovio
Copy link
Contributor Author

nicovio commented Sep 22, 2019

Estuve corriendo de nuevo los tests de acá: https://github.com/wollok/test-performance-set--1370.

Comento los que tuvieron diferencias notables:

Objetos definidos por el usuario sin == ni >

Esperado: 1740 ms
Resultado: 133408 ms

Objetos definidos por el usuario (redefiniendo == y >)

Esperado: 6692 ms
Resultado: 15664 ms

@npasserini
Copy link
Member

npasserini commented Sep 22, 2019 via email

@nicovio
Copy link
Contributor Author

nicovio commented Sep 22, 2019

Me tiran un centro para pdoer correr esos tests? El dom., 22 de sep. de 2019 a la(s) 15:56, Nicolas Viotti ( notifications@github.com) escribió:

Estuve corriendo de nuevo los tests de acá: https://github.com/wollok/test-performance-set--1370. Comento los que tuvieron diferencias notables: Objetos definidos por el usuario con == sin el > Paso a tardar 133 segundos!! Objetos definidos por el usuario (redefiniendo == y >) Paso a tardar 15664 ms, cuando lo esperado era 6692 ms. — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub <#1786?email_source=notifications&email_token=ABDLKOJ2KX7LO57JLTWXOBDQK65WTA5CNFSM4IVQURO2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD7JMUOQ#issuecomment-533908026>, or mute the thread https://github.com/notifications/unsubscribe-auth/ABDLKOJ2K5PM7RDUQGACLJLQK65WTANCNFSM4IVQUROQ .

yo ejecute un entorno Wollok embebido e importe el proyecto que paso Fer. Y ahí corrí los tests.

pero ahora me surgió la duda de si esa era la forma correcta... veamos que dice @fdodino.

@npasserini
Copy link
Member

npasserini commented Sep 22, 2019 via email

@nicovio
Copy link
Contributor Author

nicovio commented Sep 22, 2019

Ah si, perdón. Lo esperado lo calcule con los resultados que posteo Dodino en #1370 (comment)

Sume los que tardan las operaciones en cada test. De ahí sale lo que debería tardar.

@fdodino
Copy link
Collaborator

fdodino commented Sep 22, 2019

Sí, es así como los corrés.
Estaría bueno el que dura 133 segundos ver cuánto tarda cada test, es medio garrón pero si buscás el PR del issue 1370 te dice cómo

@npasserini
Copy link
Member

Bueno, acá tengo los tiempos ```

const unList = [], tiempo total: [214 ms]

(1 .. 1000).forEach{ i => 
	unList.add(i * 2)
}, tiempo total: [587 ms]

unList.contains(55), tiempo total: [347 ms]

unList.filter { i => i.even() }.size(), tiempo total: [700 ms]

unList.map { i => i + 1 }, tiempo total: [291 ms]

unList.anyOne(), tiempo total: [2 ms]

unList.sum(), tiempo total: [216 ms]

unList.max(), tiempo total: [16 ms]

unList.fold(0, { acum, i => acum + i }), tiempo total: [47 ms]

var numbers = unList.fold(unList.anyOne(), { acum, number => acum.max(number) }), tiempo total: [439 ms]

2000

console.println(numbers), tiempo total: [6 ms]

[...1000 elements]

console.println(unList), tiempo total: [13 ms]

unList.take(10), tiempo total: [36 ms]

unList.drop(10), tiempo total: [167 ms]

unList.subList(100, 170), tiempo total: [15 ms]

unList.find { i => i > 900 }, tiempo total: [24 ms]

unList.clear(), tiempo total: [1 ms]

assert.that(true), tiempo total: [4 ms]

const unList = [], tiempo total: [0 ms]

(1 .. 1000).forEach{ i => 
	const nombre = "pepita" + i
	const ave = new Ave(nombre = nombre, energia = i)
	unList.add(ave)
}, tiempo total: [308 ms]

unList.contains(unList.anyOne()), tiempo total: [422 ms]

unList.filter { ave => ave.energia() > 10 }, tiempo total: [56 ms]

unList.map { ave => ave.energia() }, tiempo total: [191 ms]

unList.anyOne(), tiempo total: [1 ms]

unList.take(10), tiempo total: [8 ms]

unList.drop(10), tiempo total: [136 ms]

unList.subList(100, 170), tiempo total: [13 ms]

unList.find { ave => ave.energia() > 105 }, tiempo total: [7 ms]

unList.sum({ ave => ave.energia() }), tiempo total: [222 ms]

unList.max(), tiempo total: [112605 ms]

unList.fold(0, { acum, ave => acum + ave.energia() }), tiempo total: [56 ms]

unList.clear(), tiempo total: [0 ms]

assert.that(true), tiempo total: [1 ms]

const unList = [], tiempo total: [0 ms]

(1 .. 1000).forEach{ i => 
	const nombre = "pepita" + i
	const ave = new AveLoca(nombre = nombre, energia = i)
	unList.add(ave)
}, tiempo total: [188 ms]

unList.contains(unList.anyOne()), tiempo total: [11 ms]

unList.filter { ave => ave.energia() > 10 }, tiempo total: [45 ms]

unList.map { ave => ave.energia() }, tiempo total: [167 ms]

unList.anyOne(), tiempo total: [1 ms]

unList.take(10), tiempo total: [6 ms]

unList.drop(10), tiempo total: [125 ms]

unList.subList(100, 170), tiempo total: [12 ms]

unList.find { ave => ave.energia() > 105 }, tiempo total: [6 ms]

unList.sum({ ave => ave.energia() }), tiempo total: [163 ms]

unList.max(), tiempo total: [122911 ms]

unList.fold(0, { acum, ave => acum + ave.energia() }), tiempo total: [55 ms]

unList.clear(), tiempo total: [1 ms]

assert.that(true), tiempo total: [1 ms]

const unList = [], tiempo total: [0 ms]

(1 .. 1000).forEach{ i => 
	const nombre = "pepita" + i
	const ave = new AveMagica(nombre = nombre, energia = i)
	unList.add(ave)
}, tiempo total: [180 ms]

unList.contains(unList.anyOne()), tiempo total: [152 ms]

unList.filter { ave => ave.energia() > 10 }, tiempo total: [48 ms]

unList.map { ave => ave.energia() }, tiempo total: [187 ms]

unList.anyOne(), tiempo total: [1 ms]

unList.take(10), tiempo total: [10 ms]

unList.drop(10), tiempo total: [163 ms]

unList.subList(100, 170), tiempo total: [15 ms]

unList.find { ave => ave.energia() > 105 }, tiempo total: [9 ms]

unList.sum({ ave => ave.energia() }), tiempo total: [190 ms]

unList.max(), tiempo total: [12879 ms]

unList.fold(0, { acum, ave => acum + ave.energia() }), tiempo total: [63 ms]

unList.clear(), tiempo total: [0 ms]

assert.that(true), tiempo total: [0 ms]

const unList = [], tiempo total: [0 ms]

(1 .. 1000).forEach{ i => 
	unList.add("hola" + i)
}, tiempo total: [134 ms]

unList.contains("hola88"), tiempo total: [2 ms]

unList.filter { unString => unString.length().even() }, tiempo total: [369 ms]

unList.map { unString => unString.length() }, tiempo total: [184 ms]

unList.anyOne(), tiempo total: [2 ms]

unList.take(10), tiempo total: [6 ms]

unList.drop(10), tiempo total: [128 ms]

unList.subList(100, 170), tiempo total: [12 ms]

unList.find { s => s == "hola809" }, tiempo total: [39 ms]

unList.sum({ unString => unString.length() }), tiempo total: [202 ms]

unList.max(), tiempo total: [13 ms]

unList.fold(0, { acum, unString => acum + unString.length() }), tiempo total: [59 ms]

unList.clear(), tiempo total: [0 ms]

assert.that(true), tiempo total: [1 ms]

const unList = [], tiempo total: [0 ms]

const unList = [], tiempo total: [1 ms]

(1 .. 1000).forEach{ i => 
	unList.add(true)
}, tiempo total: [57 ms]

unList.contains(false), tiempo total: [10 ms]

unList.filter { unBoolean => unBoolean }, tiempo total: [6 ms]

unList.map { unBoolean => not unBoolean }, tiempo total: [195 ms]

unList.anyOne(), tiempo total: [1 ms]

unList.take(10), tiempo total: [7 ms]

unList.drop(10), tiempo total: [124 ms]

unList.subList(100, 170), tiempo total: [16 ms]

unList.find { b => b }, tiempo total: [1 ms]

unList.sum({ unBoolean => if (unBoolean) 1 else 0 }), tiempo total: [155 ms]

unList.max(), tiempo total: [1 ms]

unList.fold(0, { acum, unBoolean => acum + (if (unBoolean) 1 else 0) }), tiempo total: [41 ms]

unList.clear(), tiempo total: [0 ms]

assert.that(true), tiempo total: [1 ms]

const unSet = #{}, tiempo total: [8 ms]

(1 .. 1000).forEach{ i => 
	unSet.add(i * 2)
}, tiempo total: [121 ms]

unSet.contains(55), tiempo total: [459 ms]

unSet.filter { i => i.even() }.size(), tiempo total: [719 ms]

unSet.map { i => i + 1 }, tiempo total: [203 ms]

unSet.anyOne(), tiempo total: [0 ms]

unSet.difference(#{1, 2, 5}), tiempo total: [500 ms]

unSet.sum(), tiempo total: [172 ms]

unSet.max(), tiempo total: [1 ms]

unSet.fold(0, { acum, i => acum + i }), tiempo total: [40 ms]

var numbers = unSet.fold(unSet.anyOne(), { acum, number => acum.max(number) }), tiempo total: [1886 ms]

2000

console.println(numbers), tiempo total: [1 ms]

#{...1000 elements}

console.println(unSet), tiempo total: [2 ms]

unSet.clear(), tiempo total: [1 ms]

assert.that(true), tiempo total: [0 ms]

const unSet = #{}, tiempo total: [0 ms]

(1 .. 1000).forEach{ i => 
	const nombre = "pepita" + i
	const ave = new Ave(nombre = nombre, energia = i)
	unSet.add(ave)
}, tiempo total: [126870 ms]

unSet.contains(unSet.anyOne()), tiempo total: [2 ms]

unSet.filter { ave => ave.energia() > 10 }, tiempo total: [55 ms]

unSet.map { ave => ave.energia() }, tiempo total: [234 ms]

unSet.anyOne(), tiempo total: [1 ms]

unSet.difference(#{unSet.anyOne()}), tiempo total: [737 ms]

unSet.sum({ ave => ave.energia() }), tiempo total: [245 ms]

unSet.max(), tiempo total: [0 ms]

unSet.fold(0, { acum, ave => acum + ave.energia() }), tiempo total: [75 ms]

unSet.clear(), tiempo total: [0 ms]

assert.that(true), tiempo total: [1 ms]

const unSet = #{}, tiempo total: [0 ms]

(1 .. 1000).forEach{ i => 
	const nombre = "pepita" + i
	const ave = new AveLoca(nombre = nombre, energia = i)
	unSet.add(ave)
}, tiempo total: [136560 ms]

unSet.contains(unSet.anyOne()), tiempo total: [1 ms]

unSet.filter { ave => ave.energia() > 10 }, tiempo total: [47 ms]

unSet.map { ave => ave.energia() }, tiempo total: [183 ms]

unSet.anyOne(), tiempo total: [0 ms]

unSet.difference(#{unSet.anyOne()}), tiempo total: [286 ms]

unSet.sum({ ave => ave.energia() }), tiempo total: [169 ms]

unSet.max(), tiempo total: [1 ms]

unSet.fold(0, { acum, ave => acum + ave.energia() }), tiempo total: [51 ms]

unSet.clear(), tiempo total: [0 ms]

assert.that(true), tiempo total: [1 ms]

const unSet = #{}, tiempo total: [0 ms]

(1 .. 1000).forEach{ i => 
	const nombre = "pepita" + i
	const ave = new AveMagica(nombre = nombre, energia = i)
	unSet.add(ave)
}, tiempo total: [15247 ms]

unSet.contains(unSet.anyOne()), tiempo total: [1 ms]

unSet.filter { ave => ave.energia() > 10 }, tiempo total: [47 ms]

unSet.map { ave => ave.energia() }, tiempo total: [179 ms]

unSet.anyOne(), tiempo total: [1 ms]

unSet.difference(#{unSet.anyOne()}), tiempo total: [289 ms]

unSet.sum({ ave => ave.energia() }), tiempo total: [190 ms]

unSet.max(), tiempo total: [0 ms]

unSet.fold(0, { acum, ave => acum + ave.energia() }), tiempo total: [54 ms]

unSet.clear(), tiempo total: [0 ms]

assert.that(true), tiempo total: [1 ms]

const unSet = #{}, tiempo total: [0 ms]

(1 .. 1000).forEach{ i => 
	unSet.add("hola" + i)
}, tiempo total: [145 ms]

unSet.contains("hola88"), tiempo total: [43 ms]

unSet.filter { unString => unString.length().even() }, tiempo total: [380 ms]

unSet.map { unString => unString.length() }, tiempo total: [215 ms]

unSet.anyOne(), tiempo total: [1 ms]

unSet.difference(#{unSet.anyOne()}), tiempo total: [108 ms]

unSet.sum({ unString => unString.length() }), tiempo total: [217 ms]

unSet.max(), tiempo total: [1 ms]

unSet.fold(0, { acum, unString => acum + unString.length() }), tiempo total: [74 ms]

unSet.clear(), tiempo total: [0 ms]

assert.that(true), tiempo total: [1 ms]

const unSet = #{}, tiempo total: [0 ms]

const unSet = #{}, tiempo total: [1 ms]

(1 .. 1000).forEach{ i => 
	unSet.add(true)
}, tiempo total: [59 ms]

unSet.contains(false), tiempo total: [1 ms]

unSet.filter { unBoolean => unBoolean }, tiempo total: [1 ms]

unSet.map { unBoolean => not unBoolean }, tiempo total: [1 ms]

unSet.anyOne(), tiempo total: [0 ms]

unSet.difference(#{false}), tiempo total: [1 ms]

unSet.sum({ unBoolean => if (unBoolean) 1 else 0 }), tiempo total: [1 ms]

unSet.max(), tiempo total: [0 ms]

unSet.fold(0, { acum, unBoolean => acum + (if (unBoolean) 1 else 0) }), tiempo total: [1 ms]

unSet.clear(), tiempo total: [1 ms]

assert.that(true), tiempo total: [1 ms]

@npasserini
Copy link
Member

TL;DR lo que anda mal es el max() sobre list, fundamentalmente con objetos que no definen el <. Asumo que tiene que ver con la creación de toStrings que se agregó.

Se podría tunear, pero me parece un caso que no sólo cae fuera de la campana de Gauss sino que es ilógico (pedir el máximo de objetos que no definen <). Si quieren creamos un ticket para seguir iterando esto, pero yo mergearía esto como está.

@fdodino
Copy link
Collaborator

fdodino commented Sep 23, 2019

+1 a crear dos tickets:

  • 1 por List>>max
  • 1 por Set para que use otra estrategia si no tiene definido el > (está tardando también en agregar)

Y definitivamente +1 a mergear

@fdodino
Copy link
Collaborator

fdodino commented Sep 23, 2019

Ah, y una cosa que podemos ver es cuánto tarda en agregar 10 elementos, que supongo que es lo más común, no debería superar medio segundo...

@lspigariol
Copy link
Contributor

lspigariol commented Sep 23, 2019 via email

@npasserini npasserini merged commit 0bcee4b into dev Sep 23, 2019
@fdodino fdodino deleted the fix-#1771-inconsistencias-en-list.asSet branch November 17, 2019 19:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Inconsistencias en list.asSet()
7 participants