Il paradigma procedurale è quello che conosci dal C. Il codice sorgente è diviso in blocchi (chiamati procedure, funzioni o subroutine) che implementano una certa funzionalità. Le differenza tra procedura e funzione è praticamente inesistente, infatti la maggior parte dei linguaggi non fa alcuna distinzione sintattica tra le due cose. Le procedure sono semplicemente delle funzioni senza parametri. Il paradigma funzionale è una cosa completamente diversa, anche perché invece di essere imperativo (come il paradigma procedurale e quello object-oriented) è dichiarativo.
Nel paradigma funzionale il concetto di funzione deriva dalle funzioni in matematica: una funzione è la descrizione di un output, dato un certo input. Se una funzione chiamata con con in input "Pippo" deve restituire "Hello Pippo!", ogni volta che la chiami con in input "Pippo" otterrai "Hello Pippo!". Fin qui non sembra niente di strano, ma c'è una differenza fondamentale rispetto alle funzioni del C: come faccio a scrivere una funzione che prende in input "pippo.txt" e mi restituisce in output il contenuto del file pippo.txt? Abbiamo due problemi: 1) il contenuto del file dipende dal tuo computer; e 2) il file potrebbe non esistere. Apparentemente, non possiamo scrivere una funzione (ie., dato un input avere sempre lo stesso output) per leggere files, né per leggere l'input dell'utente, o per interagire con la rete. Non possiamo farlo perché l'output di queste funzioni dipende da agenti esterni. Questi problemi si risolvono usando delle astrazioni matematiche (eg., la monade) che ti permettono di modellare anche questi scenari; oppure, più semplicemente, introducendo delle impurità nel linguaggio.
Oltre a questa differenza, un altra differenza colossale rispetto al paradigma procedurale è che nel paradgma funzionale tutto è una costante: se hai una variabile x che vale 7, non puoi fare x++ per farla valere 8. Le variabili di fatto sono solo dei nomi legati (infatti si chiamano "binding" e non "variabili") a delle cose. Rinunciare alle variabili significa che non può esistere il ciclo for, perché non puoi creare una variabile che cambia di valore, e non esiste nemmeno il ciclo while, perché il predicato sarebbe o sempre vero (loop infinito) o sempre falso. Queste problematiche si risolvono con un uso massiccio delle funzioni ricorsive.
Se ne potrebbe parlare di più, ma queste sono le caratteristiche principali che mi sento di introdurti in questo post. (se hai qualche curiosità, chiedi pure).
La maggior parte dei linguaggi sono di fatto multiparadigma, perché ti permettono di seguire approcci diversi per risolvere un dato problema, ma di fatto i programmi scritti usando linguaggi puri tendono ad essere anche molto più chiari e puliti e molto meno bug-prone. Quelle che io ti ho elencato come limitazioni, in realtà da un certo punto di vista sono dei vantaggi. La programmazione funzionale è tornata in auge (è nata negli anni '50, ma fino al 2005 non se la filava quasi nessuno) anche perché quelle che io ti ho elencato come limitazioni rendono di fatto impossibile avere problemi di data-race. Se hai scritto un programma utilizzando un linguaggio di programmazione purely functional non avrai alcuna difficoltà a fargli sfruttare tutti i core della tua CPU, cosa che in C è molto difficile.