====== Tarea 3====== Esta tarea se distribuye con dos ficheros start.rkt y tests.rkt {{ :teaching:cc4101:resources:tareas:2017-1:basetarea3-2017.zip | base}}. Considere las definiciones del archivo start.rkt y escriba sus funciones en él. Escriba sus tests en el archivo tests.rkt adjunto. Ambos ficheros deben ser entregados vía U-Cursos. Los tests forman parte de su evaluación! Consulte las normas de entrega de tareas en http://pleiad.cl/teaching/cc4101. ====== Polimorfismo Ad-Hoc ====== El polimorfismo paramétrico permite definir funciones que actúan igual sobre datos de distintos tipos. Por ejemplo, la función ''%%length%%'' hace lo mismo sea cual sea el tipo de los elementos de la lista. Una forma complementaria de polimorfismo es el polimorfismo llamado "ad-hoc". Este polimorfismo permite definir distintas versiones de una función (llamada método), donde el tipo de los argumentos influye sobre qué versión se ejecuta realmente. Por ejemplo, en Haskell, uno define una clase de tipo (type class), como ''%%Show%%'': class Show a where show :: a -> String La definición de la clase especifica que el método show, dado un elemento de tipo a, retorna una String. Una definición de clase puede incluir varios métodos. Luego, uno específica la implementación de una clase para ciertos tipos de datos a través de instancias, por ejemplo: instance Show Expr where show (Id x) = "id(" ++ x ++ ")" show (Add e1 e2) = "add(" ++ show e1 ++ ", " ++ show e2 ++ ")" instance Show (a -> b) where show x = "" ====== Clases de predicados ====== En esta tarea, vamos a explorar una alternativa a las clases de tipos de Haskell, para un lenguaje dinámico. La idea sigue siendo la misma: poder tener distintas implementaciones de una misma función (método). Pero en vez de elegir estáticamente en base al tipo estático de un argumento, vamos a basarnos en predicados evaluados dinámicamente. Vamos a tomar como base el lenguaje MiniScheme, sin tipos estáticos. El objetivo de la tarea es extender el lenguaje con la definición de clases de predicados, y sus instancias respectivas. Esto significa que extendemos la categoría de definiciones de MiniScheme: ‹def› ::= ‹id-def› | ‹class-def› | ‹instance-def› La sintáxis para definir clases es la siguiente: ‹class-def› ::= (define-class ‹id› ‹id›+) Por ejemplo, uno puede definir la clase ''Show'', con su único método ''show'': (define-class Show show) La sintáxis para definir instancias de clases es la siguiente: ‹instance-def› ::= (define-instance ‹id› ‹expr› [‹id›‹expr›]+) Después del nombre de la clase a extender, viene una expresión cuyo valor debe ser un predicado (ie. una función de un argumento que retorna un booleano). Este predicado determina cuando la instancia es activa. Recibe como argumento, el primer parametro del metodo. Esto significa que si usamos predicados de tipo como ''number?'', ''bool?'' y ''string?'', obtenemos algo muy similar a las clases de tipos de Haskell (salvo que la resolución de metodos se hace dinámicamente): >(local ((define-class Show show) (define-instance Show number? [show number->string]) (define-instance Show string? [show (fun (x) x)]) (define-instance Show bool? [show (fun (x) (if x "true" "false"))])) (string-append (show #t) " " (show 1) " " (show "hola"))) "true 1 hola" Otro ejemplo es definir una clase ''Size'' con el método ''size'', y decidir que por ejemplo el tamaño de un número es el número mismo, y el tamaño de una string es su largo: >(local ((define-class Size size) (define-instance Size number? [size (fun (x) x)]) (define-instance Size string? [size string-length])) (+ (size 10) (size "hola"))) 14 La ventaja de hacer esto dinámicamente, es que podemos usar cualquier predicado para decidir qué instancia es activa. Por ejemplo, podemos modificar la forma con la cual se muestran datos cuyo tamaño excede 100: >(local ((define-instance Show (fun (v) (> (size v) 100))) [show (fun (x) "big data!!")]) (string-append (show 200) (show "this is a very long string that should be longer than a hundred characters, so it should be a bit longer still"))) "big data!!big data!!" ====== (4.0) MiniScheme con clases de predicados ====== (0.5) Extienda la sintáxis de MiniScheme para definiciones de clases e instancias. (1.5) Defina tipos de datos para clases e instancias, con las siguientes funciones: * ''(append-instance cls inst)'' que retorna una nueva clase cuya lista de instancias incluye ''inst''. * ''(get-instance cname val env)'' para recuperar la instancia áctiva de la clase para el valor ''val'' (''cname'' es el nombre de la clase, ''env'' es el ambiente en el cual ''cname'' está asociado a una clase). * ''(get-method inst name)'' que retorna la implementación del método name en la instancia ''inst''. (1.0) Extienda ''interp-def'' para procesar las definiciones de clases e instancias. La instancia de una clase debe implementar todos los métodos de la clase que esta instanciando. (1.0) Extienda ''interp'' para procesar los métodos. **Indicaciones** Los métodos son entidades de primera clase. Por ejemplo: (run '(local ((define-class Show show) (define-instance Show number? [show number->string])) ((fun (x) (x 4)) show))) "4" Para entender el alcance de las clases y sus métodos, considere el siguiente ejemplo: > (run '(local ((define-class Size size) (define-instance Size number? [size (fun (x) x)])) (+ (size 5) (local ((define size (fun (x) 1))) (size 5))))) 6 Esto sugiere que los métodos viven en el ambiente. Sin embargo, los métodos no están directamente en el ambiente. Lo que está directamente en el ambiente, son las clases. Una clase tiene asociada sus métodos, y la posición en el ambiente determina la visibilidad de sus métodos. En el caso anterior, para la primera llamada a ''(size 5)'', en el ambiente se encuentra que hay una clase, ''Size'', que tiene un método con el nombre buscado. Se aplica entonces este método (buscándolo de la primera instancia activa). En la segunda llamada a ''(size 5)'', la redefinición de ''size'' en el ambiente tiene precedencia sobre la clase ''Size'', y por ende se usa dicha definición. En conclusión, hay un único ambiente, en el cual conviven valores normales (funciones, números, strings, etc.) y clases, que sirven de representantes de sus métodos. Noten que en este lenguaje las clases no son de primera clase. Por ejemplo, no se pueden pasar clases como argumento de una función: (run '(local ((define-class Show show) (define-instance Show number? [show number->string])) ((fun (x) x) Show))) "error: free identifier Show" Es decir, el nombre de la clase no es relevante al momento de buscar algo en el ambiente. Las instancias viven exclusivamente dentro de las clases. Aplicar un método implica buscar la primera instancia de la clase que es activa (considerando el primer argumento de la aplicación). Note que por la forma en que se busca la implementación de un método, si una instancia implementa un método que no esta en la clase, entonces ese método no será accesible. Asegúrese que los distintos ejemplos del enunciado funcionen como esperado, y entregue una batería de tests completa. En particular, asegúrese que el scope de las instancias es local a un bloque ''local'', lo que permite sobre-escribir una instancia localmente: >(local ((define-class C foo bar) (define-instance C number? [foo (fun (x) (+ x 1))] [bar (fun (x) (- x 1))])) (+ (foo 3) (bar 4) (local ((define-instance C number? [foo (fun (x) (* x 2))] [bar (fun (x) (- x 2))])) (+ (foo 3) (bar 4))) (foo 3))) 19 ======(2.0pt) Implementaciones de métodos por defecto ====== Recuerden que una clase puede declarar varios métodos. En algunos casos, ciertos métodos pueden ser definidos en función de los demás métodos de la misma clase. Por ejemplo, en la clase ''Comp'', el método ''greater?'' se puede definir directamente en función de ''smaller?'' y ''same?'': (local ((define-class Comp same? smaller? [greater? (fun (a b) (and (not (smaller? a b)) (not (same? a b))))]) ... Así, las instancias de ''Comp'' no necesitan volver a implementar ''greater?'': ... (define-instance Comp number? [same? equal?] [smaller? <]) (define-instance Comp string? [same? equal?] [smaller? string Obviamente, si una instancia redefine ''greater?'', dicha re-definición tiene precedencia sobre la implementación por defecto. Extienda su lenguaje para soportar la definición de implementación por defecto de los métodos. ======Bonus[+1pt]: Software adaptable al contexto ====== Las clases de predicados pueden ser usadas para hacer software adaptable a su contexto de ejecución. Por ejemplo, considere una clase ''Server'' con métodos ''service1'' y ''service2'': (define-class Server service1 service2) (define-instance Server (fun (x) #t) [service1 (fun (x) "serv1: full quality")] [service2 (fun (x) "serv2: ok")]) ... Cuando el servidor ejecuta en un contexto inseguro, la calidad del ''service1'' se degrada, y se prohibe ''service2'': (define-instance Server (fun (x) (untrusted-ctx?)) [service1 (fun (x) "serv1: low quality")] [service2 (fun (x) (error "serv2: denied"))]) Para esto, necesitamos extender el lenguaje con una forma de identificar computación insegura. Por ejemplo: (define g (fun (x) (untrusted (f1 x)))) cuando aplicamos ''g'', se activa el modo inseguro para la extensión dinámica de la evaluación de ''(f1 x)'' (es decir, hasta que ''(f1 x)'' retorne, se considera que estamos en un contexto inseguro). La forma más conveniente de implementar esto es usando una variable dinámica, es decir, con alcance dinámico. Una variable dinámica en Racket se llama un ''parameter''. Usando un parameter, extienda el lenguaje con ''untrusted'' y ''untrusted-ctx?''. Notarán que no se requiere modificar mucho el interprete. Para aprender a usar variables dinámicas en Racket, refierase a la [[https://docs.racket-lang.org/guide/parameterize.html|documentación]].