Partager via


Introduction aux concepts de programmation fonctionnelle en F#

La programmation fonctionnelle est un style de programmation qui met l’accent sur l’utilisation des fonctions et des données immuables. La programmation fonctionnelle typée est lorsque la programmation fonctionnelle est combinée à des types statiques, tels que F#. En général, les concepts suivants sont mis en évidence dans la programmation fonctionnelle :

  • Fonctions en tant que constructions principales que vous utilisez
  • Expressions au lieu d’instructions
  • Valeurs immuables sur des variables
  • Programmation déclarative sur la programmation impérative

Tout au long de cette série, vous allez explorer les concepts et les modèles de programmation fonctionnelle à l’aide de F#. Le long du chemin, vous allez apprendre un peu F# aussi.

Terminologie

La programmation fonctionnelle, comme d’autres paradigmes de programmation, est fournie avec un vocabulaire que vous aurez finalement besoin d’apprendre. Voici quelques termes courants que vous verrez tout le temps :

  • Fonction : une fonction est une construction qui produit une sortie lorsqu’une entrée est donnée. Plus formellement, il mappe un élément d’un ensemble à un autre ensemble. Ce formalisme est soulevé dans le concret de nombreuses façons, en particulier lors de l’utilisation de fonctions qui fonctionnent sur des collections de données. Il s’agit du concept le plus simple (et important) de la programmation fonctionnelle.
  • Expression : une expression est une construction dans le code qui produit une valeur. Dans F#, cette valeur doit être liée ou explicitement ignorée. Une expression peut être remplacée de manière triviale par un appel de fonction.
  • Purity - Purity est une propriété d’une fonction telle que sa valeur de retour est toujours la même pour les mêmes arguments, et que son évaluation n’a aucun effet secondaire. Une fonction pure dépend entièrement de ses arguments.
  • Transparence référentielle - La transparence référentielle est une propriété d’expressions qui peuvent être remplacées par leur sortie sans affecter le comportement d’un programme.
  • Immuabilité : l’immuabilité signifie qu’une valeur ne peut pas être modifiée sur place. Cela diffère des variables, qui peuvent changer en place.

Exemples

Les exemples suivants illustrent ces concepts fondamentaux.

Fonctions

La construction la plus courante et fondamentale dans la programmation fonctionnelle est la fonction. Voici une fonction simple qui ajoute 1 à un entier :

let addOne x = x + 1

Sa signature de type est la suivante :

val addOne: x:int -> int

La signature peut être lue comme suit : «addOne accepte un int nom x et produit un int». Plus formellement, addOnemapper une valeur de l’ensemble d’entiers à l’ensemble d’entiers. Le -> jeton signifie ce mappage. En F#, vous pouvez généralement examiner la signature de fonction pour obtenir un sens de ce qu’elle fait.

Pourquoi la signature est-elle importante ? Dans la programmation fonctionnelle typée, l’implémentation d’une fonction est souvent moins importante que la signature de type réelle ! Le fait d’ajouter addOne la valeur 1 à un entier est intéressant au moment de l’exécution, mais lorsque vous construisez un programme, le fait qu’il accepte et retourne un int est ce qui indique comment vous utiliserez réellement cette fonction. En outre, une fois que vous utilisez cette fonction correctement (par rapport à sa signature de type), le diagnostic des problèmes ne peut être effectué que dans le corps de la addOne fonction. C’est l’impulsion derrière la programmation fonctionnelle typée.

Expressions

Les expressions sont des constructions qui évaluent une valeur. Contrairement aux instructions, qui effectuent une action, les expressions peuvent être considérées comme effectuant une action qui donne une valeur. Les expressions sont presque toujours utilisées dans la programmation fonctionnelle au lieu d’instructions.

Considérez la fonction précédente. addOne Le corps d’une addOne expression est :

// 'x + 1' is an expression!
let addOne x = x + 1

Il s’agit du résultat de cette expression qui définit le type de résultat de la addOne fonction. Par exemple, l’expression qui compose cette fonction peut être modifiée pour être un type différent, tel qu’un string:

let addOne x = x.ToString() + "1"

La signature de la fonction est maintenant :

val addOne: x:'a -> string

Étant donné que n’importe quel type en F# peut l’appeler ToString() , le type de x ce type a été rendu générique (appelé Généralisation automatique) et le type résultant est un string.

Les expressions ne sont pas seulement les corps des fonctions. Vous pouvez avoir des expressions qui produisent une valeur que vous utilisez ailleurs. Une commune est la suivante if:

// Checks if 'x' is odd by using the mod operator
let isOdd x = x % 2 <> 0

let addOneIfOdd input =
    let result =
        if isOdd input then
            input + 1
        else
            input

    result

L’expression if produit une valeur appelée result. Notez que vous pouvez omettre result entièrement l’expression en faisant du if corps de la addOneIfOdd fonction. La principale chose à retenir sur les expressions est qu’elles produisent une valeur.

Il existe un type spécial, unitqui est utilisé lorsqu’il n’y a rien à retourner. Par exemple, considérez cette fonction simple :

let printString (str: string) =
    printfn $"String is: {str}"

La signature ressemble à ceci :

val printString: str:string -> unit

Le unit type indique qu’aucune valeur réelle n’est retournée. Cela est utile lorsque vous avez une routine qui doit « faire du travail » malgré l’absence de valeur à retourner à la suite de ce travail.

