uqbar-project / wollok-language

Wollok language definition
GNU General Public License v3.0
7 stars 9 forks source link

Inicializaciones de atributos #62

Closed PalumboN closed 3 years ago

PalumboN commented 4 years ago

LINK AL DOCUMENTO CON LA VERSIÓN FINAL: https://docs.google.com/document/d/1DnRnRKnO3IDQya3NZJqj-U-6ICrAhgUhaqpuZfGjRFk/edit#

class C inherits D {
  const a
  const b = 2
  const c
  const d

  method initialize() {
    super()
  // Analizar qué cosas se inicializan (y sus dependecias) puede ser complejo
    a = a + 1 // acá es necesario pasar un a
    // a = algo.cosaLoca(self) // Acá es más difícil
    d = 1
  }
}

new C(a=1, c=3) // OK

new C(c=3) // ERROR: Falta a (caso complicado)

new C() // ERROR: Falta a y c

new C(a=1, b=5, c=3) // -> b queda con 5

new C(d=0, c=3) // WARNING: valor d está siendo definido en el initialize()

Tanto para var como const (sino forzar la inicialización a = null).

nscarcella commented 4 years ago

Bueno, paso en limpio las notas de la última reunión de roadmap.

Establecimos 5 usos para el initialize:

a. Ejecutar validaciones y preservar invariantes: Es discutible si el initialize es EL lugar para hacer esto, pero hoy Wollok no tiene una opción mejor (sobre todo si sacamos los constructores) porque los parametros del new no pasan por los setters. Personalmente, este es tal vez el único uso que realmente me importa del initialize, porque ya me saltaron varias veces alumnos con la duda de dónde validar cuando vemos excepciones.

Ej: Validar que el saldo de un cliente (parámetro de construcción) no sea negativo.

b. Lógica de inicialización: Evaluar cualquier regla de negocio que requiera evaluarse al inicio de la vida del objeto. Es importante para esto considerar en que estado de "correctitud" se encuentra el objeto cuando se ejecuta esta lógica: ¿Puede asumir que tiene todas sus variables Inicializadas? ¿Puede asumir que al menos tiene las que hereda? etc.

Este uso es bastante importante en mi opinión, porque yo hago mucho foco en que un objeto tiene estar siempre en un estado consistente y que eso hay que mantenerlo o fallar. Supongo que puedo evitar dar ejemplos donde esto sea un problema, pero creo que, por completitud, Wollok debería dejarme hacer esto.

Ej: Mantener una referencia cruzada entre alumnos y curso, haciendo que el alumno le mande un mensaje al curso que recibió en la construcción para que lo agregue a sus hijos.

c. Restringir los parámetros de instanciación: El razonamiento es que, si el initialize va a pisar lo que yo pase como parámetro a un new, podría resultar confuso. Con lo cual, podría ser deseable que los campos seteados en el initialize no sean obligatorios para el new.

Ej: Si mi initialize setea la energía máxima de una golondrina independientemente de lo que me pases en el constructor quisiera ver un mensaje de alerta cuando haga new Golondrina(energíaMáxima = 10) o que directamente no me dejes pasarle eso, porque va a ser ignorado. También querría que me deje hacer new Golondrina() (asumiendo que no hay otros campos sin defaults), porque obligarme a pasarle un valor que va a ignorar es rarísimo.

d. Inicializar atributo que depende de otro: Básicamente, si una clase tiene un atributo a que espera recibir en el new, y otro atributo b que se calcula a partir de a tiene que haber algún lugar dónde esto pueda resolverse. El initialize podría ser este lugar.

Acá yo haría la mención de que algunas constantes de Wollok aspiran (entiendo yo) a usar sólo expresiones puras y eso es lo que permite que su inicialización sea lazy. Por eso este test, por ejemplo, funciona:

const a = b
const b = 1

test "inicialización lazy" { assert.equals(a, 1) }

Acá hay unos tests que agregamos comentados en su momento que cubren algunas expectativas de lazyness en la inicialización.

Menciono esto porque la inicialización lazy es necesaria para tener objetos inmutables que se referencien mutuamente (super común) y entiendo que este es un tema sobre el que se busca avanzar.

Obviamente, de existir este feature, no habría ninguna necesidad de recurrir al initialize para definir un campo a partir de otro y podríamos directamente hacerlo inline (mucho más cómodo, en mi opinión).

Ej: La energía de una golondrina se inicializa en su energía máxima.

e. Reformular un valor que llega: La instanciación con parámetros nombrados tiene la limitación de que sólo recibe cosas que sean atributos. Esto implica que cualquier parámetro que no sea exactamente el valor final para un atributo es contramano y, a veces te obliga a hacer chanchadas para poder recibir un valor que querés usar para inicializarte pero no querés exponer. Esto es especialmente choto con constantes porque, de última, con una variable podés pisar el valor original con el valor procesado, pero eso no lo podés hacer con una constante, salvo que desactives la constancia de las constantes durante el initialize (cosa que a mi me parece super turbio).

Ej: Quiero construir un Guerrero con una fuerza (constante) que tiene que ser un número >=1. Si recibo un número menor quiero convertirlo en 1. Sin constructores, hoy tengo que hacer que la fuerza sea una variable o agregar un atributo fuerzaInicial para que me pasen y luego no usarlo nunca más.


También me gustaría mencionar un par de otras inquietudes que surgieron:


Paso entonces a contar la propuesta que terminamos hilando al final y agrego otra un poco más volada que se me ocurrió durante la reunión y a Nico pareció interesarle. En todos los casos voy a contarlo usando la sintáxis de linearización de 3.0 que discutimos acá.

Propuesta 1: initialize como método ordinario

Esta es la propuesta más simple de todas las que manejamos y, básicamente, propone descartar cualquier trato especial para el initialize y dejarlo como un método más, sin parámetros.

En esta variante, el new no considera al initialize en absoluto para decidir qué cosas son o no parámetros obligatorios (con lo cual se mantiene la idea de que cualquier parámetro que no tenga un default o sea pasado en la linearización de supertipos tiene que ser provisto en el new). Esto exime al initialize de la responsabilidad de "llenar huecos" y garantiza que el objeto ya tiene todos sus atributos (heredados o no) en algún valor default o parametrizado al momento de evaluarse, lo cual da cierto grado de seguridad de que podés enviar mensajes sin que todo explote. Al mismo tiempo evita tener que deducir si una variable se setea a través de un setter (que yo digo que es imposible de hacer sound).

