[SPA] Introducción práctica a la programación funcional
Actualizado el 03/Mayo/2017 - Se agrega el uso de colecciones immutables.
pero ... ¿porque necesito saber programación funcional?
Para tu vida profesional es muy bueno que manejes al menos tres tipos de lenguajes
- Orientado a Objetos (c#, java)
- Dinámico (javascript, python)
- Funcional (scala, f#, Haskeel)
Porque detrás del lenguaje en si, esta la forma en resolver el problema. Y eso es lo que más cuesta de la programación funcional. No hablo de que una es superior a la otra, sino que son dos paradigmas diferentes y que es bueno que las tengas en tu caja de herramientas mental.
¿Que necesitas saber para seguir leyendo
- Estar familiarizado con los conceptos (clase, objetos, polimorfismo y herencia) de programación orientada a objetos (POO en adelante)
- Poder leer código c#
¿Que no necesitas?
- Ser un experto en POO
- Ser un maestro ninja de c#
- Conocer algo de Haskeel, Scala, F# y otros lenguajes funcionales.
¿porque tengo que confiar en las mates?
En la wikipedia, si buscas programación funcional dice
En ciencias de la computación, la programación funcional es un paradigma de programación declarativa basado en el uso de funciones matemáticas...
¿que tienen las mates que me ayudaran a ser mejor programador? ¿porque tengo que confiar en ellas cuando casi arruinan mi adolescencia? :)
Vamos a empezar con un ejemplo sencillo. Una función que me devuelve el iva de un importe dado.
Primero definimos la función, en mates sera IVA(importe, porcentajeIva) = importe * (porcentajeIva/100)
mientras que en C# sera
public decimal GetVat(double amount, double percentageVAT)
{
return (amount * (percentageVAT/100));
}
Ahora llamamos a la función definida con el valor 100 y 21
En mates IVA(100, 21)
y en C# var result = GetVat(100, 21);
De este ejemplo, vemos 3 cosas en común que tienen las mates y esta función de c#
- Solo se dedican a calcular el iva.
- La única dependencia que tienen es hacia las variables de entrada.
- Para un conjunto de valores de entrada solo le corresponde un valor de salida. No importa cuantas veces ponga 100 y 21, siempre me devolverá 21.
Primero: La S de solid.
En las mates, sumar solo suma. Puedes hacer que se combinen diferentes fórmulas, pero cada cual se dedica a lo suyo, tiene una sola responsabilidad, es decir son Single responsability, la S de SOLID
Ahora déjame poner una función típica de programación orientada a objetos, agregar un detalle de factura a una factura.
void AddLine(InvoiceLine newInvoiceLine)
{
this.lines.Add(newInvoiceLine); // 1
this.totalPrice += newInvoiceLine.Total; //2
}
Una de las propiedades de la programación orientada a objetos en la encapsulación, y esta función lo cumple muy bien porque oculta lo que sucede después de agregar. Pero como bien te haz dado cuenta, hace dos tareas, 1 - agrega un item y 2 - recalcula el total.
Lo primero que piensas es en separarlo en dos métodos
void AddLine(InvoiceLine newInvoiceLine)
{
this.lines.add(newInvoiceLine);
}
void UpdateTotal(InvoiceLine newInvoiceLine)
{
this.totalPrice += newInvoiceLine.Total;
}
Pero el problema de esta solución es que generas una dependencia entre las funciones, teniendo que ser la segunda llamada luego de la primera para que el estado interno siga siendo coherente. Así que vamos a cambiar la clase para que el total sea una función en lugar de un resultado materializado.
decimal GetTotal()
{
decimal total = 0;
foreach(var line in this.lines)
{
total += line.Total;
}
return total;
}
Ahora podemos quitar variable totalPrice
porque es reemplazada por la función GetTotal
.
Podemos hacer la función GetTotal un poco diferente, que tengo un toque más funcional sin usar la variable interna total, para ello usaremos Linq.
decimal GetTotal()
{
return this.details.Sum(item => item.Price);
}
O mejor aún, como una propiedad de solo lectura
public decimal Total
{
get { return Details.Sum(l => l.Total); }
}
Ahora bien, apunta la primer lección de la programación funcional. Cuando refactorizas hacía programación funcional el estado interno de los objetos se reduce.
Ahora tenemos el primer objetivo completo. Cada función tiene una única responsabilidad.
Segundo: Sin efectos colaterales.
Cuando una función causa un cambio en algo fuera de su alcance, se llama side effect. Para ello la función tiene que recibir todo lo necesario para poder completar su responsabilidad por medio de parámetros.
Continuando con el ejemplo de la factura, imagina que tienes un método llamado cancelar.En el momento que cancelas la factura actualizas el saldo del cliente.
private Customer customer;
public void Cancel()
{
this.state = StateKind.canceled;
this.customer.UpdateBalance(this.GetTotal());
}
Ahora pongamos un escenario donde el cliente tiene un saldo de 500 € y se cancela una factura de 100 €.
customer.Balance; // 500€
invoice.Cancel();
customer.Balance // 600€
Este es un ejemplo claro de side effect y de una función que no es matemática ya que no devuelve siempre el mismo resultado. ¿Como puedo hacer para que Cancelar sea no-side-effect?
Pues otra vez volvemos al tema de responsabilidad. No es la responsabilidad de cancelar la factura actualizar el saldo del cliente.
public void Cancel()
{
this.state = StateKind.canceled;
}
La clase Cliente será la encargada de recibir una factura para cancelar y así actualizar su saldo. Después de verificar que la factura está cancelada, ajustará el saldo.
public class Customer
{
void CancelInvoice(Invoice invoice)
{
if (invoice.State == StateKind.canceled;)
{
this.UpdateBalance(invoice.Total);
}
}
}
Con este diseño tenemos dos buenas noticias.
1. El método Cancelar ya no necesita de la clase cliente. Puede que en otra parte de la clase factura si, pero este metodo ya no. Esto hace que el alcance (Bounded context) del objeto cliente para el sistema de facturación quede más limitado.
2. En la clase cliente ahora el método ActualizarSaldo puede ser un método privado.
Volviendo al ejemplo anterior ahora tenemos que
customer.Balance; // 500€
invoice.Cancel();
customer.Balance; // still is 500€
customer.CancelInvoice(invoice);
customer.Balance; //now is 600€
Le hemos pasado la responsabilidad al cliente para que haga las dos llamadas en forma secuencial.
A pesar que este diseño es mejor, no es 100% funcional, porque el método Cancelar modifica una variable (estado) que no se recibe por parámetro.
Tecero: funciones puras o transparencia referencial
En mates, todas las funciones son referencialmente transparentes. El coseno de un ángulo de 90 grados sigue siendo 0, no importa cuantos años han pasado y cuantas veces lo repitas, siempre Cos(90) = 0. No importa como le pases la referencia Cos(89+1) o Cos(91-1), te dará lo mismo.
Volvamos a la función AddInvoice. Aunque es perfectamente valida en programación orientada a objetos, en términos de transparencia referencial no lo es porque cambia el estado interno al agregar un ítem a la lista.
void AddLine(InvoiceLine newInvoiceLine)
{
this.lines.add(newInvoiceLine);
}
Para que esta función sea una función pura y no-side-effect necesitamos algunas cosas
1 - Poner un constructor donde le poner todas las propiedades del objeto.
2 - La lista interna que esta como List, pasarla a ImmutableList.
3 - Crear una funcion local que maneje los cambios en la coleccion, y si hay cambios que retorne una nueva instancia de la clase Invoice.
4 - Agregar el item a la lista immutable.
5 - Devolver una nueva instance de Invoice en lugar del void que hay ahora.
Pues manos a la obra. Primero paso. En los siguientes pasos necesitaremos crear una nueva instancia de Invoice con los nuevos valores. PAra ello necesitamos un constructor. En el constructor usaremos un IEnumerable para hacer la llamada al constructor mas facil, pero internamente lo convertimos a Immutable.
public Invoice(IEnumerable<InvoiceLine> lines, StateKind state)
{
this.lines = lines.ToImmutableList();
State = state;
}
Segundo paso.
private ImmutableList<InvoiceLine> lines;
public IEnumerable<InvoiceLine> Lines
{
get { return lines; }
}
Tercer paso. Nuevo metodo local. Si la instancia de immutable list son diferentes, entonces hay que crear un nuevo objeto Invoice.
private Invoice WithLines(IEnumerable<InvoiceLine> value)
{
return Object.ReferenceEquals(lines, value)
? this
: new Invoice(value, State);
}
Cuarto y quinto paso. Devolver la nueva instancia.
public Invoice AddLine(InvoiceLine newOrderLine)
{
return WithLines(lines.Add(newOrderLine));
}
Que pasa con el metodo Cancel? Lo haremos immutable tambien.
public Invoice Cancel()
{
return (State == StateKind.canceled)
? this
: new Invoice(Lines, StateKind.canceled);
}
Nota dos, cuando intentas hacer transparencia funcional cae por su propio peso la inmutabilidad de los datos. Es decir, la inmutabilidad es una consecuencia de la transparencia referencial que se usa en las mates.
Genial!! Ahora la clase invoice es immutable, todas sus propiedades son de solo lectura y los metodos devuelven una nueva instancia cuando realizan un cambio.
Ahora, hacemos lo mismo en la clase Customers.
public class Customer
{
public Customer(decimal balance)
{
this.Balance = balance;
}
public decimal Balance { get; private set; }
public Customer CancelInvoice(Invoice invoice)
{
return (invoice.State == StateKind.canceled)
? DiscountFromBalance(invoice.Total)
: this;
}
private Customer DiscountFromBalance(decimal toTake)
{
return new Customer(Balance - toTake);
}
}
Ahora veamos como se llama a estas funciones. Empezaremos suponiendo que la lista de detalles tiene 2 items, uno de
List<InvoiceLine> lines = new List<InvoiceLine>
{
new InvoiceLine(1, 5),
new InvoiceLine(1, 15)
};
Invoice invoice = new Invoice(lines, StateKind.incomplete);
var invoiceWithNewItem = invoice.AddLine(new InvoiceLine(1, 20));
Console.WriteLine(__aSyNcId_<_uOwLamRC__quot;older total -> {invoice.Total}");
Console.WriteLine(__aSyNcId_<_uOwLamRC__quot;new total -> {invoiceWithNewItem.Total}");
Lo realmente importante aquí es que no importa cuantas veces ejecutes invoice.Total;
siempre te devolverá 20 porque a) todos los metodos retornan una nueva instancia y b) porque el calculo del total no guarda ninguna información interna.
Ahora lo haremos con la clase customers.
Entonces ahora lo vemos así
Customer customer = new Customer(500);
Console.WriteLine(customer.Balance);
var invoiceCanceled = invoice.Cancel();
var customerWithInvoiceCanceledProcessed = customer.CancelInvoice(invoiceCanceled);
Console.WriteLine(customerWithInvoiceCanceledProcessed.Balance);
Conclusiones y próximos pasos / desafios
Vimos una intro muy práctica de programación funcional -eso espero :) - y de cómo las mates nos pueden ayudar a programar por medio de funciones que:
- Solo tengan una responsabilidad.
- Sean no side effect.
- Usen transparencia referencial.
Vimos como impacto esto en el diseño de nuestra aplicación. Y cómo nos ayuda a pensar diferente a la hora de resolver un problema.
Pero solo hemos rascado un poco, nos deja algunas preguntas que iremos viendo en otros artículos como:
1 - No cabe duda que los datos inmutables son seguros, pero ¿qué pasa con el rendimiento? Si tengo que agregar 1000 item a la factura creará mil instancias diferentes de la lista para terminar usando sola la última. ¿Que pasa con la memoria?
2 - Si en los métodos no se guarda estado que vaya mutando, ¿qué pasa con las variables de índice del comando for o con la variable de item en un foreach?