Inicio / Ruby / Ruby: Lenguaje Elegante y Expresivo / Procs, Lambdas y Programación Funcional

Procs, Lambdas y Programación Funcional

Higher-order functions, composición, curry, memoization, pipelines con then/tap.

Intermedio Funciones
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

Procs, Lambdas y Programación Funcional

Ruby soporta programación funcional con first-class functions a través de Procs y Lambdas.


Funciones de orden superior

# Pasar funciones como argumentos
def aplicar(array, funcion)
  array.map { |item| funcion.call(item) }
end

doble = ->(n) { n * 2 }
cuadrado = ->(n) { n ** 2 }

puts aplicar([1, 2, 3], doble).inspect      # [2, 4, 6]
puts aplicar([1, 2, 3], cuadrado).inspect    # [1, 4, 9]

# Retornar funciones
def multiplicador(factor)
  ->(n) { n * factor }
end

triple = multiplicador(3)
puts triple.call(5)    # 15
puts triple.(10)       # 30

# Method objects
def gritar(texto)
  texto.upcase + "!"
end

m = method(:gritar)
puts m.call("hola")                          # "HOLA!"
puts ["hola", "mundo"].map(&m).inspect       # ["HOLA!", "MUNDO!"]

Composición de funciones

# Operador >> (Ruby 2.6+)
doble = ->(n) { n * 2 }
incrementar = ->(n) { n + 1 }
cuadrado = ->(n) { n ** 2 }

# Componer: primero doble, luego incrementar
transformar = doble >> incrementar
puts transformar.call(5)    # 11 (5*2=10, 10+1=11)

# Operador << (orden inverso)
transformar2 = incrementar << doble
puts transformar2.call(5)   # 11 (mismo resultado)

# Cadena más larga
pipeline = doble >> incrementar >> cuadrado
puts pipeline.call(3)       # 49 (3*2=6, 6+1=7, 7²=49)

Currying

# curry — convierte una función de N args en N funciones de 1 arg
suma = ->(a, b) { a + b }
suma_curried = suma.curry

suma_5 = suma_curried.(5)
puts suma_5.(3)       # 8
puts suma_5.(10)      # 15

# Currying práctico
filtrar = ->(condicion, array) { array.select(&condicion) }
filtrar_pares = filtrar.curry.(method(:even?).to_proc)

# Multiplicador con curry
multiplicar = ->(a, b) { a * b }.curry
doble = multiplicar.(2)
triple = multiplicar.(3)

puts [1, 2, 3].map(&doble).inspect    # [2, 4, 6]
puts [1, 2, 3].map(&triple).inspect   # [3, 6, 9]

Memoization

# Básico con hash
def fibonacci(n, memo = {})
  return n if n <= 1
  memo[n] ||= fibonacci(n - 1, memo) + fibonacci(n - 2, memo)
end

puts fibonacci(100)   # instantáneo

# Patrón memoize genérico
module Memoizable
  def memoize(method_name)
    original = instance_method(method_name)
    define_method(method_name) do |*args|
      @_memo ||= {}
      key = [method_name, args]
      @_memo[key] ||= original.bind(self).call(*args)
    end
  end
end

class Calculadora
  extend Memoizable

  def factorial(n)
    return 1 if n <= 1
    n * factorial(n - 1)
  end

  memoize :factorial
end

Pipelines con then/yield_self

# then (alias de yield_self, Ruby 2.6+)
resultado = 5
  .then { |n| n * 2 }
  .then { |n| n + 1 }
  .then { |n| n.to_s }
  .then { |s| "Resultado: #{s}" }
# "Resultado: 11"

# Práctico: transformar datos
usuario = { nombre: "  Ana García  ", email: "ANA@TEST.COM" }

procesado = usuario
  .then { |u| u.merge(nombre: u[:nombre].strip) }
  .then { |u| u.merge(email: u[:email].downcase) }
  .then { |u| u.merge(slug: u[:nombre].downcase.gsub(" ", "-")) }
# {nombre: "Ana García", email: "ana@test.com", slug: "ana-garcía"}

# tap — ejecutar side effects sin romper la cadena
[3, 1, 4, 1, 5]
  .tap { |a| puts "Original: #{a}" }
  .sort
  .tap { |a| puts "Ordenado: #{a}" }
  .reverse
  .tap { |a| puts "Invertido: #{a}" }

Patrones funcionales

Map / Filter / Reduce

pedidos = [
  { producto: "Laptop", precio: 999, cantidad: 1 },
  { producto: "Mouse", precio: 29, cantidad: 3 },
  { producto: "Monitor", precio: 499, cantidad: 2 },
  { producto: "Teclado", precio: 79, cantidad: 1 },
  { producto: "Cable", precio: 9, cantidad: 5 }
]

# Total de pedidos mayores a $50
total_premium = pedidos
  .map { |p| { **p, total: p[:precio] * p[:cantidad] } }
  .select { |p| p[:total] > 50 }
  .reduce(0) { |sum, p| sum + p[:total] }
# 2076

Either pattern

class Either
  attr_reader :value

  def self.right(value) = Right.new(value)
  def self.left(value) = Left.new(value)
end

class Right < Either
  def initialize(value) = @value = value
  def right? = true
  def left? = false
  def map(&block) = Right.new(block.call(@value))
  def flat_map(&block) = block.call(@value)
  def or_else(_) = self
end

class Left < Either
  def initialize(value) = @value = value
  def right? = false
  def left? = true
  def map(&_block) = self
  def flat_map(&_block) = self
  def or_else(default) = Right.new(default)
end

def parse_int(str)
  Integer(str)
    .then { |n| Either.right(n) }
rescue ArgumentError
  Either.left("'#{str}' no es un número")
end

resultado = parse_int("42")
  .map { |n| n * 2 }
  .map { |n| "Resultado: #{n}" }

puts resultado.value   # "Resultado: 84"

error = parse_int("abc")
  .map { |n| n * 2 }
  .or_else(0)

puts error.value   # 0

Enumerator como generador

# Crear secuencias infinitas
naturales = Enumerator.new do |y|
  n = 1
  loop do
    y.yield n
    n += 1
  end
end

puts naturales.lazy.select(&:odd?).first(5).inspect
# [1, 3, 5, 7, 9]

# Collatz sequence
def collatz(n)
  Enumerator.new do |y|
    current = n
    loop do
      y.yield current
      break if current == 1
      current = current.even? ? current / 2 : current * 3 + 1
    end
  end
end

puts collatz(27).to_a.length   # 112 pasos

Resumen

Concepto Descripción
->() { } Lambda (función anónima)
curry Aplicación parcial
>> / << Composición de funciones
then Pipeline de transformaciones
tap Side effects en cadena
method(:name) Referencia a método
lazy Evaluación diferida
Memoize Cache de resultados
🔒

Ejercicio práctico disponible

Composición, curry y programación funcional

Desbloquear ejercicios
// Composición, curry y programación funcional
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes