This is an old revision of the document!


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 Lambdas con Objetos: El objetivo de esta sección es extender el lenguaje para soportar 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 (X pts)

Extensiones del AST y Parser

  • [x pts] Defina el tipo de datos Method para representar una definición de método en el AST.
  • [x 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.
  • [x 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"
  • [x 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

  • [x pts] Extienda el tipo Val con un constructor llamado classV que permita almacenar la información necesaria para representar a una clase como valor.
  • [x 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 (X pts)

Extensiones del AST y Parser

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

Extensiones de well-formed

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

Intérprete

  • [x 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.
  • [x pts] Defina la función invoke-method que permita buscar un método dentro de un objeto e invocarlo utilizando los argumentos entregados.
  • [x pts] Extienda el intérprete para permitir instanciar una clase utilizando la expresión new.

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

  • La creación de un objeto con un número inválido de argumentos debe lanzar el error “constructor not found”.

Además, para la creación de un objeto, los constructores son simplemente métodos llamados init. Es posible definir varios métodos init, sin embargo, definir varios constructores con la misma aridad es un error (en tiempo de creación de la clase). Cuando se ejecuta un new, se selecciona el constructor que corresponda al número de argumentos pasados (error si no hay ninguno que calza). Si no hay ningún constructor declarado, solo se puede usar {new c} sin argumentos (constructor por defecto).

A continuación se muestran algunos ejemplos de los errores que pueden lanzarse al instanciar un objeto:

;; 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"

Llamados a métodos (X pts)

Extensiones del AST y Parser

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

Extensiones de well-formed

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

Intérprete

  • [x pts] Extienda el intérprete para permitir invocar métodos de una clase.

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

  • La invocación de un método inexistente debe lanzar el error “method <id> not found”.

A continuación se muestran algunos ejemplos de los errores que pueden lanzarse al invocar un método:

;; Invocar un método no definido de una clase
> (run-val '{with {{A {class {}}}
                   {o {new A}}}
              {-> o m}})
"error: method m not found"

Acceso a campos (X pts)

Extensiones del AST y Parser

  • [x pts] Extienda el tipo de datos Expr y la función parse para soportar la expresión get del lenguaje.
  • [x 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"
  • [x pts] Extienda well-formed para verificar el nodo get.
  • [x pts] Extienda well-formed para verificar el nodo set.

Intérprete

  • [x pts] Extienda el intérprete para permitir obtener el valor de un campo en una clase.
  • [x 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"

Extienda el lenguaje para soportar estas expresiones, donde tanto clases y objetos son valores. Usted debe decidir los atributos que tendrán sus respectivos nodos en el AST. Si se encuentra con casos no especificados anteriormente debe tomar supuestos e indicarlo en su entrega. Modifique el parser y el intérprete para soportar el lenguaje extendido.

Codificando Lambdas (1 pto)

Ahora incorporaremosmos lambdas (funciones anónimas de primera clase) 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.

<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