El initialize se enviaría automáticamente (como pasa ahora) luego de la instanciación, garantizando que siempre que haces new obtenés un objeto inicializado. El orden de llamado de los initialize de la jerarquía está sujeto a las mismas reglas de overrideo y super que cualqueir otro método: requiriendo el uso de override para pisar el mensaje (que ya tiene una implementación vacía en Object) y quedando el llamado a super() como responsabilidad del programador.

A continuación cuento cómo queda cada uno de los usos mencionados más arriba con esta variante:

a. Ejecutar validaciones y preservar invariantes: El caso queda cubierto perfecto. Para cuando el initialize se llama ya se tienen todos los atributos seteados y se puede validar o ajustar lo que sea necesario.

// Validar que el saldo de un cliente (parámetro de construcción) no sea negativo.

class Persona {
  const nombre

  override method initialize() {
    super()  // No hace falta realmente llamar a super porque en Object no hace nada, pero es buena práctica
    if(nombre.isEmpty()) self.error("El nombre está vacío")
  }
}

class Cliente inherits Persona {
  var saldo = 0

  override method initialize() {
    if(saldo < 0) self.error("El saldo no puede ser negativo")
    super() // Puedo llamar a super en cualquier punto
  }
}

...

new Cliente()                              // ERROR! Hay que pasarle un nombre
new Cliente(nombre = "Diego")              // OK! Pasan las validaciones y saldo defaultea a 0
new Cliente(nombre = "Jose", saldo = 100)  // OK! Pasan las validaciones y saldo queda en 100
new Cliente(nombre = "")                   // OK! Las validaciones fallan y el objeto nunca llega nuestras manos

b. Lógica de inicialización: Idem (a). Todo piola.

// Mantener una referencia cruzada entre alumnos y curso, haciendo que el alumno le mande un mensaje al curso que recibió en la construcción para que lo agregue a sus hijos.

class Curso {
  const alumnos = []

  method inscribir(alumno) { alumnos.add(alumno) }
}

class Alumno {
  const curso

  override method initialize() {
    super()
    curso.inscribir(self)
  }
}

...

const curso = new Curso()

new Alumno(curso = curso)  // OK! El alumno queda asociado al curso y viceversa.

c. Restringir los parámetros de instanciación: En esta variante el initialize no se considera para decidir los parámetros de instanciación ni puede usarse para setear constantes.

// Energía máxima de una golondrina

class Golondrina {
  const energíaMáxima

  override method initialize() {
    super()
    // energíaMáxima = 100  // ERROR! Las constantes no se pueden pisar. De última convertila en variable.
  }
}

new Golondrina(energíaMáxima = 100) // OK!
new Golondrina()                    // ERROR! Hay que pasar el máximo, no puede setearse en ningún otro lugar.

d. Inicializar atributo que depende de otro: Esta variante no sirve para esto y requiere que la inicialización de atributos sea lazy o se ponga un placeholder (sólo sirve para variables).

// La energía de una golondrina se inicializa en su energía máxima.

class Golondrina {
  var otraEnergía = energíaMáxima // inicialización lazy
  const energíaMáxima
  var energía

  override method initialize() {
    super()
    energía = energíaMáxima // OK! Esto vale porque energía es una variable, no una constante, pero sería mejor hacerlo inline.
  }
}

new Golondrina(energíaMáxima = 100)                // ERROR! Hay que pasar energía, no importa que se setee en el initialize. Por eso hubiera sido mejor la variante inline o ponerle un default.
new Golondrina(energíaMáxima = 100, energía = 100) // OK! No necesito pasar otraEnergía.

e. Reformular un valor que llega: Esta variante no sirve para esto y requiere un workaround.

// construir un Guerrero con una fuerza (constante) que tiene que ser un número >=1. Si recibo un número menor quiero convertirlo en 1.

class Guerrero {
  const fuerza
  var velocidad
  const inteligenciaSugerida  // Workaround turbio: Pongo esta const para que me la pasen por parámetro, pero sólo la uso para Inicializar
  const inteligencia = inteligenciaSugerida.max(1)

  override method initialize() {
    super()
    // fuerza = fuerza.max(1)    // ERROR! No puedo pisar const
    velocidad = velocidad.max(1) // OK! usé un var para velocidad en lugar de una const
  }
}

new Guerrero(fuerza = -1, velocidad = -1, inteligenciaSugerida = -1)                   // OK! fuerza, velocidad e inteligencia quedan en 1
new Guerrero(fuerza = -1, velocidad = -1, inteligenciaSugerida = -1, inteligencia = 0) // OK! Me cagaste el workaround...

Mi opinión es que esta solución es super simple y suficientemente buena. Todos los casos que no cubre son más bien raros y tienen workarounds simples. Además, al ser un método, nos evita introducir un concepto extra y mantiene simples los conceptos de new y const. Para un lenguaje productivo podría no alcanzarme, pero para Wollok creo que va como trompada.


Propuesta 2: initialize como bloque parametrizado (o constructor++)

Quiero hacer el disclaimer de que esta variante me la saqué del culo y la estoy tocando como sale.

La idea era tratar de presentar una alternativa menos simple pero más poderosa que cubra todos los casos que la otra alternativa deja medio afuera.

Básicamente, en esta variante el initialize es un bloque que tiene parámetros nombrados.

El new espera recibir todos los atributos que no tengan default o sean pisados por una asignación en el initialize y todos los parámetros del initialize.

El initialize se ejecuta después de que los defaults y el pasaje de parametros tuvo lugar y tiene todos esos valores disponibles.

El initialize no es un método y no puede (ni necesita) llamar a super. Los parametros para el initialize del supertipo se pasan igual que en la otra variante: en el new o la linearización. El orden de evaluación es de arriba para abajo en la jerarquía, cómo es en C# o JavaScript, sólo que sin obligarte a escribirlo vos. Esto garantiza que el objeto siempre recibe los atributos de sus supertipos consistentes y listos para usar. Cualquier template loco que quieran hacer se puede hacer llamando a un mensaje y poniendo el super ahí.

Así quedarían los usos:

a. Ejecutar validaciones y preservar invariantes: Funciona igual que la Propuesta 1.

// Validar que el saldo de un cliente (parámetro de construcción) no sea negativo.

class Persona {
  const nombre