Cela contraste fortement avec la programmation impérative, où la construction équivalente if est une instruction et la production de valeurs est souvent effectuée avec des variables mutantes. Par exemple, en C#, le code peut être écrit comme suit :

bool IsOdd(int x) => x % 2 != 0;

int AddOneIfOdd(int input)
{
    var result = input;

    if (IsOdd(input))
    {
        result = input + 1;
    }

    return result;
}

Il est important de noter que C# et d’autres langages de style C prennent en charge l’expression ternaire, ce qui permet la programmation conditionnelle basée sur des expressions.

Dans la programmation fonctionnelle, il est rare de muter des valeurs avec des instructions. Bien que certains langages fonctionnels prennent en charge les instructions et la mutation, il n’est pas courant d’utiliser ces concepts dans la programmation fonctionnelle.

Fonctions pures

Comme mentionné précédemment, les fonctions pures sont des fonctions qui :

  • Évaluez toujours la même valeur pour la même entrée.
  • N’avez pas d’effets secondaires.

Il est utile de penser aux fonctions mathématiques dans ce contexte. En mathématiques, les fonctions dépendent uniquement de leurs arguments et n’ont aucun effet secondaire. Dans la fonction f(x) = x + 1mathématique, la valeur de f(x) dépend uniquement de la valeur de x. Les fonctions pures dans la programmation fonctionnelle sont de la même façon.

Lors de l’écriture d’une fonction pure, la fonction doit dépendre uniquement de ses arguments et n’effectuer aucune action qui entraîne un effet secondaire.

Voici un exemple de fonction non pure, car elle dépend de l’état global mutable :

let mutable value = 1

let addOneToValue x = x + value

La addOneToValue fonction est clairement impure, car value elle peut être modifiée à tout moment pour avoir une valeur différente de 1. Ce modèle selon une valeur globale doit être évité dans la programmation fonctionnelle.

Voici un autre exemple de fonction non pure, car elle effectue un effet secondaire :

let addOneToValue x =
    printfn $"x is %d{x}"
    x + 1

Bien que cette fonction ne dépende pas d’une valeur globale, elle écrit la valeur dans x la sortie du programme. Bien qu’il n’y ait rien de mal à faire cela, cela signifie que la fonction n’est pas pure. Si une autre partie de votre programme dépend d’un élément externe au programme, tel que la mémoire tampon de sortie, l’appel de cette fonction peut affecter cette autre partie de votre programme.

La suppression de l’instruction printfn rend la fonction pure :

let addOneToValue x = x + 1

Bien que cette fonction ne soit pas intrinsèquement meilleure que la version précédente avec l’instruction printfn , elle garantit que toutes ces fonctions retournent une valeur. L’appel de cette fonction n’importe quel nombre de fois produit le même résultat : il produit simplement une valeur. La prévisibilité donnée par la pureté est quelque chose que beaucoup de programmeurs fonctionnels s’efforcent.

Immuabilité

Enfin, l’un des concepts les plus fondamentaux de la programmation fonctionnelle typée est l’immuabilité. En F#, toutes les valeurs sont immuables par défaut. Cela signifie qu’ils ne peuvent pas être mutés sur place, sauf si vous les marquez explicitement comme mutables.

Dans la pratique, l’utilisation de valeurs immuables signifie que vous modifiez votre approche de la programmation de « J’ai besoin de changer quelque chose », à « J’ai besoin de produire une nouvelle valeur ».

Par exemple, l’ajout de 1 à une valeur signifie produire une nouvelle valeur, sans muter celle existante :

let value = 1
let secondValue = value + 1

Dans F#, le code suivant ne mute pas la fonction ; à la value place, il effectue un contrôle d’égalité :

let value = 1
value = value + 1 // Produces a 'bool' value!

Certains langages de programmation fonctionnels ne prennent pas en charge la mutation du tout. En F#, il est pris en charge, mais ce n’est pas le comportement par défaut pour les valeurs.

Ce concept s’étend encore plus loin aux structures de données. Dans la programmation fonctionnelle, les structures de données immuables telles que les jeux (et bien d’autres) ont une implémentation différente de celle attendue initialement. Conceptuellement, quelque chose comme l’ajout d’un élément à un jeu ne modifie pas l’ensemble, il produit un nouvel ensemble avec la valeur ajoutée. Sous les couvertures, cela est souvent effectué par une structure de données différente qui permet de suivre efficacement une valeur afin que la représentation appropriée des données puisse être donnée en conséquence.

Ce style d’utilisation des valeurs et des structures de données est essentiel, car il vous oblige à traiter toute opération qui modifie quelque chose comme s’il crée une nouvelle version de cette chose. Cela permet aux choses telles que l’égalité et la opacité d’être cohérentes dans vos programmes.

Étapes suivantes

La section suivante aborde minutieusement les fonctions, en explorant différentes façons de les utiliser dans la programmation fonctionnelle.

L’utilisation de fonctions dans F# explore les fonctions en profondeur, montrant comment les utiliser dans différents contextes.

Lectures complémentaires

La série Thinking Functionally est une autre ressource intéressante pour en savoir plus sur la programmation fonctionnelle avec F#. Il couvre les principes fondamentaux de la programmation fonctionnelle d’une manière pragmatique et facile à lire, à l’aide de fonctionnalités F# pour illustrer les concepts.