Haskell parte 2

Funciones


 

Declaración de funciones

nombreFuncion :: Entrada -> Resultado (declaración)
nombreFuncion varEntrada = expresión (implementación)

En la declaración se indica el nombre de la función, el tipo de dominio (el tipo de
la entrada), y el tipo de la imagen (resultado). La similaridad con una función
matemática esta implícita.

En la implementación se indica el nombre de la función de nuevo, el nombre de la
variable en la que se va a recibir el parámetro de entrada (varEntrada).

La “variable” que “recibirá” el valor de entrada de la función va seguida de un
símbolo de igual “=” y después de una expresión que determinará el resultado de la
función.

por ejemplo:
doble :: Int -> Int
doble x = x + x

Uso de una función:

f a b
a y b son parámetros, f es la función
por ejemplo:
doble 7

Imprimirá por pantalla 14

Las funciones son el operador con mayor prioridad, por tanto si hacemos

doble 7 + 3

se ejecutaría

doble(7) + 3 = 14 + 3 = 17


 

Notación currificada

En Haskell las funciones únicamente tienen un argumento. Por tanto cuando queremos
declarar una función que reciba dos parámetros debemos aplicar la notación currificada.

Es decir, haremos que “la función reciba una función”.

La notación es la siguiente:

nombreFuncion TipoArgumento1 -> TipoArgumento2 -> TipoResultado

Esto realmente se interpreta como:

nombreFuncion TipoArgumento1 -> (TipoArgumento2 -> TipoResultado)

Es decir, hemos agrupado el segundo parámetro y el resultado en una tupla.
Por tanto es como si la función nombreFuncion recibiera un parámetro y devolviera
como resultado a su vez una función.

Ejemplo:

resta :: Int -> Int -> Float
resta x y = x – y

Además otra propiedad de la notación currificada es la aplicación parcial.
La notación parcial consiste en aplicar internamente en una función otra
función que recogerá los parámetros que necesite.

Ejemplo:

cubo :: tipoargumento -> tiporesultado
cubo varArgumento = varArgumento*varArgumento*varArgumento

cubomasdos :: tipoargumento -> tiporesultado
cubomasdos = 2 + cubo

La función cubo esta recibiendo de forma automática el valor del argumento sin necesidad
de asignárselo explícitamente ni declarar la “variable” en que se volcaría el argumento.


 

Evaluación perezosa

Esta es una característica de algunos lenguajes que Haskell posee.
Básicamente se trata de que no se evaluará una expresión hasta que se vaya a usar.
Dicha expresión puede ser una función, una operación…

Por ejemplo supongamos que tenemos la función anteriormente empleada doble, pero
que en este caso recibe dos argumentos

dobleDos :: Int -> Int -> Int
dobleDos x y = 2*x

y realizamos la siguiente llamada

dobleDos 6 doble 7

por tanto tenemos

dobleDos(6, doble(7))

sin embargo como el valor del segundo parámetro (doble 7) nunca va a llegar a ser usado en la
función dobleDos, no llegará a resolverse ni evaluarse.


 

Composición de funciones

Es similar a la composición de funciones matemática.
Por si alguien no tiene estos conceptos la composición matemática se caracteriza
por que, si tenemos dos funciones f y g, y componemos fºg(x) sería igual a pasar
el resultado de g(x) a f como argumento.

En haskell la sintaxis para realizar la composición es con un punto:

funcion1 :: Int -> Int
funcion1 y = operacion sobre y

funcion2 :: Int -> Int -> Int
funcion2 x = (suma x).funcion1

Por tanto la funcion2 recibiria 2 parámetros y su resultado sería la suma del primer parámetro
mas el resultado de aplicar la funcion1 al segundo parámetro. Es decir, seria el equivalente de

funcion2(x, funcion1(y))


 

Patrones

Un patrón es una “especie” de condición que debe cumplirse. Un ejemplo de patrones en una función sería:

multiplicacionde3 :: (Int, Int, Int) -> Int
multiplicacionde3 (0, x, y) = 0
multiplicacionde3 (x, 0, y) = 0
multiplicacionde3 (x, y, 0) = 0
multiplicacionde3 (x, y, z) = x*y*z

Se podría comparar con una estructura condicional de las mencionadas previamente:

multiplicacionde3 :: (Int, Int, Int) -> Int
multiplicacionde3 (x, y, z) = if x == 0 then 0 else
if y == 0 then 0 else
if z == 0 then 0 else x*y*z

Ambas estructuras funcionarían de igual forma, pero realizarlo mediante patrones nos deja un código mucho mas limpio y elegante a la hora de evaluar expresiones.
Una característica de la evaluación de patrones es que se buscará el primer patrón que se ajuste a los datos y dejará de realizar comparaciones.
Por ejemplo si en nuestro caso anterior tanto x como y valen 0, se quedaría en la primera evaluación (1, x, y) y no pasaría a evaluar las siguientes opciones.
Si no se cumple ninguno de los patrones se producirá un error.

En el ejercicio anterior hemos aplicado patrones a tuplas, peo existen distintos tipos:

Constante:

Solo un dato, y concuerda un argumento, que coincide con la constante.

boolToBin :: Bool -> Int
boolToBin True = 1
boolToBin False = 0

Lista:

[] indica lista vacía
[x] es una lista de un único elemento, [x, y, z] indica lista de 3 elementos
(x:xs) indica lista de al menos 1 elemento, (x:y:zs) indica lista de al menos 2 elementos

longLista :: [Int] -> Int
longLista [] = 0
longLista [x] = 1
longLista [x:y:zs] = length [x:y:zs]

Tuplas:

multiplicacionTupla :: (Int, Int, Int) -> Int
multiplicacionTupla (x, y, z) = x*y*z

Además los patrones pueden anidarse:

multListaTuplas :: [(Int, Int)] -> Int
multListaTuplas ((x, y):xs) = x*y

Aritmético:

Es un patrón para descomponer, por ejemplo, un entero que cumpla una propiedad:

funcion :: Int -> Int
funcion 0 = 0
funcion (n+1) = n

Esta función descompone el argumento en n+1. Por tanto por ejemplo si se ejecutara:

funcion 4

devolveria:

3


 

Nombrados o seudónimos:

Se usa para nombrar un patrón que aparece en la “parte izquierda” de la función para utilizarlo en la derecha.
Se denota como “nombre@”:

longLista :: [Int] -> Int
longLista t@[x:y:zs] = length t

Subrayado:

El patron subrayado es el que indica que no hace falta conocer el argumento para calcular el resultado:

boolToBin :: Bool -> Int
boolToBin True = 1
boolToBin _ = 0

Aquí, como ya sabemos que si no concuerda con el primer patrón, es decir el argumento es “True”, el resultado será 0 sin necesidad de realizar mas comparación.


 

Definiciones locales

Supongamos que queremos utilizar una función dentro de otra. El paso más natural para esto es crear una función auxiliar que cumpla con nuestras necesidades.

f1 :: Int -> Int
f1 x = cuadrado (max x 4)

f :: Int -> Int
f x = if x <= 1 then 1 else f1 x

Pero, y si solo vamos a utilizar dicha función una vez?? es esto elegante?, y sobretodo, es esto eficiente??
La respuesta es no. Por ello se utilizan las definiciones locales, que consisten en definir una funcion que solo se va a usar una vez en el mismo sitio donde se usa. JavaScript implementa una funcionalidad similar (aunque claro, la sintaxis es completamente distinta).

Hay dos maneras de implementar definiciones locales:

– Let:

f :: Int -> Int
f x = let a = cuadrado (max x 4)
in (if x <= 1 then 1 else a)

– Where:

f :: Int -> Int
f x = a + (if x <= 1 then 1 else a)
where
a = cuadrado (max x 4)

Como se puede observar esta “buena práctica” aporta eficiencia (las expresiones se evaluarán una única vez y posteriormente se aplicarán) y brevedad y claridad al código.

 

Próximamente la parte 3.

Se han cerrado los comentarios