  initialize() {
    if(nombre.isEmpty()) self.error("El nombre está vacío")
  }
}

class Cliente inherits Persona {
  var saldo = 0

  initialize() {
    // Cualquier referencia a 'nombre' puede hacerse normalmente. Si llegaste acá nombre está seteado y cumple las espectativas del supertipo.
    if(saldo < 0) self.error("El saldo no puede ser negativo")
  }
}

...

new Cliente()                              // ERROR! Hay que pasarle un nombre
new Cliente(nombre = "Diego")              // OK! Pasan las validaciones y saldo defaultea a 0
new Cliente(nombre = "Jose", saldo = 100)  // OK! Pasan las validaciones y saldo queda en 100
new Cliente(nombre = "")                   // OK! Las validaciones fallan y el objeto nunca llega nuestras manos

b. Lógica de inicialización: Mismas capacidades que la Propuesta 1.

// Mantener una referencia cruzada entre alumnos y curso, haciendo que el alumno le mande un mensaje al curso que recibió en la construcción para que lo agregue a sus hijos.

class Curso {
  const alumnos = []

  method inscribir(alumno) { alumnos.add(alumno) }
}

class Alumno {
  const curso

  initialize() {
    curso.inscribir(self)
  }
}

...

const curso = new Curso()

new Alumno(curso = curso)  // OK! El alumno queda asociado al curso y viceversa.

c. Restringir los parámetros de instanciación: Tanto variables como constantes (incluyendo las heredadas) pueden ser inicializadas (pero no sobrescritas si son const) en el initialize, similar a lo que hacen lenguajes como TypeScript y Kotlin. Esto vuelve el concepto de constante un poco más complejo a cambio de dar más control en su inicialización.

// Energía máxima de una golondrina

class Golondrina {
  const energíaMáxima

  initialize() {
    energíaMáxima = 100                  // OK!
    // energíaMáxima = energíaMáxima + 1 // ERROR! no se puede sobreescribir una const
  }
}

new Golondrina(energíaMáxima = 100) // ERROR! energíaMáxima ya está inicializada en el initialize
new Golondrina()                    // OK!

d. Inicializar atributo que depende de otro: Similar a la propuesta 1, pero depende menos de tener lazyness.

// La energía de una golondrina se inicializa en su energía máxima.

class Golondrina {
  var otraEnergía = energíaMáxima // inicialización lazy
  const energíaMáxima
  var energía

  override method initialize() {
    energía = energíaMáxima // OK!
  }
}

new Golondrina(energíaMáxima = 100)                    // OK! No hace falta (y de hecho, no puedo) pasar energía.
new Golondrina(energíaMáxima = 100, energía = 100)     // ERROR! No puedo pasar energía
new Golondrina(energíaMáxima = 100, otraEnergía = 100) // OK! Si tenemos lazyness podés defaultear otraEnergía al máximo y aun así pisarla.

e. Reformular un valor que llega: Este es el punto donde esta propuesta es mejor la 1. Teniendo parámetros en el initialize que no son atributos podés pasar falopa de inicialización sin tener que agregarsela a interfaz del objeto.

// construir un Guerrero con una fuerza (constante) que tiene que ser un número >=1. Si recibo un número menor quiero convertirlo en 1.

class Guerrero {
  const fuerza
  const inteligencia = 0

  initialize(fuerzaSugerida) {
    fuerza = fuerzaSugerida.max(1) // OK!
  }

  method fuerzaSugerida() {
    return fuerzaSugerida          // ERROR! fuerzaSugerida no existe
  }
}

new Guerrero(fuerzaSugerida = -1, inteligencia = 10)              // OK! fuerza queda en 1
new Guerrero(fuerzaSugerida = -1, fuerza = -1, inteligencia = 10) // ERROR! No le podés pasar fuerza porque se setea en el initialize

Yo creo que es indiscutible que esta solución es más poderosa que la (1), peeeeero la pregunta es si ese poder vale la pena. Para un lenguaje productivo estoy seguro que me gusta la posibilidad de recibir no-atributos en la construcción. Eso es también más parecido a un constructor y otras cosas que hay en el mercado. Pensando en un aula no estoy tan seguro... Yo el initialize sólo lo uso para el caso (a) en clase, con lo cual un método me alcanza y sobra. Por otro lado, la complejidad de usar la propuesta 2 es puramente optativa: si no querés contar los parámetros podés sólo no hacerlo y se parece mucho a la opción 1. Pero bueno, no sé... Me gustaría saber que opinan ustedes. A mi cualquiera de estas dos variantes me harían feliz. En particular quiero evitar la "desactivación" de validaciones, quisiera que todas las reglas sean lo más generales y claras posible.

npasserini commented 4 years ago

Excelente resumen Nico. La verdad es delicado el tema, hay muchas cosas para pensar. Voy con algunas:

0) Quiero arrancar aclarando algo sobre la propuesta que hice en la reunión porque entendí que fue confuso.

Lo concreto es: tomando como base el primer ejemplo de este thread opino que los casos de las referencias a y d son malos usos del initialize. Si queremos que el lenguaje tenga esas capacidades ok a discutir cómo incorporarlas, pero deberían ir por otro camino.

Luego, mi propuesta iba en la línea de "si a alguien le parece importante ese feature y está utilizando el initialize para eso, yo estaría dispuesto a aceptar esos usos, al menos mientras no tengamos un mecanismo mejor para ofrecer y con la condición de que no lo publiquemos como la forma de hacer eso en wollok".

Enfatizo el punto de aceptar, distinto de proponer eso como camino. Si tenemos acuerdo en ser más rigurosos y prefieren directamente prohibir eso por mí no hay problema (aunque también imagino que la lógica para detectar esos casos y marcarlos como errores no es trivial).

1) Luego, la cuestión de los features / principios / objetivos, me parece lo más importante para guiar la discusión. Retomo la lista que documentó Nico

a. Ejecutar validaciones y preservar invariantes: Nico dice: Es discutible si el initialize es EL lugar para hacer esto Yo digo que si bien podría haber herramientas más locas, me parece un uso sano del initialize, debería permitirse.

b. Lógica de inicialización: Creo que coincidimos con Nico en que es EL uso del initialize.

