c# - programacion - ¿Qué tarea se realiza mejor en un estilo de programación funcional?
programacion imperativa (16)
Recientemente descubrí el estilo de programación funcional [...] Bueno, recientemente tuve la oportunidad de dar una charla sobre cómo reducir los esfuerzos de desarrollo de software, y quería introducir el concepto de programación funcional.
Si acaba de descubrir la programación funcional, no recomiendo tratar de hablar con autoridad sobre el tema. Sé durante los primeros 6 meses mientras aprendía F #, todo mi código era solo C # con una sintaxis un poco más incómoda. Sin embargo, después de ese período de tiempo, pude escribir código consistentemente bueno en un estilo idiomático y funcional.
Te recomiendo que hagas lo mismo: espera durante aproximadamente 6 meses hasta que el estilo de programación funcional sea más natural, luego da tu presentación.
Intento ilustrar los beneficios de la programación funcional, y tuve la idea de mostrarle a las personas 2 conjuntos de códigos que hacen lo mismo, uno codificado de manera muy imperativa, y el otro de una manera muy funcional, para mostrar que la programación funcional puede hacer que el código sea más corto, más fácil de entender y, por lo tanto, mantener. ¿Hay algún ejemplo, además del famoso ejemplo de suma de cuadrados de Luca Bolognese?
Realicé una presentación de F # para el grupo de usuarios de .NET en mi área, y muchas personas de mi grupo quedaron impresionadas por la coincidencia de patrones de F #. Específicamente, mostré cómo atravesar un árbol sintáctico abstracto en C # y F #:
using System;
namespace ConsoleApplication1
{
public interface IExprVisitor<t>
{
t Visit(TrueExpr expr);
t Visit(And expr);
t Visit(Nand expr);
t Visit(Or expr);
t Visit(Xor expr);
t Visit(Not expr);
}
public abstract class Expr
{
public abstract t Accept<t>(IExprVisitor<t> visitor);
}
public abstract class UnaryOp : Expr
{
public Expr First { get; private set; }
public UnaryOp(Expr first)
{
this.First = first;
}
}
public abstract class BinExpr : Expr
{
public Expr First { get; private set; }
public Expr Second { get; private set; }
public BinExpr(Expr first, Expr second)
{
this.First = first;
this.Second = second;
}
}
public class TrueExpr : Expr
{
public override t Accept<t>(IExprVisitor<t> visitor)
{
return visitor.Visit(this);
}
}
public class And : BinExpr
{
public And(Expr first, Expr second) : base(first, second) { }
public override t Accept<t>(IExprVisitor<t> visitor)
{
return visitor.Visit(this);
}
}
public class Nand : BinExpr
{
public Nand(Expr first, Expr second) : base(first, second) { }
public override t Accept<t>(IExprVisitor<t> visitor)
{
return visitor.Visit(this);
}
}
public class Or : BinExpr
{
public Or(Expr first, Expr second) : base(first, second) { }
public override t Accept<t>(IExprVisitor<t> visitor)
{
return visitor.Visit(this);
}
}
public class Xor : BinExpr
{
public Xor(Expr first, Expr second) : base(first, second) { }
public override t Accept<t>(IExprVisitor<t> visitor)
{
return visitor.Visit(this);
}
}
public class Not : UnaryOp
{
public Not(Expr first) : base(first) { }
public override t Accept<t>(IExprVisitor<t> visitor)
{
return visitor.Visit(this);
}
}
public class EvalVisitor : IExprVisitor<bool>
{
public bool Visit(TrueExpr expr)
{
return true;
}
public bool Visit(And expr)
{
return Eval(expr.First) && Eval(expr.Second);
}
public bool Visit(Nand expr)
{
return !(Eval(expr.First) && Eval(expr.Second));
}
public bool Visit(Or expr)
{
return Eval(expr.First) || Eval(expr.Second);
}
public bool Visit(Xor expr)
{
return Eval(expr.First) ^ Eval(expr.Second);
}
public bool Visit(Not expr)
{
return !Eval(expr.First);
}
public bool Eval(Expr expr)
{
return expr.Accept(this);
}
}
public class PrettyPrintVisitor : IExprVisitor<string>
{
public string Visit(TrueExpr expr)
{
return "True";
}
public string Visit(And expr)
{
return string.Format("({0}) AND ({1})", expr.First.Accept(this), expr.Second.Accept(this));
}
public string Visit(Nand expr)
{
return string.Format("({0}) NAND ({1})", expr.First.Accept(this), expr.Second.Accept(this));
}
public string Visit(Or expr)
{
return string.Format("({0}) OR ({1})", expr.First.Accept(this), expr.Second.Accept(this));
}
public string Visit(Xor expr)
{
return string.Format("({0}) XOR ({1})", expr.First.Accept(this), expr.Second.Accept(this));
}
public string Visit(Not expr)
{
return string.Format("Not ({0})", expr.First.Accept(this));
}
public string Pretty(Expr expr)
{
return expr.Accept(this).Replace("(True)", "True");
}
}
class Program
{
static void TestLogicalEquivalence(Expr first, Expr second)
{
var prettyPrinter = new PrettyPrintVisitor();
var eval = new EvalVisitor();
var evalFirst = eval.Eval(first);
var evalSecond = eval.Eval(second);
Console.WriteLine("Testing expressions:");
Console.WriteLine(" First = {0}", prettyPrinter.Pretty(first));
Console.WriteLine(" Eval(First): {0}", evalFirst);
Console.WriteLine(" Second = {0}", prettyPrinter.Pretty(second));
Console.WriteLine(" Eval(Second): {0}", evalSecond);;
Console.WriteLine(" Equivalent? {0}", evalFirst == evalSecond);
Console.WriteLine();
}
static void Main(string[] args)
{
var P = new TrueExpr();
var Q = new Not(new TrueExpr());
TestLogicalEquivalence(P, Q);
TestLogicalEquivalence(
new Not(P),
new Nand(P, P));
TestLogicalEquivalence(
new And(P, Q),
new Nand(new Nand(P, Q), new Nand(P, Q)));
TestLogicalEquivalence(
new Or(P, Q),
new Nand(new Nand(P, P), new Nand(Q, Q)));
TestLogicalEquivalence(
new Xor(P, Q),
new Nand(
new Nand(P, new Nand(P, Q)),
new Nand(Q, new Nand(P, Q)))
);
Console.ReadKey(true);
}
}
}
El código anterior está escrito en un estilo idiomático de C #. Utiliza el patrón de visitante en lugar de la prueba de tipo para garantizar la seguridad del tipo. Esto es aproximadamente 218 LOC.
Aquí está la versión F #:
#light
open System
type expr =
| True
| And of expr * expr
| Nand of expr * expr
| Or of expr * expr
| Xor of expr * expr
| Not of expr
let (^^) p q = not(p && q) && (p || q) // makeshift xor operator
let rec eval = function
| True -> true
| And(e1, e2) -> eval(e1) && eval(e2)
| Nand(e1, e2) -> not(eval(e1) && eval(e2))
| Or(e1, e2) -> eval(e1) || eval(e2)
| Xor(e1, e2) -> eval(e1) ^^ eval(e2)
| Not(e1) -> not(eval(e1))
let rec prettyPrint e =
let rec loop = function
| True -> "True"
| And(e1, e2) -> sprintf "(%s) AND (%s)" (loop e1) (loop e2)
| Nand(e1, e2) -> sprintf "(%s) NAND (%s)" (loop e1) (loop e2)
| Or(e1, e2) -> sprintf "(%s) OR (%s)" (loop e1) (loop e2)
| Xor(e1, e2) -> sprintf "(%s) XOR (%s)" (loop e1) (loop e2)
| Not(e1) -> sprintf "NOT (%s)" (loop e1)
(loop e).Replace("(True)", "True")
let testLogicalEquivalence e1 e2 =
let eval1, eval2 = eval e1, eval e2
printfn "Testing expressions:"
printfn " First = %s" (prettyPrint e1)
printfn " eval(e1): %b" eval1
printfn " Second = %s" (prettyPrint e2)
printfn " eval(e2): %b" eval2
printfn " Equilalent? %b" (eval1 = eval2)
printfn ""
let p, q = True, Not True
let tests =
[
p, q;
Not(p), Nand(p, p);
And(p, q),
Nand(Nand(p, q), Nand(p, q));
Or(p, q),
Nand(Nand(p, p), Nand(q, q));
Xor(p, q),
Nand(
Nand(p, Nand(p, q)),
Nand(q, Nand(p, q))
)
]
tests |> Seq.iter (fun (e1, e2) -> testLogicalEquivalence e1 e2)
Console.WriteLine("(press any key)")
Console.ReadKey(true) |> ignore
Esto es 65 LOC. Dado que utiliza la coincidencia de patrones en lugar del patrón de visitante, no perdemos ningún tipo de seguridad, y el código es muy fácil de leer.
Cualquier tipo de procesamiento simbólico es de órdenes de magnitud más fáciles de escribir en F # que en C #.
[Editar para agregar:] Ah, y la coincidencia de patrones no es solo un reemplazo para el patrón de visitante, sino que también le permite coincidir con la forma de los datos. Por ejemplo, aquí hay una función que convierte Nand''s a sus equivalentes:
let rec simplify = function
| Nand(p, q) when p = q -> Not(simplify p)
| Nand(Nand(p1, q1), Nand(p2, q2))
when equivalent [p1; p2] && equivalent [q1; q2]
-> And(simplify p1, simplify q1)
| Nand(Nand(p1, p2), Nand(q1, q2))
when equivalent [p1; p2] && equivalent [q1; q2]
-> Or(simplify p1, simplify q1)
| Nand(Nand(p1, Nand(p2, q1)), Nand(q2, Nand(p3, q3)))
when equivalent [p1; p2; p3] && equivalent [q1; q2; q3]
-> Xor(simplify p1, simplify q1)
| Nand(p, q) -> Nand(simplify p, simplify q)
| True -> True
| And(p, q) -> And(simplify p, simplify q)
| Or(p, q) -> Or(simplify p, simplify q)
| Xor(p, q) -> Xor(simplify p, simplify q)
| Not(Not p) -> simplify p
| Not(p) -> Not(simplify p)
No es posible escribir este código de forma concisa en C #.
Recientemente descubrí el estilo de programación funcional y estoy convencido de que reducirá los esfuerzos de desarrollo, facilitará la lectura del código y hará que el software sea más fácil de mantener. Sin embargo, el problema es que no logré convencer a nadie.
Bueno, recientemente tuve la oportunidad de dar una charla sobre cómo reducir el desarrollo de software y los esfuerzos de mantenimiento, y quería presentarles el concepto de programación funcional y cómo beneficia al equipo. Tuve la idea de mostrarles a las personas 2 juegos de códigos que hacen exactamente lo mismo, uno codificado de manera muy imperativa, y el otro de una manera muy funcional, para mostrar que la programación funcional puede hacer que el código sea más corto, más fácil de entender y así mantenible. ¿Hay algún ejemplo, además del famoso ejemplo de suma de cuadrados de Luca Bolognese?
Muestra cómo codificar una distinta de una matriz. Distinct es muy fácil en SQL pero fue difícil en una matriz de memoria. Ahora es fácil distinguir una matriz con LINQ.
Puede explicarles que habrá PARRALEL LINQ (PLINQ) en el futuro. Cuando comiences a escribir código funcional, será más fácil parralelizar tu aplicación. Google usa MapReduce extensivamente.
Explíqueles que LINQ es un lenguaje de consulta para manipular los diferentes tipos de datos. En la memoria, en una base de datos, excell, servicios web, archivos xml, archivos JSON. Es algún tipo de SQL universal. Sin embargo, las personas que no les gusta SQL estarán menos convencidas.
No hablaría mucho sobre programación funcional, explicaría cómo LINQ puede ayudar a los desarrolladores.
Es interesante que nadie realmente ha respondido la pregunta: ¿qué tarea se hace mejor en un "estilo funcional"?
Un programa / algoritmo consta de 2 partes: control lógico y estructura de datos. Creo que las tareas mejor hechas en un estilo funcional son aquellas listas o arreglos involucrados en los casos en que se comportan como listas (por ejemplo, qsort). No es coincidencia que los lenguajes de programación funcionales tengan un muy buen soporte para listas.
Cuando las estructuras de datos se desvían de las listas, creo que el uso de un estilo de programación funcional se vuelve un poco "antinatural".
Hay muchos ejemplos, pero ninguno será tan impactante como usar una muestra relevante para uno de sus proyectos en el trabajo. Ejemplos como "Sum Of Squares" de Luca son increíbles, pero si alguien lo usara como prueba de cómo se podría escribir mejor nuestra base de códigos, no estaría convencido. Todo el ejemplo demuestra que algunas cosas son mejores para escribir funcionalmente. Lo que necesita probar es que su código base está mejor escrito funcionalmente
Mi consejo sería elegir algunos puntos problemáticos populares y algunos puntos clave en la base de códigos, y reescribirlos en un estilo funcional. Si puede demostrar una solución sustancialmente mejor, le será de gran ayuda a los compañeros de trabajo.
Los algoritmos que implican la búsqueda de retroceso y la simplificación del soporte de deshacer en las GUI son dos lugares en los que he utilizado el estilo funcional en la práctica.
Muéstreles la forma en jQuery de iterar sobre elementos DOM:
$(".magic-divs").click(function(){
// FYI, in this context, "this" will be the element clicked on.
alert("somebody clicked on " + this.id);
this.hide();
});
$(".magic-divs").show();
vs. cómo la mayoría de los resultados de google para "javascript element by classname" lo hacen:
var arrayOfElements = // this is filled with the elements somehow
for(var i=0,j=arrayOfElements.length; i<j; i++) {
alert("now I get to add an onclick event somehow " + i);
}
// i dont even want to type the ugly for-loop stuff to hide every div...
¡La programación funcional es útil en cosas cotidianas como la anterior!
(nota: no sé si mi ejemplo se ajusta a la definición exacta de programación funcional, pero si lo hace, la programación funcional es impresionante)
Otro ejemplo sería el algoritmo Quicksort . Se puede describir muy brevemente en un lenguaje funcional como Haskell:
qsort [] = []
qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++ qsort (filter (>= x) xs)
pero necesita un poco más de codificación en un lenguaje iterativo. En el sitio web al que se hace referencia también puede encontrar muchos otros ejemplos con comparaciones entre idiomas.
Para lograr lo que desea, y para comunicarlo a otras personas de su organización, debe demostrar que el negocio de su empresa se está construyendo de una mejor manera.
No sirve de nada usar un par de algoritmos para demostrar el poder de la programación funcional si es totalmente inútil para su dominio comercial. Por lo tanto, tome un código existente y vuelva a escribirlo funcionalmente. Si puedes demostrarlo, es mejor que la gente te escuche; les has mostrado un ejemplo concreto y relevante. Si no puede, tal vez la programación funcional no sea la solución que ha estado buscando.
Si con el ''estilo funcional'' se entiende el uso de conceptos como ''mapa'', ''aplicar'', ''reducir'', ''filtro'', funciones lambda y listas de comprensiones, entonces debería ser evidente que el código tiene que tratar con operaciones en listas es casi siempre más conciso cuando está escrito en el "estilo funcional". Pero si mezclas un "estilo funcional" con un código imperativo en un lenguaje predominantemente imperativo, realmente es solo una cuestión de estilo.
En python, por ejemplo, puede volver a implementar Haskell qsort crackmigg publicado de la siguiente manera:
def qsort(list):
if list == []:
return []
else:
x = list[0]; xs = list[1:]
return qsort(filter(lambda y: y<x, xs)) + [x] + qsort(filter(lambda y: y>= x, xs))
Aunque está escribiendo la última línea como
return qsort([y for y in xs if y<x]) + [x] + qsort([y for y in xs if y>=x])
es posiblemente más "pitónico".
Pero esto es obviamente más conciso que la implementación aquí:
http://hetland.org/coding/python/quicksort.html
Que, por cierto, es cómo habría pensado en implementarlo antes de conocer a Haskell.
La versión funcional es extremadamente clara si y solo si está acostumbrado a la programación funcional y piensa qué filter
va a hacer tan fácilmente como el desgastado programador C ++ crea un bucle sin siquiera tener que pensar mucho al respecto. Y eso es claramente lo que realmente está en juego aquí: la programación funcional de "estilo" es una mentalidad completamente diferente . Si las personas con las que trabajas no están acostumbradas a pensar de manera recursiva, y no son del tipo de las que emocionarse, no solo una nueva tecnología, sino una manera completamente diferente de pensar sobre cómo resolver sus problemas, entonces cualquier cantidad de comparaciones de códigos no es posible. los va a ganar.
Tareas para un estilo funcional? Cada vez que tenga un patrón de codificación común y desee reducirlo. Hace un tiempo escribí un poco sobre el uso de C # para el estilo funcional, al tiempo que me aseguré de que fuera práctico: Practical Functional C # (dudo en vincular mis propias cosas aquí, pero creo que es relevante en este caso). Si tiene una aplicación común de "empresa", mostrar, por ejemplo, cómo son bellas las expresiones en la coincidencia de patrones, no será demasiado convincente.
Pero en las aplicaciones del mundo real, hay TONELADAS de patrones que aparecen en un nivel bajo, de codificación. Usando funciones de orden superior, puedes hacer que desaparezcan. Como muestro en ese conjunto de publicaciones de blog, mi ejemplo favorito es el patrón "try-close / finally-abort" de WCF. El patrón "try / finally-dispose" es tan común que se convirtió en una palabra clave de idioma: using. Lo mismo para "bloqueo". Ambas están trivialmente representadas como funciones de orden superior, y solo porque C # no las soportaba originalmente necesitamos palabras clave de lenguaje codificadas para respaldarlas. (Rápido: cambie sus bloques de "bloqueo" para usar un bloqueo ReaderWriter. Vaya, primero deberemos escribir una función de orden superior).
Pero quizás convencer solo requiere mirar a Microsoft. ¿Genéricos también conocidos como polimorfismo paramétrico? Eso es difícilmente OO, pero es un concepto funcional que, ahora, todos aman. El lindo marco de Ninject no funcionaría sin él. Lambdas? Como árboles de expresión, son como LINQ, Fluither NHibernate, etc. obtienen todo su poder. Nuevamente, eso no viene de OO o programación imperativa. ¿La nueva biblioteca de Threading? Bastante feo sin cierres.
Entonces, la programación funcional ha estado bendiciendo cosas como .NET en la última década. Los principales avances (como los genéricos, "LINQ") provienen directamente de los lenguajes funcionales. ¿Por qué no te das cuenta de que hay algo y se involucra más en eso? Así es como lo diría a los escépticos.
El problema más grande es lograr que las personas hagan un salto en la comprensión de las funciones de orden superior. Si bien es bastante fácil, si nunca lo has visto antes en tu vida, podría ser chocante e incomprensible. (Diablos, parece que mucha gente piensa que los genéricos son solo para colecciones seguras para tipos, y LINQ es solo SQL incrustado).
Entonces, lo que debes hacer es ir a través de tu base de código y encontrar lugares que son una pesadilla imperativa demasiado complicada. Busque los patrones subyacentes y use las funciones para unirlos entre sí. Si no puede encontrar ninguno, puede conformarse con solo las listas de demostración. Por ejemplo, "encuentra todos los Foos en esta lista y elimínalos". Eso es una cosa de una línea en el estilo funcional "myList.Remove (x => x.Bla> 0)" versus 7 líneas en estilo C # (crea una lista temporal, recorre y agrega para quitar elementos, repite el ciclo y elimina los elementos )
La esperanza es que, aunque la sintaxis es impar, la gente reconocerá "wow, eso es mucho más simple". Si pueden dejar de lado "verboso == más legible" y "eso parece confuso" por un tiempo, tendrás una oportunidad.
Buena suerte.
Un buen ejemplo podría ser crear su propio lenguaje de programación usando el existente, donde tendrá que usar Mónadas .
Con F # es mucho más simple escribir lógica de análisis que con C #.
Eche un vistazo a este artículo: Funcional .NET - LINQ o Language Integrated Monads?
Un ejemplo simple de una tarea que a menudo es más fácil de realizar en un estilo funcional es la transformación de datos de una forma a otra. La "suma de cuadrados" es un ejemplo trivial de transformación de datos. La charla de Luca en el PDC del año pasado mostró cómo usar este tipo de transformación de datos para algo más práctico, descargando y analizando cotizaciones de acciones. La demostración se realiza en F #, pero los conceptos son los mismos y se pueden aplicar a C # o a la mayoría de los otros lenguajes de programación.
Esencialmente, el paradigma funcional es altamente efectivo para el procesamiento paralelo:
"Lo realmente interesante que quiero que noten, aquí, es que tan pronto como piensen en el mapa y reduzcan como funciones que todos pueden usar, y los usan, solo tienen que obtener un supergenius para escribir el código duro para ejecutar mapear y reducir en un conjunto de computadoras paralelo masivamente paralelo, y todo el código anterior que solía funcionar bien cuando se ejecuta un ciclo solo funciona un billón de veces más rápido, lo que significa que puede usarse para abordar grandes problemas en un instante.
Déjame repetir eso. Al abstraer el concepto mismo de bucle, puede implementar bucles de la forma que desee, incluida su implementación de una manera que se adapta bien con hardware adicional ".
Hace poco saqué un pequeño truco para hacer lambdas, pasé a mis métodos de extensión para ver más F # ish. Aquí va:
Lo que quería hacer era algo así como:
3.Times(() => Console.WriteLine("Doin'' it"));
Ahora el método de extensión para eso se implementa fácilmente:
public static void Times(this int times, Action action)
{
Enumerable.Range(1, times).ToList().ForEach(index => action());
}
Lo que no me gustó es que estoy especificando el índice aquí: ForEach(index => action())
aunque nunca se usa, así que lo reemplacé con a _
y obtuve ForEach(_ => action())
Eso está bien, pero ahora estaba motivado para que mi código de llamada se viera similar
(Nunca me gustó el "()" al comienzo de la expresión lambda), así que en vez de: 3.Times(() => ...);
Yo quería 3.Times(_ => ...);
La única forma de implementar esto fue pasar un parámetro falso al método de extensión, que nunca se usa, así que lo modifiqué así:
public static void Times(this int times, Action<byte> action)
{
Enumerable.Range(1, times).ToList().ForEach(_ => action(byte.MinValue));
}
Esto me permite llamarlo así:
3.Times(_ => Console.WriteLine("Doin'' it"));
No fue un gran cambio, pero aún así disfruté, fue posible hacer ese pequeño ajuste tan fácilmente y al mismo tiempo eliminar el ruido "()" lo hace mucho más legible.
El mejor documento de defensa jamás escrito para el estilo funcional es un documento de John Hughes llamado Why Functional Programming Matters . Sugiero que haga algunos ejemplos para usted hasta que llegue a una etapa en la que pueda presentar de manera convincente los argumentos expuestos en ese documento.
Muchos de los ejemplos en el documento son numéricos y no resuenan con las audiencias de hoy. Un ejercicio más contemporáneo que les di a mis estudiantes fue utilizar las ideas en ese documento para empacar archivos de medios grandes en DVD de 4.7GB para hacer copias de seguridad. Usaron el algoritmo de "búsqueda de burbujas" de Michael Mitzenmacher para generar empaquetamientos alternativos, y usando este algoritmo y las técnicas de Hughes fue fácil obtener cada DVD (excepto el último) 99.9% lleno. Muy dulce.
Realmente no estoy respondiendo la pregunta, pero este es un enlace muy bueno para aquellos que quieren saber sobre la programación funcional en C #
http://blogs.msdn.com/b/ericwhite/archive/2006/10/04/fp-tutorial.aspx