Tarea 3 (Entrega: X de julio de 2024)

Esta tarea se distribuye con un archivo zip ( base) que contiene 3 archivos: main.rkt, tests.rkt y env.rkt. Los archivos están incompletos, y en ellos tiene que implementar lo que se solicita en las preguntas siguientes.

Debe entregar via U-cursos un archivo .zip que contenga los archivos main.rkt y tests.rkt.

Consulte las normas de entrega de tareas en http://pleiad.cl/teaching/cc4101

Recuerde que el testing y calidad de código se evalúan de acuerdo a la rúbrica.

Resumen

El objetivo de la tarea es extender un lenguaje base para soportar clases y objetos. El lenguaje base tiene números, booleanos y operaciones sobre ellos. Además contiene expresiones begin y with para secuenciar expresiones y definir múltiples identificadores respectivamente; pero no tiene soporte para funciones. Tome un tiempo de experimentar con el lenguaje entregado antes de comenzar a implementar las extensiones requeridas.

La tarea está dividida en 2 secciones, las cuales se describen a continuación:

- Clases y objetos: En esta sección se pide extender el lenguaje base con clases y objetos. En particular las clases deben ser entidades de primera clase, es decir, son valores del lenguaje.

- Codificando funciones anónimas de primera clase con Objetos: El objetivo de esta sección es extender el lenguaje para soportar funciones anónimas de primera clase (típicamente conocidas como “lambdas”) y aplicaciones como azúcar sintáctica, usando objetos.

  • La tarea se debe realizar SIN usar macros.
  • El objetivo de la tarea es que implemente un intérprete sintáctico para el lenguaje que se presenta. Esto quiere decir en particular que las clases y objetos no pueden ser codificados con lambdas de Racket (*)

En resumen, deben usar los conceptos de OOP vistos en clase y que también se presentan en el apunte OOPLAI.

(*) nada de (lambda (msg . args) …) en su código!

Clases y objetos (5 ptos.)

A continuación se presenta la sintaxis concreta del lenguaje extendido (se omiten las expresiones del lenguaje base):

<expr> ::= ... (expresiones del lenguaje base) ...
        | {class {<sym>*} <method>*}
        | {new <expr> <expr>*}
        | {get <expr> <sym>}
        | {set <sym> <expr>}
        | {-> <expr> <sym> <expr>*}
        | self
 
<method> ::= {def <sym> {<sym>*} <expr>}

Donde:

  • class permite crear una nueva clase anónima, con una lista de 0 o más identificadores para los campos, seguida de 0 o más métodos.
  • new permite crear una instancia de una clase dada (primera expresión), entregando 0 o más argumentos al constructor.
  • get permite acceder al campo de un objeto dado.
  • set permite modificar el campo de un objeto. Solamente es válido usar set dentro del cuerpo de un método y para modificar campos propios del objeto desde donde se llama. No es posible modificar campos de un objeto externo.
  • permite invocar un método de un objeto dado, con 0 o más argumentos.
  • self permite acceder al objeto actual. Solamente es válido usar self dentro del cuerpo de un método.
  • def permite definir un método, donde se especifica el nombre del método, 0 o más parámetros, y el cuerpo del método.

A lo largo de la implementación, verificaremos que los programas del lenguaje cumplan múltiples requisitos. Por ejemplo, revisaremos que no hayan campos duplicados dentro de una clase, o que los usos de self estén sólo dentro de un método. Todas las verificaciones que puedan hacerse de manera estática estarán centralizadas en una función llamada well-formed. En el archivo base main.rkt se entrega una implementación inicial que deberá extender siguiendo las indicaciones del enunciado.

Veamos algunos programas de ejemplo para ilustrar de manera general las características esperadas del lenguaje.

;; comportamiento esperado
> (run-val '{with {{c {class {x y}
                        {def init {}
                          {begin {set x 1} {set y 2}}}
                        {def init {init-x init-y}
                          {begin {set x init-x} {set y init-y}}}  
                        {def sum {z} {+ {get self x} {+ {get self y} z}}}
                        {def set-x {val} {set x val}}}}
                   {o {new c 3 4}}}
              {begin
                {-> o set-x {+ 1 3}}
                {+ {-> o sum 3} {get o y}}}})
15
;; las clases son valores
> (run-val '{with {{A {class {}
                        {def apply {c} {-> {new c} m}}}}
                   {o {new A}}}
              {-> o apply {class {x}
                            {def init {} {set x 2}}
                            {def m {} {get self x}}}}})
2
;;la definición de la clase tiene scope léxico
> (run-val '{begin {with {{A {class {x}
                               {def init {} {set x 2}}
                               {def init {init-x} {set x init-x}}
                               {def m {} {get self x}}}}}
                     10}
                   {new A}})
"free identifier: A"
;; los identificadores dentro de una clase tienen scope léxico 
;; (note el uso de la “x” en la definición del método “m”
> (run-val '{with {{x 10}
                   {A {class {}
                        {def m {y} {+ x y}}}}
                   {o {new A}}}
              {-> o m 1}})
11            

Manos a la Obra!

Clases (1.5 pts)

Extensiones del AST y Parser

  • [0.1 pts] Defina el tipo de datos Method para representar una definición de método en el AST.
  • [0.1 pts] Implemente la función parse-method que recibe una definición de método en sintaxis concreta y retorna el nodo de AST correspondiente.
  • [0.2 pts] Extienda el tipo de datos Expr y la función parse para soportar la expresión class del lenguaje.

Extensiones de well-formed

Durante la creación de clases, deben cumplirse los siguientes requisitos:

  • Una clase no puede tener campos repetidos. En caso de que esto no se cumpla, debe lanzar el error “duplicate fields”.
  • Una clase no puede tener 2 o más constructores de igual aridad. Si esto no se cumple, debe lanzar el error “same arity constructor”. Los constructores son simplemente métodos llamados 'init'.
  • Al sobrecargar un método con la misma aridad, debe lanzar el error “overloading method <id> with the same arity”
  • self sólo puede ser usado dentro del cuerpo de un método. Si esto no se cumple, debe lanzar el error “self outside of method”

A continuación se muestran algunos ejemplos de los errores que pueden lanzarse al hacer las verificaciones:

;; Crear una clase con campos duplicados es un error en tiempo de creación de la clase
> (run-val '{begin {with {{A {class {x y x}}}}
                     10}
                   {new A}})
"error: duplicate fields"
;; Tener 2 init con la misma aridad es un error en tiempo de creación de la clase
> (run-val '{begin {with {{A {class {x}
                               {def init {init-x} {set x init-x}}
                               {def init {init-x} {set x 12}}}}}
                     10}
                   {new A}})
"error: same arity constructor"
;; Tener 2 métodos con la misma aridad es un error en tiempo de creación de la clase
> (run-val '{begin {with {{A {class {x}
                               {def foo {a} {set x a}}
                               {def foo {b} {set x {+ b 1}}}}}}
                     10}
                   {new A}})
"error: overloading method foo with the same arity"
;; No se puede usar self fuera de un método
> (run-val 'self)
"error: self outside of method"
  • [0.7 pts] Extienda well-formed para realizar las verificaciones mencionadas.

Observación: Puede añadir un parámetro extra a well-formed para indicar si esta dentro de una clase

Intérprete

  • [0.3 pts] Extienda el tipo Val con un constructor llamado classV que permita almacenar la información necesaria para representar a una clase como valor.
  • [0.1 pts] Extienda el intérprete para soportar la creación de clases.

Observación: Recuerde que los métodos definidos en una clase deben utilizar el ambiente al momento de la creación de la clase y no el ambiente de cuando son invocados.

Objetos (1.5 pts)

Extensiones del AST y Parser

  • [0.2 pts] Extienda el tipo de datos Expr y la función parse para soportar la expresión new del lenguaje.

Extensiones de well-formed

  • [0.2 pts] Extienda well-formed para verificar el nodo new. En este caso, sólo es necesario llamar well-formed para cada subnodo del nodo new.

Intérprete

  • [0.3 pts] Extienda el tipo Val con un constructor llamado objV que permita almacenar la información necesaria para representar a un objeto como valor. Hint: Recuerde que uno de los objetivos de tener clases, es permitir que sus intancias puedan compartir métodos.
  • [0.4 pts] Defina la función invoke-method que permita buscar un método dentro de la clase un objeto e invocarlo utilizando los argumentos entregados.
  • [0.4 pts] Extienda el intérprete para permitir instanciar una clase utilizando la expresión new.

Observación: Cuando se evalúa una expresión new, se debe buscar en la clase un constructor que corresponda al número de argumentos entregados. Si no hay ninguno que tenga la aridad requerida, se debe lanzar el error “constructor not found”. Si se está instanciando una clase que no declara ningún constructor, solo se puede usar el constructor por defecto que no recibe argumentos, es decir, {new c}.

A continuación se muestran algunos ejemplos de instanciación de clases:

;; Una clase sin constructores puede ser creado solo con {new class}, sin argumentos
> (run-val '{with {{x 10}
                   {A {class {x}}}
                   {o {new A x}}}
              1})