c. Restringir los parámetros de instanciación: Este me parece un mal uso del initialize. Como dije antes, eventualmente podría aceptar que no tengamos validaciones para impedirlo, pero no quisiera que este uso guíe nuestras decisiones. Nico planteó que era un feature necesario, yo no estoy super convencido, pero no me opondría a que se agregue un mecanismo para eso(uno que no sea el initialize). Acá hay más detalles, ver punto 2 más abajo.

d. Inicializar atributo que depende de otro Me parece un uso lícito. Sin embargo, después de analizarlo largo en la reunión quedó como que era el único motivo que generaba varias complicaciones (ver punto 3) y si fuera así podría bajarme de este feature.

e. Reformular un valor que llega También me parece un mecanismo retorcido. No me queda claro que sea super necesario y en todo caso propondría otros mecanismos para hacerlo. De todas maneras, como dije también en el punto c, yo podría aceptar que no se valide esto y si alguien lo quiere hacer, adelante. Al menos mientras no tengamos una propuesta mejor.

2) En la discusión sobre lo que es una const o no llegamos a la conclusión de que hay dos conceptos distintos a los que tal vez les estamos poniendo el mismo nombre de forma confusa.

Hoy la const de Wollok define un atributo que es inmutable. El mejor ejemplo me parece este:

class Point { const property x const property y }

El punto es inmutable, por eso el uso de const. Pero además quiero poder recibir x e y por parámetro. Wollok le cae como anillo al dedo a ese ejemplo.

La alternativa es el uso de const como una forma de "darle nombre" a un valor que aparece en el código y no queremos repetir o justamente queremos darle un nombre. Por ejemplo:

class Circulo { const pi = 3.14 const radio

method superficie() = pi radio radio method perimetro() = pi radio 2 }

Claramente no espero que alguien me pase pi por parámetro en el constructor.

Acá yo creo que aparece una cuestión que primero tenemos que discutir desde la didáctica. ¿Cuáles son los conceptos que queremos enseñar? ¿Cómo los contamos? Y luego que eso nos guíe a diseñar el lenguaje.

Opino que los dos usos de const que mostré son conceptos radicalmente distintos y que el hecho de que estemos usando const para los dos está sesgando nuestramente a pensar que son parecidos, sólo porque el lenguaje no tiene otra herramienta. Si fuera C, la segunda const sería un define. Si fuera Smalltalk sería una variable de clase. Si fuera JS probablemente sería una const, pero la pondría fuera de la clase. En Java seguramente usaría static final. En Wollok usamos const. ¿Cuál de todas esas formas refleja mejor lo que queremos modelar?

La verdad no tengo LA respuesta, lo que sí estoy tentado de afirmar es que el juego acá no va por la idea de "restringir los parámetros que me pueden pasar en el constructor", eso es un sesgo que nos está imponiendo Wollok.

No sé, a lo mejor nos pasa que descubrimos que necesitamos variables/constantes de clase. Ahora, yo pregunto, ¿qué tan grave es usar esta versión?

const pi = 3.14 class Circulo { const radio

method superficie() = pi radio radio method perimetro() = pi radio 2 }

No sé, tal vez sólo resuelve mi ejemplo y hace agua en otros.

En fin... en lo que sigue se piensa en variables y constantes "que no se pueden enviar por parámetro en el momento de construcción"... no sé si es la mejor manera de modelar lo que queremos, pero hay que admitir que es un feature versátil, quiero decir, me permite modelar lo que hice con pi arriba, pero también otras cosas de naturaleza distinta. Creo que da para pensar qué camino quere3mos.

3) Con esas ideas en mente, al final de la reunión llegamos a esta propuesta (que medio descartamos, pero lo cuento para que se entienda el hilo y el por qué).

Acá hay dos ideas. a. En primer lugar que uno puede decidir qué variables/constantes se pueden recibir por parámetro y cuáles no. No encontramos una sintaxis que satisficiera a todos para eso, así que puse una que claramente no es candidata, pero muestra el punto. b. Pensar el initialize como bloque y entender qué reglas especiales queremos para poder lograr algún objetivo puntual.

class C {
  noparametrizable var a = 1 // Prohibida en el constructor
  var b = 1 // Opcional, valor 1 por default
  var c // Obligatoria en el constructor

  noparametrizable const d = 1 // Prohibida en el constructor
  const e = 1 // Opcional, default 1
  const f // Obligatoria en el constructor 

  initialize {
    // Con eso, nos preguntamos cuáles se podrían pisar en el initialize.
    // Para las variables, me parece que no tienen mucho sentido pero hay que ver los usos.
    // Podría tirar warnings, pero en definitiva, siempre es válido pisar una variable, con lo cual no sé qué validar. 
    a = 2 
    b = 2
    c = 2

    // Las constantes se ponen más interesantes.
    // d = 2 => inválido, no tiene sentido lo definiste inline y acá de nuevo, camine cucha.
    // e = 2 => inválido, no quiero permitir que pises el valor del constructor, si querés valor fijo poné noparametrizable

    // ----------------
    // Esta es la papa
    // ----------------
    f = e + 1 => tiene sentido, depende de un valor recibido.
  }
}

A lo que llegamos es que toooodo este lío de tener un initialize que fuera un bloque de código es simplemente para el caso f. Parece demasiada burocracia por un chiche menor. Y para colmo Nico dice que eso se puede hacer sin initialize en virtud de la inicialización lazy de variables, tranquilamente podría escribir esto (pongo sólo la parte importante):

class C {
  const e = 1
  const f = e + 1
}

Y si hago new C(e=2), luego f vale 3. Entiendo que eso ya funciona hoy. Deberíamos validarlo.

Luego de todo esto, medio que nos convencimos de dejarnos de joder con el bloque, y tener un método. Pero quedábamos más bien Nico y yo, habría que ver qué opina el resto.

4) Finalmente, sobre el initialize con parámetros... creo que podría ser algo para analizar. Entiendo que puede parecer un volantazo porque parece que reintroducimos los constructores con otro nombre, pero quiero argumentar que no es un retroceso. A saber:

a. Los constructores son burocráticos y generan gran cantidad de código sólo para copiar parámetros en atributos. En esta solución, usarías los parámetros nombrados para eso y no tenés tanta burocracia.

b. Los constructores tienen reglas especiales, distintas a los métodos en más de un aspecto. Eso en el initialize se mantendría, pero termina siendo opcional, uno bien podría no enseñarlo o de mínima dejarlo para una etapa muy avanzada.

