Cuando pensamos en abstracción y polimorfismo, solemos imaginar clases, interfaces y herencia. Pero ¿sabías que la programación funcional también tiene sus propios superpoderes para modelar el comportamiento genérico y abstraer detalles?
En POO, usamos clases abstractas o interfaces para definir estructuras que deben ser implementadas. En programación funcional, el enfoque es distinto, pero el objetivo es similar: ocultar detalles de implementación y exponer un comportamiento general.
Un ADT define un tipo por sus operaciones, no por cómo están implementadas. Por ejemplo:
data Pila a = Vacía | Empujar a (Pila a)
Este tipo Pila podría representar una pila genérica, y podríamos tener funciones que operen sobre ella sin importar cómo esté construida internamente.
En la programación funcional se identifican varios tipos de polimorfismo:
Polimorfismo Paramétrico: Permite escribir funciones genéricas sobre cualquier tipo. Es como los genéricos de Java, pero más poderoso:
identidad :: a -> a
identidad x = x
La función identidad funciona para cualquier tipo a.
Polimorfismo ad-hoc (Typeclasses / Traits / Protocolos) : En Haskell, Rust, Scala o Elixir podemos definir interfaces de comportamiento según el tipo. Esto recuerda al "método virtual" de POO.
class Metrico a where
distancia :: a -> a -> Double
instance Metrico (Double, Double) where
distancia (x1, y1) (x2, y2) =
sqrt ((x2 - x1)^2 + (y2 - y1)^2)
Veamos un ejemplo en Scala:
trait Metrico[T] {
def distancia(a: T, b: T): Double
}
implicit object Punto2D extends Metrico[(Double, Double)] {
def distancia(a: (Double, Double), b: (Double, Double)) =
math.sqrt(math.pow(a._1 - b._1, 2) + math.pow(a._2 - b._2, 2))
}
def calcularDistancia[T](a: T, b: T)(implicit m: Metrico[T]) =
m.distancia(a, b)
Pattern Matching como Polimorfismo Estructural: Otro recurso poderoso es el pattern matching, que permite seleccionar comportamiento según la "forma" del dato.
sealed trait Forma
case class Circulo(r: Double) extends Forma
case class Rectangulo(ancho: Double, alto: Double) extends Forma
def area(f: Forma): Double = f match {
case Circulo(r) => math.Pi * r * r
case Rectangulo(a, h) => a * h
}
¿Y qué ganamos con esto?
- Abstracción sin herencia: no hay jerarquías rígidas.
- Mayor seguridad de tipos: muchos errores se detectan en tiempo de compilación.
- Separación de datos y comportamiento: las funciones no "viven" dentro de las estructuras de datos, lo cual facilita la composición y el testing.
La programación funcional ofrece mecanismos muy sólidos y expresivos para manejar abstracción y polimorfismo. Aunque no se usa herencia, se logra el mismo efecto (o incluso uno más flexible) usando funciones genéricas, pattern matching y typeclasses.