Quale compito è meglio fare in uno stile di programmazione funzionale?

Recentemente ho scoperto lo stile di programmazione funzionale e sono convinto che ridurrà gli sforzi di sviluppo, renderà il codice più facile da leggere, renderà il software più gestibile. Tuttavia, il problema è che ho aspirato a convincere chiunque.

Bene, di recente mi è stata data la possibilità di tenere un discorso su come ridurre lo sviluppo del software e gli sforzi di manutenzione, e ho voluto introdurre loro il concetto di programmazione funzionale e il modo in cui esso avvantaggia la squadra. Ho avuto l’idea di mostrare alla gente 2 set di codice che fa esattamente la stessa cosa, uno codificato in modo molto imperativo e l’altro in un modo molto funzionale, per dimostrare che la programmazione funzionale può rendere il codice più semplice, più facile da capire e quindi mantenibile. C’è un tale esempio, accanto alla famosa sum di piazze esempio di Luca Bolognese?

Recentemente ho scoperto lo stile di programmazione funzionale […] Bene, recentemente mi è stata data la possibilità di tenere un discorso su come ridurre gli sforzi di sviluppo del software, e ho voluto introdurre il concetto di programmazione funzionale.

Se hai appena scoperto la programmazione funzionale, non è consigliabile provare a parlare autorevolmente sull’argomento. So che per i primi 6 mesi mentre stavo imparando F #, tutto il mio codice era solo C # con una syntax un po ‘più scomoda. Tuttavia, dopo quel periodo di tempo, sono stato in grado di scrivere costantemente un buon codice in uno stile idiomatico e funzionale.

Vi raccomando di fare lo stesso: attendere circa 6 mesi fino a quando lo stile di programmazione funzionale non si presenta in modo più naturale, quindi date la vostra presentazione.

Sto cercando di illustrare i vantaggi della programmazione funzionale, e ho avuto l’idea di mostrare alla gente 2 set di codice che fa la stessa cosa, uno codificato in modo molto imperativo e l’altro in un modo molto funzionale, per mostrare che la programmazione funzionale può rendere il codice più semplice, più facile da capire e quindi mantenere. Esiste un tale esempio, accanto alla famosa sum di quadrati di esempio di Luca Bolognese?

Ho presentato una presentazione di F # al gruppo di utenti .NET nella mia zona e molte persone del mio gruppo sono rimaste colpite dalla corrispondenza del modello di F #. Nello specifico, ho mostrato come attraversare un albero di syntax astratto in C # e F #:

using System; namespace ConsoleApplication1 { public interface IExprVisitor { 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(IExprVisitor 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(IExprVisitor visitor) { return visitor.Visit(this); } } public class And : BinExpr { public And(Expr first, Expr second) : base(first, second) { } public override t Accept(IExprVisitor visitor) { return visitor.Visit(this); } } public class Nand : BinExpr { public Nand(Expr first, Expr second) : base(first, second) { } public override t Accept(IExprVisitor visitor) { return visitor.Visit(this); } } public class Or : BinExpr { public Or(Expr first, Expr second) : base(first, second) { } public override t Accept(IExprVisitor visitor) { return visitor.Visit(this); } } public class Xor : BinExpr { public Xor(Expr first, Expr second) : base(first, second) { } public override t Accept(IExprVisitor visitor) { return visitor.Visit(this); } } public class Not : UnaryOp { public Not(Expr first) : base(first) { } public override t Accept(IExprVisitor visitor) { return visitor.Visit(this); } } public class EvalVisitor : IExprVisitor { 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 { 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); } } } 

Il codice sopra è scritto in uno stile C # idiomatico. Utilizza lo schema del visitatore piuttosto che il test del tipo per garantire la sicurezza del tipo. Questo è circa 218 LOC.

Ecco la versione 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 (^^) pq = 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 

Questo è 65 LOC. Poiché utilizza la corrispondenza del modello piuttosto che il modello di visitatore, non perdiamo alcun tipo di sicurezza e il codice è molto facile da leggere.

Qualsiasi tipo di elaborazione simbolica è più facile da scrivere di ordine di grandezza in F # rispetto a C #.

[Modifica per aggiungere:] Oh, e la corrispondenza del modello non è solo una sostituzione per il modello di visitatore, ma ti consente anche di confrontare la forma dei dati. Ad esempio, ecco una funzione che converte Nand ai loro equivalenti:

 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) 

Non è ansible scrivere questo codice in modo conciso a tutti in C #.