c. Los constructores son más poderosos que la inicialización automática por parámetros nombrados, en el sentido de que permiten muchas más variantes. El initialize con parámetros tendría la misma potencia.

En resumen, veo

PalumboN commented 4 years ago

Buenas!! Muy piola todo lo expuesto. Siendo conciso, después de la charla del otro día, mi opinión respecto a los puntos mencionados que son: a. Ejecutar validaciones y preservar invariantes b. Lógica de inicialización c. Restringir los parámetros de instanciación d. Inicializar atributo que depende de otro. e. Reformular un valor que llega

Concuerdo plenamente que los usos que me interesan son para a) y b). Y como decía NicoP para mí este es el foco que quiero darle al initialize, y pensarlo para esto. De hecho, después de la reunión del otro día, creo que no quiero darle más responsabilidades al initialize que no sean esas. (Y con esto ya voy spoileando la propuesta que más me gusta).

Sobre el punto c) y la discusión de qué tan constantes son las const de Wollok la quiero patear a otro hilo. Me gusta lo que se dijo de atacar este problema por otro lado, como trabajando más el concepto de referencia y sus variantes. No me parece, por lo menos en primer medida, algo a tener en cuenta al momento de difinir el alcance del initialize. Una vez que lleguems a un acuerdo mínimo y lo vea en acción podemos ver cómo queremos que se lleven, pero por ahora no lo tendría en el alcance y atacaría ese problema por otro lado.

Sobre el punto d), ok si pasa y usás el initialize para eso, pero no quiero que tenga ninguna regla rara proveniente de este punto (como por el ejemplo, el dejar pisar las const, que comento más abajo). Lo tomaría como un caso especial de b) o de e).

Sobre el punto e), que es el que menos me gusta, prefiero descartarlo totalmente. Si para crear tu objeto necesitás parámetros que al objeto en cuestión no les interesa, entonces que esos parámetros nunca deberían llegar al objeto. No veo una necesidad de hacer que el objeto creado los conozca (aunque sea por un período de tiempo). Para mí querés un factory.

Y después hay otro cuestión que me hace cortocircuito en la cabeza y es este:

ENTONCES dicho todo esto, yo voto por el initialize como método sin validaciones locas, con la característica que se llama automáticamente luego de hacer new. Ya que eso es lo más simple para cubrir los casos que me interesan. FAQ

nscarcella commented 4 years ago

Sobre el punto e), que es el que menos me gusta, prefiero descartarlo totalmente. Si para crear tu objeto necesitás parámetros que al objeto en cuestión no les interesa, entonces que esos parámetros nunca deberían llegar al objeto. No veo una necesidad de hacer que el objeto creado los conozca (aunque sea por un período de tiempo). Para mí querés un factory.

Acá, si bien estoy de acuerdo, me parece que capaz estás siendo muy categórico. O bueno creo que, si convenimos que "la creación" del objeto es una etapa de su vida y hacemos foco en la separación entre esta y la vida "útil" del objeto, no es tan loco pensar que podrías tener referencias que sean útiles en esta etapa pero no hagan falta en la siguiente. Se arregla con patrones creacionales? Yo creo que sí, pero ahí es donde entra el hilado fino de esto fuerza a presentar conceptos antes de tiempo o no.

Qué se yo... Recuerdo que en su momento hubo bastante discusión sobre si permitir o no la sobrecarga de los constructores. Yo me lo pasaba por los huevos y decía lo mismo que estás diciendo vos ahora, pero habían varios que sentían que era un feature muy importante porque a veces querés construir así y a veces asá y capaz no querés desviarte a armar un builder.

Obvio, son cosas distintas, pero bueno...

Y después hay otro cuestión que me hace cortocircuito en la cabeza y es este:

  • Es importante para nosotros que al momento de usar los objetos, estén en un estado consistentes.
  • Dentro del initialize podés enviar mensajes.
  • Si el initialize forma parte de la construcción del objeto, puede que ahí se manden mensajes y se use al objeto cuando todavía podría no está en un estado consistente. Entonces choca me choca con la primera regla: solo se quiere usar el objeto cuando se encuentra en un estado consistente, pero necesito usar el objeto para lograr ese estado exploding_head

Sí, esto es un tema, pero fijate que, hasta cierto punto, es inevitable. Si vos no podés hacer que inicialización del objeto sea atómica entonces necesariamente vas a tener etapas dónde el objeto es menos consistente que otras. Por ejemplo, dentro de los constructores de C# o Java, this no apunta a un objeto "consistente", justamente porque parte de lo que el objeto necesita para ser consistente es lo que vas a ejecutar dentro de ese constructor.

Con lo cual no hay realmente forma de hacer que el initialize sea "un método más". O sea, sí, podés implementarlo en el lenguaje usando métodos, pero ese método va a ser siempre "especial", en el sentido que tenés que tener cuidado qué es lo que haces adentro porque el objeto no está "listo". En ese sentido, un bloque especial es un poco más honesto...

Pero lo realmente importante es que, método o no método, cuando alguien hace new MiClase(...) eso sí devuelve un objeto consistente al que el usuario no tiene que setearle boludeces después. Internamente el objeto define sus etapas y sabe dónde cuidarse y dónde está tranquilo, pero para el usuario la instanciación sí es atómica y lo que recibe es un objeto listo para usar. Esto va de la mano de otro aspecto que me parece la papa de la programación, que es Compartamentalizar, y aprender a aislar el quilombo en un lugar.

FAQ

  • ¿Entonces también es un método que se pueda llamar en cualquier parte del código? Sí, es un método más, salvo que la implementación de Wollok lo conoce (como el toString()). Podría agregarse un warning o info al momento de usarlo, si lo prefieren.

Si lo ponés como un método para mi no hace falta warning.

  • ¿Cómo funciona con la herencia? Como cualquier método, como explica NicoS. Para mí esto es superador a los constructores (que tenían una sintaxis especial)

Definitivamente las dos opciones que hay arriba son mejores que el quilombo de superllamado que tienen los constructores de cualquier lenguaje.

  • ¿Cuál sería la semántica del initialize? Es un método que se ejecuta después de la creación del objeto y estaría en un estado consistente. Serviría específicamente para los casos a) y b).