"error: constructor not found"
;; Esta clase no tienen ningún constructor con aridad 2
> (run-val '{begin {with {{C {class {x}
                               {def init {init-x} {set x init-x}}}}}
                     10}
                   {new C 1 2}})
"error: constructor not found"

Llamados a Métodos (0.8 pto)

Extensiones del AST y Parser

  • [0.2 pts] Extienda el tipo de datos Expr y la función parse para soportar la expresión del lenguaje.

Extensiones de well-formed

  • [0.2 pts] Extienda well-formed para verificar el nodo . En este caso, solo es necesario llamar well-formed para cada subnodo del nodo new.

Intérprete

  • [0.4 pts] Extienda el intérprete para permitir evaluar la invocación de métodos de un objeto.

Observaciones:

  • Recuerde en en la sección anterior (Objetos) implementó la función invoke-method.
  • La invocación de un método inexistente o cuya aridad no coincide con el número de argumentos entregados debe lanzar el error “method <id> not found”.

A continuación se muestran programas en los que se intenta invocar un método inexistente o donde no se encuentra una sobrecarga con la aridad correcta:

;; Invocar un método no definido de una clase
> (run-val '{with {{A {class {}}}
                   {o {new A}}}
              {-> o m}})
"error: method m not found"
;; Esta clase no tiene el método set-x definido para la aridad 2
> (run-val '{with {{A {class {x}
                        {def set-x {val-x} {set x val-x}}}}
                   {o {new A}}}
              {-> o set-x 10 20}})
"error: method set-x not found"

Acceso y Modificación a Campos (1.2 pts)

Extensiones del AST y Parser

  • [0.2 pts] Extienda el tipo de datos Expr y la función parse para soportar la expresión get del lenguaje.
  • [0.2 pts] Extienda el tipo de datos Expr y la función parse para soportar la expresión set del lenguaje.

Extensiones de well-formed

Durante el instanciado de clases, deben cumplirse los siguientes requisitos:

  • La invocación de set fuera de un método debe lanzar el error “set outside of method”.
;; No se puede usar set fuera de un método
> (run-val '{set x 1})
"error: set outside of method"
  • [0.1 pts] Extienda well-formed para verificar el nodo get.
  • [0.2 pts] Extienda well-formed para verificar el nodo set.

Intérprete

  • [0.3 pts] Extienda el intérprete para permitir obtener el valor de un campo en una clase.
  • [0.2 pts] Extienda el intérprete para permitir modificar el valor de un campo en una clase.

Los errores dentro de interp para objetos deben manejarse de la siguiente forma:

  • El acceso a un campo inexistente de un objeto debe arrojar el error “field <id> not found”.
  • El acceso a un campo no inicializado debe arrojar el error “field <id> not initialized”.
;; Acceder a un campo no definido de una clase
> (run-val '{with {{A {class {}}}
                   {o {new A}}}
              {get o z}})
"error: field z not found"
;; Acceder a un campo no inicializado de una clase
> (run-val '{with {{A {class {x y} {def init {init-x init-y} {set x init-x}}}}
                   {o {new A 1 2}}}
              {get o y}})
"error: field y not initialized"

Codificando funciones anónimas de primera clase con Objetos (1 pto)

Ahora incorporaremos funciones anónimas de primera clase (típicamente conocidas como “lambdas”) a nuestro lenguaje. A diferencia de lo visto durante el curso, en esta ocasión no daremos una interpretación directa de las funciones. Usted debe idear una manera de usar la implementación de clases y objetos hecha en la parte anterior para codificar las lambdas. Esto significa que no puede modificar el AST y el intérprete para soportar funciones y aplicaciones de funciones. Las modificaciones que debe hacer son en el parser. En otras palabras, las funciones y aplicaciones serán sólo azúcar sintáctica.

Hint: Piense en lo que comúnmente hemos visto como sintaxis de “aplicación de función” como azúcar sintáctico para la invocación de un método en un objeto, este método puede contener el código de la “función” definida. ¿De qué clase sería ese objeto? ¿Cómo se podría llamar ese método?

<expr> ::= ...
         | (fun (<id>*) <expr>)
         | (<expr> <expr>*)

Ejemplos de uso:

> (run-val '{{fun {x} {+ x 1}} 2})
3
> (run-val '{with {{add1 {fun {x} {+ x 1}}}}
                  {+ {add1 2} {add1 4}}})
8