Bitácora

Segunda clase

03 de Abril, 2023

Tarea para la clase que viene:

  • Armar grupo si todavía no lo hiciste. Podés buscar integrantes en el canal #buscando-grupo de Discord. Una vez que ya lo tengas armado, anunciarlo en el canal #grupos indicando quiénes lo integran.
  • Continuar resolviendo el ejercicio PdeP - commerce.
  • Realizar (si todavía no los hiciste) los ejercicios de la primera guía.

¿Qué vimos hoy?

Estos son los ejemplos de funciones y valores que vimos en clase:

esMayorDeEdad unaEdad = unaEdad >= 18
​
frecuenciaCardiacaPromedio = 80
​
hacerActividadFisica unaFrecuencia = unaFrecuencia + 50
​
tieneTaquicardia unaFrecuencia = unaFrecuencia >= 180

De esa forma le pusimos un alias o etiqueta al valor 80 con frecuenciaCardiacaPromedio y creamos funciones como esMayorDeEdad, hacerActividadFisica y tieneTaquicardia. Las funciones van a ser nuestras herramientas para poder operar a los valores.

Algo muy importante es que en Haskell no hay efecto. Esto quiere decir que los valores igualados no van a mutar luego de ser operados por las funciones. Este concepto se llama inmutabilidad.

Por ejemplo, si aplicamos hacerActividadFisica a la frecuenciaCardiacaPromedio, podemos ver que frecuenciaCardiacaPromedio no cambia su valor:

> frecuenciaCardiacaPromedio
=> 80
> hacerActividadFisica frecuenciaCardiacaPromedio
=> 130
> frecuenciaCardiacaPromedio
=> 80

Por esto, es que en Haskell logramos tener lo que se llama transparencia referencial. 🤯 Es importante recordar que frecuenciaCardiacaPromedio no es una variable, sino que es simplemente un alias, es decir, otra manera de decirle al valor 80.

Ahora supongamos que queremos ver si tenemos taquicardia después de hacer actividad física. ¿Cómo lo resolvemos? 😱 Usando composición:

> (tieneTaquicardia.hacerActividadFisica) 70
=> True

Como ya mencionamos anteriormente, el valor que retorne la función de la derecha, tiene que ser un valor que la función de la izquierda pueda operar. Si quisiéramos componerlo al revés:

> (hacerActividadFisica.tieneTaquicardia) 70

Va a romper 💥 ya que hacerActividadFisica tiene que recibir un número, y está recibiendo un booleano.

Por último, vamos a darle un nombre a la acción de preguntar si se tiene taquicardia luego de hacer una actividad física creando la funcióntieneTaquicardiaDespuesDeEntrenar:

tieneTaquicardiaDespuesDeEntrenar unaFrecuencia = (tieneTaquicardia.hacerActividadFisica) unaFrecuencia

Que no exista el estado en Haskell, hace que la composición tenga más relevancia. Ya que como no podemos pisar valores con variables, la composición nos permite encadenar las funciones para trabajar con diferentes valores y así poder crear soluciones más complejas. ✨

Precedencia de operadores

En matemática, cuando tenemos una expresión como 2 * 3 + 4, solemos operarla dependiendo de la precedencia de cada operador. Como el * es de mayor precedencia que el +, operamos primero 2 * 3 y luego le sumamos 4.

En Haskell también se respeta esto. Les dejamos una tabla para que puedan ver la precedencia que utiliza Haskell:

Precedencia (Mayor número, mayor precedencia) “Operador”
11 ( )
10 Aplicacion prefija
9 .
8 ^
7 *,/
6 +,-
5 :
4 ==, /=, <, <=, >, >=
3 &&
2 ||
1 $

Type classes

Pensemos en la función suma:

suma unNumero otroNumero = unNumero + otroNumero

¿Qué tipo debería tener?

¿Enteros?

¿Que tal suma :: Int -> Int -> Int?

Dados estos números:

unEntero :: Int
unEntero = 2

otroEntero :: Int
otroEntero = 3

unFlotante :: Float
unFlotante = 2

otroFlotante :: Float
otroFlotante = 3

¿Qué creen que pasaría si queremos evaluar la siguiente expresión: suma unEntero otroEntero?

> suma unEntero otroEntero
5

¿y suma unFlotante otroFlotante?

> suma unFlotante otroFlotante
-- * estalla *

¡Tiene sentido! le dijimos a nuestra función suma que su dominio son los enteros, entonces cuando le damos un flotante, nos dice “eh, no; yo trabajo sólo con enteros”

¿Flotantes?

¿Y qué tal si la hubiésemos definido como suma :: Float -> Float -> Float? Después de todo, los enteros son un subconjunto de los reales, ¿no?

> suma unFlotante otroFlotante
5
> suma unEntero otroEntero
-- * estalla *

Bueno, no; si bien en la matemática es cierto que los enteros son reales, en definitiva para haskell Float e Int son tipos de datos distintos.

¿a?

¿Y si hago suma :: a -> a -> a?

*Falla al cargar el archivo*

Parece que haskell no nos permite sumar cualquier cosa tampoco, lo cual es de esperarse; ¿tendría sentido que nos deje sumar dos funciones? ¡no!

¿Pero qué onda? si yo en la consola hago

> unFlotante + otroFlotante
5

ó

> unEntero + otroEntero
5

¡Me andan las dos!

¡Números!

Para expresar el tipo de suma, en realidad nos está faltando una herramienta, a la cual llamamos Familia de Tipos, o Type Class.

Mientras que a un tipo lo podríamos describir como un conjunto de valores asociado a un conjunto de operaciones con las que los podemos trabajar, una familia de tipos es más bien un contrato que te dice qué operaciones tiene que entender un tipo para pertenecer a esa familia.

Hasta acá suena todo muy abstracto, así que bajémoslo a un ejemplo concreto: ¡los números!

Esa cosa en común que tienen Int y Float que nos permite sumarlos es la familia de tipos de los números Num; y su contrato nos dice que cualquier tipo que pertenezca a su familia, se puede sumar (+), restar (-), y multiplicar (*)!

¿Y esto cómo lo escribimos en el código?

suma :: Num a => a -> a -> a
suma unNumero otroNumero = unNumero + otroNumero

Cuando escribimos esto, estamos restringiendo a que el tipo a debe pertenecer a la familia de tipos Num.

Es importante recalcar que en toda la firma de suma, a representa a un mismo tipo. Si bien ahora podemos sumar enteros con enteros, y flotantes con flotantes, esto no nos permite sumar enteros con flotantes.

Y así como tenemos una familia de tipos para los números, tenemos otro montón con distintos propósitos, como:

  • Show: Las cosas que se saben mostrar por pantalla (en la consola).
  • Eq: Las cosas que se saben comparar por igualdad (==).
  • Ord: Las cosas que se saben comparar por orden (>), (<), etc.
  • Num: ¡Los números! se saben sumar, restar y multiplicar.
  • Integral: Para números enteros; entienden la división entera (div), el resto de la división rem, se les puede preguntar si son pares (even) o impares (odd).
  • Fractional: Para números reales, que se pueden dividir con la división flotante (/)

Podemos los tipos que pertenecen a cada familia en el siguiente diagrama:

diagrama_tipo_clases

Es posible que haya alguna mentira blanca en este diagrama


Podes ver nuestro github acá.

Y saber mas sobre nosotros acá