Consistente asterísco. Tiene todas sus variables inicializadas (heredadas o no). Es null-safe, si querés. Consistencia es un concepto que depende del dominio. La opción no-método podría ser null-safe o no, dependiendo de lo que querramos.

  • ¿Y qué onda las reglas que aplicaba a los constructores (como poder asignar const's)? Mueren. Esas reglas eran necesarias para poder tener const's parametrizables, eso ya está solucionado con la inicialización nombrada. Sí, perdimos poder, pero el reinado de los constructores no triunfó en la comunidad y prefiero la simpleza que tendríamos ahora. Para el 90% se simplificaron las cosas, el 9% le estamos buscando solución, y para el 1% mi respuesta es "por ahora delegá en otro objeto" (quizá se podría buscar una solución más piola para esos casos que quedan fuera, pero no la buscaría acá, sino por otro lado que todavía no sé).

Te sacaste esos números del culo, pero 110% de acuerdo.

fdodino commented 4 years ago

Bueno, impresionante el nivel de charla. Definitivamente prefiero la opción 1, en estos tres años en el que dejé de dar constructores los casos raros los manejé con factories (e incluso di Builder) y me fue bien. Me queda la duda de si el initialize() también debería ser un método para los describes (entiendo que sí). Una cosa loca que probé es que

class C {
  const e = 1
  const f = e + 1
}

si hago new C(e = 2) me deja tanto f como e en 2. Puede ser un bug.

Hacer esto:

class C {
  const f = e + 1
  const e = 1
}

produce error: ERROR: No se puede resolver la referencia e.

El último caso, acá:

class Guerrero {
  const fuerza
  var velocidad
  const inteligenciaSugerida  // Workaround turbio: Pongo esta const para que me la pasen por parámetro, pero sólo la uso para Inicializar
  const inteligencia = inteligenciaSugerida.max(1)

  override method initialize() {
    super()
    // fuerza = fuerza.max(1)    // ERROR! No puedo pisar const
    velocidad = velocidad.max(1) // OK! usé un var para velocidad en lugar de una const
  }
}

Yo preferiría para eso usar un método:

self.inteligencia()....

// donde
method inteligencia() = inteligencia.max(1)

me hace ruido ese caso de uso, como ya lo mencionaron antes Nahue, NicoS y NicoP.

En definitiva, creo que me gusta pensar en la opción 1 como la más simple y didáctica.

lspigariol commented 4 years ago

Flor de explicación!! Seguimos agrandando la frondosa biblioteca de criterios y estrategias de crear e inicializar objetos. Es interesante ver que hay viejas ideas que vuelven, nuevas propuestas, y que se renueva la discusión entre versatilidad y simplicidad, la ponderación de ventajas y desventajas. Sigo sin tener una posición determinante, coincido en que los casos conflictivos son poco frecuentes y que tiene que ver más con cómo nos imaginamos el lengueje más que la forma de dar clase, al menos en una materia inicial de objetos donde al construir nosotros los ejercicios podemos esquivar fácilmente los casos raros. Pero si me apuran, estoy mas cerca de la 2 que de la 1.

Un nuevo punto de vista desde el que intento analizar los planteos es desde el manejo de efecto. Puntualmente, me puse a pensar el new como que no causa efecto, al igual que los métodos que son "preguntas", donde el objeto que devuelven puede ser tanto uno nuevo como uno existente. Si bien no se valida que un método que retorna a la vez cause efecto, entiendo que es una práctica que no recomendamos.

Esto me hace pensar que en el caso b. Lógica de inicialización:

No está bueno actualizar otros objetos estilo mundo.enterateQueNacio(self) o hacer una referencia cruzada tipo curso.inscribir(self)

Una cuestión de secuencia didáctica. En caso de necesidad, quisiera que se pueda entender el initialize sin saber herencia, por lo que por ejemplo la utilización de super y override no me gustaría que fuera obligatoria ni recomendable.

Bueno, impresionante el nivel de charla. Definitivamente prefiero la *opción

1*, en estos tres años en el que dejé de dar constructores los casos raros los manejé con factories (e incluso di Builder) y me fue bien. Me queda la duda de si el initialize() también debería ser un método para los describes (entiendo que sí). Una cosa loca que probé es que

class C {

const e = 1

const f = e + 1

}

si hago new C(e = 2) me deja tanto f como e en 2. Puede ser un bug.

Para mi no es un bug. El código está queriendo decir que el valor por default de f es el valor por default de e + 1. Como la inicialización con parámetros es una fase posterior, tiene sentido que pise esos valores. "Estos son los valores iniciales del objeto si no me mandas nada, si me los querés pasar hace cargo vos..." Se está permitiendo hacer new C(e=2, f= 17) Si quisiera que siempre f sea uno más que e, f debería ser un método.

Y después hay otro cuestión que me hace cortocircuito en la cabeza y es este:

Sí, esto es un tema, pero fijate que, hasta cierto punto, es inevitable. Si vos no podés hacer que inicialización del objeto sea atómica entonces necesariamente vas a tener etapas dónde el objeto es menos consistente que otras. Por ejemplo, dentro de los constructores de C# o Java, this no apunta a un objeto "consistente", justamente porque parte de lo que el objeto necesita para ser consistente es lo que vas a ejecutar dentro de ese constructor.

Con lo cual no hay realmente forma de hacer que el initialize sea "un método más". O sea, sí, podés implementarlo en el lenguaje usando métodos, pero ese método va a ser siempre "especial", en el sentido que tenés que tener cuidado qué es lo que haces adentro porque el objeto no está "listo". En ese sentido, un bloque especial es un poco más honesto... +1. Mas allá que pueda ser un metodo, es un método "especial" y requiere una explicación especial. Me deja pensando lo de la "honestidad" y la conveniencia o no de usar la misma sintaxis para algo que no es lo mismo...

Pero lo realmente importante es que, método o no método, cuando alguien hace new MiClase(...) eso sí devuelve un objeto consistente al que el usuario no tiene que setearle boludeces después. Internamente el objeto define sus etapas y sabe dónde cuidarse y dónde está tranquilo, pero para el usuario la instanciación sí es atómica y lo que recibe es un objeto listo para usar. Esto va de la mano de otro aspecto que me parece la papa de la programación, que es Compartamentalizar, y aprender a aislar el quilombo en un lugar.

+1.

Opino que los dos usos de const que mostré son conceptos radicalmente distintos y que el hecho de que estemos usando const para los dos está sesgando nuestramente a pensar que son parecidos, sólo porque el lenguaje no tiene otra herramienta. Si fuera C, la segunda const sería un define. Si fuera Smalltalk sería una variable de clase. Si fuera JS probablemente sería una const, pero la pondría fuera de la clase. En Java seguramente usaría static final. En Wollok usamos const. ¿Cuál de todas esas formas refleja mejor lo que queremos modelar?

La verdad no tengo LA respuesta, lo que sí estoy tentado de afirmar es que el juego acá no va por la idea de "restringir los parámetros que me pueden pasar en el constructor", eso es un sesgo que nos está imponiendo Wollok.

No sé, a lo mejor nos pasa que descubrimos que necesitamos variables/constantes de clase. Ahora, yo pregunto, ¿qué tan grave es usar esta versión?

const pi = 3.14 class Circulo { const radio

method superficie() = pi radio radio method perimetro() = pi radio 2 }

No sé, tal vez sólo resuelve mi ejemplo y hace agua en otros.

En este ejemplo cierra, porque pi es unico en todo el programa y tiene mas sentido definirse fuera que dentro de la clase. Si hubiera diferentes clases que tiene cada una su propio pi, no se podría hacer, o habria que buscar diferentes identificadores. Pero también es un caso muy poco frecuente

  1. Con esas ideas en mente, al final de la reunión llegamos a esta propuesta (que medio descartamos, pero lo cuento para que se entienda el hilo y el por qué).

Acá hay dos ideas. a. En primer lugar que uno puede decidir qué variables/constantes se pueden recibir por parámetro y cuáles no. No encontramos una sintaxis que satisficiera a todos para eso, así que puse una que claramente no es candidata, pero muestra el punto.

Esta idea que estuvimos hablando me parece razonable.

b. Pensar el initialize como bloque y entender qué reglas especiales queremos para poder lograr algún objetivo puntual.

class C {

noparametrizable var a = 1 // Prohibida en el constructor

var b = 1 // Opcional, valor 1 por default

var c // Obligatoria en el constructor

noparametrizable const d = 1 // Prohibida en el constructor

const e = 1 // Opcional, default 1

const f // Obligatoria en el constructor

initialize {

// Con eso, nos preguntamos cuáles se podrían pisar en el initialize.

// Para las variables, me parece que no tienen mucho sentido pero

hay que ver los usos.

// Podría tirar warnings, pero en definitiva, siempre es válido

pisar una variable, con lo cual no sé qué validar.

a = 2

b = 2

c = 2

// Las constantes se ponen más interesantes.

// d = 2 => inválido, no tiene sentido lo definiste inline y acá

de nuevo, camine cucha.

// e = 2 => inválido, no quiero permitir que pises el valor del

constructor, si querés valor fijo poné noparametrizable

// ----------------

// Esta es la papa

// ----------------

f = e + 1 => tiene sentido, depende de un valor recibido.

si no me perdí en el camino, esto tiene sentido si f es noparemetrizable. Como no se la pasan, la setea el objeto. Como el valor depende de parámetros en vez de in line, lo inicializo en el initialize

}

}

Con lo del initalize con parametros puede estar bueno, no tengo posición clara, pero en principio suena interesante. Entiendo que esta bueno seguir separando lo que es "atributo" de "parametro necesario para crear el objeto". La definicion de "noparametrizable" permite tener atributos sin que esté el parámetro. Esto permitiría tener parámetros sin que esté el atributo. Es un esquema mas complejo, pero no afecta el uso simple.

npasserini commented 4 years ago

Paso en limpio algunas decisiones:

nscarcella commented 4 years ago

Entonces:

class C { a = 1 b = a + 1 }

new C(a = 2) // b = 3 new C(b = 3) // a = 1

fdodino commented 3 years ago

@nscarcella @npasserini @PalumboN @lspigariol @tesonep @asanzo estoy en este punto y tengo algunas cuestiones implementativas:

Si alguien tiene un -1 que escriba el primer comment...

asanzo commented 3 years ago

El override yo lo quitaría de la faz de la tierra, así que sobre el override no opino.

fdodino commented 3 years ago

Ok, yo no es que proponga algo, tengo que implementar algo que es diferente :smile:

Y el tema es que muchas veces armamos debates super piolas, pero en la especificación nos quedamos cortos. Por ejemplo, hablamos mucho de si poner el override o no, de si hacer una llamada a super, y mi comentario venía de la mano de lo que anotó Nico como conclusión de aquella noche:

  • El initialize es un método
    • No debe ser necesario usar super() en las clases que heredan de Object directamente.

Entonces, yendo a lo concreto me desdigo del último comentario y este test ya pasa, es parte de lo que está especificado arriba:

class A {
    const property a

    method initialize() {
        a = 1
    }
}

class B inherits A {
    var property b

    method initialize() {
        super()
        b = 2
    }
}

describe "initialize is automatically called for all elements in the hierarchy" {
    const b = new B()

    test "a initial value" {
        assert.equals(1, b.a())
    }

    test "b initial value" {
        assert.equals(2, b.b())
    }

}

describe "const references can be additionally assigned in a fixture" {
  const uno = 1

  method initialize() {
    uno = 1
    uno = 1
  }

  test "uno es uno" {
    assert.equals(1, uno)
  }
}

O sea, sí a usar super para subclases concretas, no hace falta usar override (estoy toqueteando el validador) y sí puedo usar super() aun cuando el método no tiene el override (toque que necesita el validador).

Graciasss

asanzo commented 3 years ago

Ok. Si no usas super() en el initialize entonces no se hace lo de la superclase, ¿verdad?

fdodino commented 3 years ago

No, y me gusta la idea de probar lo que vos dijiste, donde el super() se puede invocar en el momento que vos quieras y hacer cosas antes y después.

fdodino commented 3 years ago

Hola @nscarcella ,

Entonces:

class C { a = 1 b = a + 1 }

new C(a = 2) // b = 3 new C(b = 3) // a = 1

este test no me funciona:

class C {
  var property a = 1
  var property b = a + 1
}

describe "lazy initialization on instances" {

  test "overriding default initialization: variable -> value" {
    assert.equals(3, new C(a = 2).b())
  }

}

pero si lo cambio a

class C {
  var property a = 1
  var property b

  method initialize() {
    b = a + 1
  }
}

describe "lazy initialization on instances" {

  test "overriding default initialization: variable -> value" {
    assert.equals(3, new C(a = 2).b())
  }

}

anda de 10 (ya estuve toqueteando además el validador para que considere las variables que están asignadas en el método initialize). La cuestión es que la variante inline, si bien es más prolija, obliga a reformar bastante más la lógica que hoy tenemos, donde

  1. se inicializan las declaraciones de variables (lazy)
  2. luego los named parameters
  3. luego se ejecuta el método initialize

Tendría que hacerse:

  1. ejecutar los named parameters
  2. inicializar el resto de las variables que no se hayan inicializado en named parameters (lazy)
  3. ejecutar el método initialize

El tema es que hay que hacer un refactor bastante groso para mejorar lo que hoy tenemos porque están los wko, los object literal (que los arreglé en algún momento) y las clases, sumado a que los 3 tienen lógica de mixines. Si es un feature muy caliente por tener, ok, hacemos ese refactor, mal no le va a venir. Pero siento que con muy poco esfuerzo podemos dejarlo bastante bien.

No se qué opina el resto @asanzo @npasserini @PalumboN y siguen las firmas...

nscarcella commented 3 years ago

Y... Para mi el test no tiene mucho sentido si se hace en el initialize. Mi opinión es que poder hacerlo inline es más piola (especialmente porque si ese b es un const en lugar de un var es la única alternativa). Pero bueno, yo no estoy cuantificando la dificultad de refactorizar, qué se yo...

fdodino commented 3 years ago

Y... Para mi el test no tiene mucho sentido si se hace en el initialize

No entendí.

Y var o const b eso funca lo mismo (ya toqueteé el validador).

Solo espero entonces que disfruten mucho este feature... porque va a ser un reverendo pijazo

nscarcella commented 3 years ago

Si no entendí mal de lo que discutimos no íbamos a dejar que un const sea pisado en ningún mensaje y eso incluía el initialize.

On Mon, Nov 23, 2020, 10:13 PM Fernando Dodino notifications@github.com wrote:

Y... Para mi el test no tiene mucho sentido si se hace en el initialize

No entendí.

Y var o const b eso funca lo mismo (ya toqueteé el validador).

Solo espero entonces que disfruten mucho este feature... porque va a ser un reverendo pijazo

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/uqbar-project/wollok-language/issues/62#issuecomment-732518656, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAFPE256YJXMQW3GXGWZURDSRMCFJANCNFSM4PYESWNA .

fdodino commented 3 years ago

Bueno, encontré una especificación, que refleja lo que se habló más arriba:

class C inherits D {
  const a
  const b = 2
  const c
  const d

  method initialize() {
    super()
  // Analizar qué cosas se inicializan (y sus dependecias) puede ser complejo
    a = a + 1 // acá es necesario pasar un a
    // a = algo.cosaLoca(self) // Acá es más difícil
    d = 1
  }
}

new C(a=1, c=3) // OK

new C(c=3) // ERROR: Falta a (caso complicado)

new C() // ERROR: Falta a y c

new C(a=1, b=5, c=3) // -> b queda con 5

new C(d=0, c=3) // WARNING: valor d está siendo definido en el initialize()

no veo la inicialización inline que metiste en el comentario. Sería bueno que lo agregues para que quede reflejado exactamente lo que se va a implementar. Perdón que me ponga quisquilloso pero quiero estar seguro de tener lo que queremos bien escrito.

nscarcella commented 3 years ago

@fdodino esa especificación entiendo que quedó vieja. Me parece que era una propuesta sobre la que después se siguió elaborando. Yo entiendo que habíamos quedado en que el initialize es un método más, con lo cual no podía inicializar ni pisar consts.

Todo eso quedó registrado en un comentario de @npasserini en este mismo ticket. Hay un comentario justo abajo que se desprende de lo que dice ahí con un ejemplo de la lazyness.

Yo la verdad me quedaría con lo que discutimos acá en Github, porque los documentos que fuimos dando vuelta no son fáciles de trazear y vamos a terminar retrocediendo casilleros.

De última, si quedó algo poco claro podemos completarlo o discutirlo de nuevo, pero no estoy seguro qué es lo que está faltando.

fdodino commented 3 years ago

Veamos qué dice el resto, yo discrepo mucho con las decisiones que me obligan a tomar en base al comentario de NicoP (que fue lo primero que leí) y con tu comentario adicional. A mí me haría feliz tener la especificación "vieja" actualizada con las últimas decisiones, y mientras tanto voy a dejar el PR stand-by.

nscarcella commented 3 years ago

@fdodino ahí actualicé el documento. Creé una versión nombrada para no perder lo que había escrito por las dudas, pero la verdad había quedado super viejo (previo a toda la discusión que se tuvo) y hubo que escribirlo casi todo de nuevo. Lo completé en función de lo que quedó anotado en este ticket y lo que se charló en tu PR quitando los constructores, pero siéntanse libres de revisarlo y ver si el formato les convence. También le agregué una versión y una última fecha de revisión, como para tratar de no perder el hilo y darnos cuenta cuándo queda viejo. Si les gusta más tener un documento versionado en lugar de usar los issues, por mi perfecto. Lo único que les pido es que lo dejemos linkeado en los tickets relevantes y en las reuniones de discusión lo completemos entre todos, así estamos seguros de que todo el mundo tiene lo que van a necesitar al momento de implementar.

Feliz año!

fdodino commented 3 years ago

Bueno, lo acabo de leer en profundidad, excelente laburo!!! Me permitió encontrar 3 problemas de implementación y hay 2 temas para discutir a futuro. Voy a estar haciendo algunos toques respecto a lo que encontré en mis ratos libres (estoy de vacaciones).

asanzo commented 3 years ago

Sí, laburazo, Nico, increíblemente claro. Ahí metí comentarios para romper los huevos.

fdodino commented 3 years ago

@nscarcella @asanzo @PalumboN @npasserini @lspigariol @tesonep acabo de pasar por todo el documento e hice los cambios pertinentes, en particular hay una mejora bastante importante en el tratamiento de los atributos que dependen de otros, se arma un orden como para tratar de que queden correctamente instanciados.

Pueden ver https://github.com/uqbar-project/wollok-language/pull/83 donde hay toda una serie de archivos lazyInitialization*.wtest para maś info

PalumboN commented 3 years ago

@fdodino @nscarcella esto ya está implementado, no?

fdodino commented 3 years ago

Jajja, en wollok xtext sí, desde enero...

nscarcella commented 3 years ago

En TS también.