Ci sono molti esempi là fuori, ma nessuno avrà un impatto completo quanto l’utilizzo di un campione relativo a uno dei tuoi progetti al lavoro. Esempi come “Sum Of Squares” di Luca sono fantastici ma se qualcuno lo usasse come prova su come il nostro codice potrebbe essere scritto meglio non ne sarei convinto. Tutto l’esempio dimostra che alcune cose sono meglio scritte funzionalmente. Quello che devi dimostrare è che il tuo codice base è scritto meglio dal punto di vista funzionale

Il mio consiglio sarebbe quello di scegliere alcuni punti problematici comuni e alcuni punti chiave nella base di codice, e riscriverli in uno stile funzionale. Se è ansible dimostrare una soluzione sostanzialmente migliore, sarà molto utile conquistare i colleghi.

Compiti per lo stile funzionale? Ogni volta che hai uno schema di codifica comune e vuoi ridurlo. Qualche tempo fa ho scritto un po ‘sull’uso di C # per lo stile funzionale, assicurandomi che fosse pratico: Practical Functional C # (sono esitante a collegarmi alle mie cose qui, ma penso che sia rilevante in questo caso). Se si dispone di un’applicazione “impresa” comune, mostrare, ad esempio, come le espressioni sono belle nell’abbinamento di modelli non sarà troppo convincente.

Ma nelle app del mondo reale ci sono tonnellate di modelli che appaiono a un livello di codifica basso. Usando le funzioni di ordine superiore, puoi farle andare via. Come mostro in quella serie di post sul blog, il mio esempio preferito è il pattern “try-close / finally-abort” di WCF. Il pattern “try / finally-dispose” è così comune che è diventato una parola chiave della lingua: using. Stessa cosa per “lock”. Questi sono entrambi banalmente rappresentati come funzioni di ordine superiore e, solo perché C # non li supportava in origine, abbiamo bisogno di parole chiave in linguaggio hard-coded per supportarli. (Veloce: cambia i blocchi di “blocco” per utilizzare un blocco ReaderWriter. Oops, per prima cosa dovremo scrivere una funzione di ordine superiore.)

Ma forse convincere richiede semplicemente guardare a Microsoft. Generico alias polimorfismo parametrico? Non è certo OO, ma un bel concetto funzionale che, ora, tutti amano. Il simpatico framework Ninject non funzionerebbe senza di esso. Lambda? Come alberi di espressione, sono come LINQ, Fluent NHibernate, ecc. Ottengono tutto il loro potere. Di nuovo, questo non viene da OO o programmazione imperativa. La nuova libreria di threading? Abbastanza brutto senza chiusure.

Quindi, la programmazione funzionale ha benedetto cose come .NET nell’ultimo decennio. I principali progressi (come i generici, “LINQ”) sono direttamente dai linguaggi funzionali. Perché non rendersi conto che c’è qualcosa in esso e farsi coinvolgere di più? Ecco come lo esprimerei agli scettici.

Il problema più grande è in realtà fare in modo che le persone facciano il salto nella comprensione delle funzioni di ordine superiore. Mentre è abbastanza facile, se non l’hai mai visto prima nella tua vita, potrebbe essere scioccante e incomprensibile. (Diamine, sembra che molte persone credano che i farmaci generici siano solo per le raccolte sicure dal punto di vista del tipo e LINQ sia solo SQL incorporato).

Quindi, quello che dovresti fare è passare attraverso il tuo codice base e trovare luoghi che sono un incubo imperativo troppo complicato. Cerca gli schemi sottostanti e usa le funzioni per metterli insieme. Se non riesci a trovarne uno, potresti accontentarti solo della demo delle liste. Ad esempio “trova tutti i Foos in questo elenco e rimuovili”. È una cosa a 1 riga in stile funzionale “myList.Remove (x => x.Bla> 0)” contro 7 righe in stile C # (crea un elenco temporaneo, passa da un anello all’altro e aggiungi elementi da rimuovere, loop e rimuovi gli elementi ).

La speranza è che, anche se la syntax è dispari, la gente riconoscerà “wow, è molto più semplice”. Se riescono a mettere giù il “verbose == più leggibile” e “che sembra confuso” per un po ‘, avrai una possibilità.

In bocca al lupo.

Essenzialmente, il paradigma funzionale è molto efficace per l’elaborazione parallela:

“La cosa veramente interessante che voglio che tu noti, qui, è che non appena pensi alla mappa e riduci come funzioni che tutti possono usare, e le usano, devi solo ottenere un supergenius per scrivere il codice duro da eseguire mappa e riduci su una serie globale di computer globale e parallela, e tutto il vecchio codice che funzionava bene quando hai appena eseguito un ciclo funziona ancora solo per un miliardo di volte più veloce, il che significa che può essere utilizzato per affrontare enormi problemi in un istante.

Fammi ripetere. Estrapolando il concetto stesso di loop, puoi implementare il loop come preferisci, incluso implementarlo in un modo che si adatta bene all’hardware aggiuntivo. ”

http://www.joelonsoftware.com/items/2006/08/01.html

Il miglior articolo di advocacy mai scritto per lo stile funzionale è un articolo di John Hughes intitolato Why Functional Programming Matters . Vi suggerisco di fare alcuni esempi per voi fino a raggiungere il punto in cui potete convincere in modo convincente gli argomenti esposti in quel documento.

Molti degli esempi nel documento sono numerici e non entrano in risonanza con il pubblico di oggi. Un altro esercizio contemporaneo che ho dato ai miei studenti è stato quello di utilizzare le idee contenute in quel documento per imballare file multimediali di grandi dimensioni su DVD da 4,7 GB per il backup. Hanno usato l’algoritmo di “Bubble Search” di Michael Mitzenmacher per generare pacchetti alternativi, e utilizzando questo algoritmo e le tecniche di Hughes è stato facile ottenere ogni DVD (tranne l’ultimo) pieno al 99,9%. Molto dolce.

Un altro esempio potrebbe essere l’ algoritmo Quicksort . Può essere descritto molto brevemente in un linguaggio funzionale come Haskell:

 qsort [] = [] qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++ qsort (filter (>= x) xs) 

ma ha bisogno di un po ‘più di codifica in un linguaggio iterativo. Sul sito web di riferimento puoi trovare anche molti altri esempi con confronti tra lingue.

Per ottenere ciò che desideri e comunicarlo agli altri nella tua organizzazione, devi dimostrare che il business della tua azienda è costruito in un modo migliore.

È inutile utilizzare un paio di algoritmi per dimostrare il potere della programmazione funzionale se è totalmente inutile per il tuo dominio aziendale. Quindi, prendi un codice esistente e riscrivilo funzionalmente. Se riesci a dimostrarlo, che è meglio, la gente ti ascolterà – hai mostrato loro un esempio concreto e pertinente. Se non puoi, allora forse la programmazione funzionale non è la soluzione che stavi cercando.

Se per “stile funzionale” intendi l’uso di concetti come “mappa”, “applica”, “riduci”, “filtro”, funzioni lambda e list comprehensions, allora dovrebbe essere evidente che il codice che deve occuparsi delle operazioni su le liste sono quasi sempre più concise se scritte nello “stile funzionale”. Ma se stai mescolando lo “stile funzionale” con il codice imperativo in un linguaggio prevalentemente imperativo, è solo una questione di stile.

Ad esempio, in python è ansible reimplementare il crack di qsort di Haskell pubblicato in questo modo:

 def qsort(list): if list == []: return [] else: x = list[0]; xs = list[1:] return qsort(filter(lambda y: y= x, xs)) 

Anche se scrivendo l’ultima riga come

 return qsort([y for y in xs if y=x]) 

è discutibilmente più “pythonic”.

Ma questo è ovviamente più conciso dell’attuazione qui:

http://hetland.org/coding/python/quicksort.html

Il che, per inciso, è il modo in cui avrei pensato di implementarlo prima che imparassi Haskell.

La versione funzionale è estremamente chiara se e solo se si è abituati alla programmazione funzionale e si fa un grok che cosa farà il filter con la stessa facilità con cui il programmatore C ++ logoro gira un ciclo for senza nemmeno pensarci troppo. E questo è chiaramente il vero problema in gioco qui: la programmazione “stile” funzionale è una mentalità completamente diversa . Se le persone con cui lavori non sono abituate a pensare in modo ricorsivo, e non sono il tipo di cui essere entusiasti, non solo una nuova tecnologia, ma un altro modo di pensare a risolvere i loro problemi, allora qualsiasi quantità di confronti tra codice non è li vincerò.

Un buon esempio è la creazione del tuo linguaggio di programmazione usando quello esistente, dove dovrai usare Monade .

Con F # è molto più semplice scrivere la logica di analisi piuttosto che con C #.

Dai un’occhiata a questo articolo: Functional .NET – LINQ o Monade integrate in linguaggio?

