Recordemos que entre la Lógica y la Teoría de Tipos existe una fuerte conexión.

Los tipos son proposiciones y los programas son demostraciones.

En esta ocasión, usaremos esto para entender mejor las siguientes dos funciones.

ghci> :type ($)
($) :: (a -> b) -> a -> b

En lógica proposicional, esto no es más que la regla de inferencia del Modus Ponens (también llamada eliminación de la implicación)

Sabemos que esta regla es válida porque su fórmula que la respalda es una tautología, es decir, es verdadera sin importar los valores que tomen y , como podemos observar:

VVVVV
VFFFV
FVVFV
FFVFV

¡Pero espera! En Haskell tenemos (a -> b) -> a -> b mientras que aquí tenemos

A simple vista no parece lo mismo pero debemos recordar la siguiente equivalencia lógica.

La cual tenemos en Haskell bajo el siguiente nombre.

ghci> :type curry
curry :: ((a, b) -> c) -> a -> b -> c
ghci> :type uncurry
uncurry :: (a -> b -> c) -> ((a, b) -> c)

Ejemplo completo:

-- Esto equivale a: ((A ⇒ B) ∧ A) ⇒ B
modusPonens :: ((a -> b), a) -> b
modusPonens (f, x) = f x
 
pesito :: (a -> b) -> a -> b
pesito = curry modusPonen

En este segundo caso, vamos a ver que la siguiente función:

ghci> :type (.)
(.) :: (b -> c) -> (a -> b) -> a -> c

No es más que la regla de inferencia del silogismo hipotético (también llamada transitividad de la implicación)

De nuevo, esta regla es válida porque la respalda una tautología, lo cual podemos comprobar haciendo la tabla de verdad de , que por brevedad omitimos, ya que ésta tendría 8 filas. No olvides que en general tienen donde es el número de variables.

Ejemplo.

-- Representa la fórmula: (B ⇒ C) ∧ (A ⇒ B) ⇒ A ⇒ C
silogismoHip :: ((b -> c), (a -> b)) -> a -> c
silogismoHip (f, g) x = f (g x)
 
puntito :: (b -> c) -> (a -> b) -> a -> c
puntito = curry silogismoHip

Habiendo revisado la teoría, pasemos al lado práctico de todo esto.

En Matemáticas, si tienes una función y una función , la composición se denota con un círculo.

El operador . hace exactamente lo mismo.

(f . g)(x) = f (g (x))

Por otro lado, el operador $ representa la evaluación de funciones. En Matemáticas escribimos mientras que en Haskell: f $ x.

Ejemplo.

ghci> reverse ("Max")
"xaM"
ghci> reverse $ "Max"
"xaM"
 
ghci> reverse (reverse ("Max"))
"Max"
ghci> (reverse . reverse) ("Max")
"Max"

¿Para qué molestarnos en escribir $, cuando sencillamente podemos escribir reverse "Max"? Porque cuando aplicamos funciones utilizando espacios, se asocia a la izquierda, es decir, escribir f g x es lo mismo que escribir (f g) x, es por esto que hacer lo siguiente resulta en un error.

ghci> reverse reverse "Max"
-- ERROR

Y así, podemos ahorrarnos el escribir paréntesis ():

ghci> reverse $ reverse "Max"
"Max"

¿Pero entonces por qué lo siguiente resulta en un error?

gchi> reverse . reverse "Max"
-- ERROR

Porque además de asociar a la izquierda, la aplicación de funciones con espacios, tiene una mayor procedencia, es decir, es lo primero que vas a hacer en este caso. Así, lo anterior es igual a reverse . (reverse "Max"). Mientras que el operador $ tiene una menor procedencia: es de lo último que haces.

gchi> reverse . reverse $ "Max"
"Max"

Como resumen, tenemos la siguiente jerarquía de precedencia.

Orden de evaluación

  1. Paréntesis
  2. Espacios
  3. El operador .
  4. El operador $