Gli algoritmi che coinvolgono la ricerca di backtracking e la semplificazione del supporto di annullamento nelle GUI sono due punti in cui ho utilizzato lo stile funzionale nella pratica.

Un semplice esempio di un’attività che è spesso più semplice eseguita in uno stile funzionale è la trasformazione dei dati da una forma all’altra. “La sum dei quadrati” è un esempio banale di trasformazione dei dati. Il discorso di Luca al PDC dello scorso anno ha mostrato come utilizzare questo tipo di trasformazione dei dati per qualcosa di più pratico, scaricando e analizzando le quotazioni azionarie. La demo è fatta in F #, ma i concetti sono gli stessi e possono essere applicati a C # o alla maggior parte degli altri linguaggi di programmazione.

http://channel9.msdn.com/pdc2008/TL11/

Mostra loro il modo di iterare di jQuery sugli elementi 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(); 

rispetto a come la maggior parte dei risultati di google per “javascript element by classname” fallo:

 var arrayOfElements = // this is filled with the elements somehow for(var i=0,j=arrayOfElements.length; i 

La programmazione funzionale è utile nelle cose di tutti i giorni come sopra!

(nota: non so se il mio esempio si adatta alla definizione esatta di programmazione funzionale, ma se lo fa, la programmazione funzionale è fantastica)

Recentemente ho escogitato un piccolo trucco per creare lambda e passare ai miei metodi di estensione più F # ish. Eccolo:

Quello che volevo fare era qualcosa di simile:

 3.Times(() => Console.WriteLine("Doin' it")); 

Ora il metodo di estensione per questo è facilmente implementabile:

  public static void Times(this int times, Action action) { Enumerable.Range(1, times).ToList().ForEach(index => action()); } 

Quello che non mi piace è che sto specificando l’indice qui: ForEach(index => action()) anche se non è mai usato, quindi l’ho sostituito con un _ e ottenuto ForEach(_ => action())

È carino, ma ora ero motivato ad avere il mio codice di chiamata simile

(Non mi è mai piaciuto il “()” all’inizio dell’espressione lambda), quindi invece di: 3.Times(() => ...); Volevo 3.Times(_ => ...); L’unico modo per implementare questo è stato passare un parametro falso al metodo di estensione, che non viene mai utilizzato e quindi l’ho modificato in questo modo:

  public static void Times(this int times, Action action) { Enumerable.Range(1, times).ToList().ForEach(_ => action(byte.MinValue)); } 

Questo mi permette di chiamarlo così:

 3.Times(_ => Console.WriteLine("Doin' it")); 

Non molto di un cambiamento, ma mi è comunque piaciuto, che è stato ansible apportare quel piccolo tweak così facilmente e allo stesso tempo rimuovere il rumore “()” lo rende molto più leggibile.

Non rispondendo veramente alla domanda, ma questo è un ottimo link per coloro che vogliono conoscere la programmazione funzionale in C #

http://blogs.msdn.com/b/ericwhite/archive/2006/10/04/fp-tutorial.aspx

  1. Mostra come codificare un distinto di un array. Distinct è molto semplice in SQL ma è stato difficile su un array di memoria. Ora è facile distinguere un array con LINQ.

  2. Puoi spiegare loro che ci sarà il LINQ Parralel (PLINQ) in futuro. Quando inizi a scrivere codice funzionale, sarà più facile eseguire il parralelize della tua applicazione. Google utilizza ampiamente MapReduce.

  3. Spiega loro che LINQ è un linguaggio di query per manipolare diversi tipi di dati. In memoria, in un database, Excel, servizi Web, file xml, file JSON. È una sorta di SQL universale. Tuttavia le persone a cui non piace SQL saranno meno convinti.

Non parlerei molto della programmazione funzionale, vorrei spiegare in che modo LINQ può aiutare gli sviluppatori.

È interessante che nessuno abbia davvero risposto alla domanda: quale compito è meglio fare in uno “stile funzionale”?

Un programma / algoritmo consiste di 2 parti: controllo logico e struttura dati. Penso che i compiti meglio eseguiti in uno stile funzionale siano quelli che riguardano liste o matrici nei casi in cui si comportano come una lista (es. Qsort). Non è un caso che i linguaggi di programmazione funzionale abbiano un ottimo supporto per le liste.

Quando le strutture dati si discostano dalle liste, penso che l’uso di uno stile di programmazione funzionale diventi un po ‘”innaturale”.