Programmation fonctionnelle, fonctionnement

[OCaml]

a marqué ce sujet comme résolu.

Bonjour,

Depuis 2 jours j’apprend OCaml et je dois avouer que j’ai un peu de mal. Raison pour laquelle plusieurs questions me turlupinent.

De manière général dans un langage lorsqu’on déclare une variable : int a = 0; alors le programme va allouer une case mémoire de (32 ou 64 bits selon l’ordinateur). Ensuite dans le programme si on veut modifier cette variable il nous suffit de faire : a = 2; et alors l’ordinateur va tout simplement aller à l’adresse de la variable a soit &a en C++, et va modifier la valeur enregistré dans la case mémoire par la nouvelle valeur. Je ne sais pas si ce que je dis est correct, mais j’espère que c’est au moins l’idée.

A l’inverse en OCaml, les variables ne marchent pas de la même façon puisqu’en OCaml toutes les variables ne sont pas mutables, il faut pour qu’elle le soit rajouter le mot clé ref. J’aimerais en faite savoir pourquoi, puisqu’on peut afficher la valeur d’une variable non mutable dans un code OCaml, c’est que le programme a bien enregistré l’adresse mémoire de la variable, et normalement il peut a fortiori la modifiée ?

De plus que signifie le ; en OCaml, je sais qu’en C++ cela signfie la fin d’une instruction néanmoins en OCaml cela me semble plus obscure et je ne sais jamais vraiment ou il faut que j’en mette une…:honte:

Merci d’avance !

Salut,

Je vais essayer de t’éclaircir les idées. J’ai moi aussi eu un peu de mal à saisir les concepts quand j’ai commencé à apprendre OCaml après avoir appris le C.

Un programme dans un langage impératif est basiquement une suite d’instructions qui, décrit comment à partir d’un état de la mémoire n°1, l’ordinateur doit modifier la mémoire pour l’amener à un état n°2. Considère l’exemple suivant :

1
2
a = 5; // état mémoire n°1, a = 1
a = a*a; // état mémoire n°2, a = 25

Un programme dans un langage fonctionnel est une expression, qui est évaluée pour donner un résultat.

1
2
3
let square a = a*a in
let b = 5 in
 square b

Le programme consiste à évaluer l’expression "square b", dans laquelle b est un symbole définit comme valant l’entier 5, et square est un symbole définit comme la fonction qui prend en argument un entier et renvoie le carré de cet entier. "square b" est donc l’application de la fonction square au symbole b, ce qui correspond à l’expression "5*5", qui est égale à 25.

Il n’y a tout simplement pas de notion de mémoire. Il se trouve qu’OCaml connaît cette notion, mais c’est parce qu’OCaml est à la fois fonctionnel, impératif et objet.

Édit. : En OCaml, les expressions impératives (par exemple print_string "blabla") modifient la mémoire bien sûr, et renvoie la valeur "unit" quand elles sont évaluées. Tu peux séparer des expressions impératives par ";" (tu perds alors le résultat "unit"). Le double point-virgule ";;" termine l’expression.

+1 -0

Merci, c’est déjà plus clair maintenant.

J’ai encore deux trois choses qui ne sont pas encore pas très clair. Pour commencer, on est d’accord que :

1
let a = 0 in 

n’est pas une expression puisqu’on alloue de la mémoire pour la variable a ?

Mais pourquoi on ne peut pas faire :

1
2
let a = 0 in
a = 1

et qu’on est obligé d’utiliser le mot clé ref pour pouvoir modifier une variable ?

De plus à propos des ;, est-il utile d’avoir le résultat () car j’ai du mal à en comprendre l’utilité…

+0 -0

Pour préciser ce qu’a dit Aabu : bien sûr, quand ton programme OCaml où tu as écrit let a = 2 est exécuté, ton a est stocké en mémoire. Et bien sûr, ton ordinateur a la capacité technique de modifier le contenu de cette case mémoire, puisqu’il est câblé pour ça.

Mais même s’il est possible pour ton ordinateur de le faire, OCaml ne te fournit pas de moyen d’écrire cette modification (sauf si tu passes par exemple par une référence explicite).

L’intérêt, c’est que puisque tu n’as pas pu écrire de modification de cette variable, OCaml est sûr que ce a là désigne toujours la même valeur. C’est utile pour toi, parce que tu évites les comportements problématiques et difficiles à débuguer où une variable que tu utilises dans une fonction est modifiée quelque part dans un autre fichier sans que tu t’en soies rendu compte. C’est en général ce que les gens considèrent comme une des forces de la programmation fonctionnelle. C’est aussi utile pour ton compilateur, parce que le fait que cette variable ne soit pas modifiée peut lui permettre de faire tout un tas d’optimisations qui seraient impossibles dans le cas contraire.

Edit : puisque tu as répondu entre temps :

let a = 0 in n’est pas une expression tout seul. Par contre, let a = 0 in a + 2 est une expression de type int dont la valeur est 2. En OCaml, tu n’as pas à te préoccuper de l’allocation : ce n’est pas quelque chose que tu peux observer. On peut dire que le langage abstrait la gestion de la mémoire pour te permettre de te concentrer sur le reste. D’ailleurs, en l’occurrence, il est bien possible que le compilateur décide de ne pas allouer et de tout simplement remplacer cette expression par 2, puisqu’elle est constante.

Pour ta deuxième question, attention, tu peux écrire let a = 0 in a = 1, mais ça n’a pas le sens que tu sous-entends dans ton message : ici, le deuxième = est l’égalité des valeurs (le == d’autres langages, en quelque sorte). = a deux sens différents : s’il est combiné avec un let (ou d’autres constructions semblables), il sert à définir quelque chose, et s’il est utilisé comme un opérateur (ici, c’est le a = 1, on aurait pu aussi utiliser l’opérateur + et écrire let a = 0 in a + 1), c’est le test d’égalité. let a = 0 in a = 1 est donc une expression de type bool et de valeur false. Note qu’OCaml a aussi un opérateur == qui teste autre chose et que tu ne devrais pas utiliser pour l’instant (c’est d’ailleurs assez rare d’en avoir besoin).

Je crois avoir répondu plus haut à « pourquoi on ne peut pas écrire ça ». Note que tu peux écrire quelque chose comme :

1
2
3
let a = 0 in
let a = 1 in
a + 2

Cette expression vaut 3. On serait tenté de croire que le deuxième let modifie la valeur de a, mais ce n’est pas le cas : il définit une nouvelle variable a qui vaut 1. Comme elle a le même nom que la précédente, elle la masque simplement, elle ne modifie pas sa valeur. On peut le voir sur le code suivant :

1
2
3
let a = 0 in
  (let a = 1 in a + 2),
   a + 4

La construction a, b sert à construire un couple de valeurs. Ici, j’ai entouré le let a = 1 de parenthèses pour limiter sa portée : ainsi, le a + 2 vaudra 3 (puisque le a = 1 a masqué la définition du a = 0). Par contre, une fois sorti de ces parenthèses, le a + 4 vaut 4 : la valeur du a défini par le let a = 0 n’a pas été modifié, et son nom n’est plus masqué ici. Tout cette expression a donc la valeur (3, 4).

Pour le (), tu peux le voir comme une astuce qui permet de mélanger facilement expressions et instructions : en simplifiant, tu peux simplement voir les instructions comme des expressions de type unit (dans lequel il n’y a qu’une valeur, ()). Ça permet d’utiliser des instructions partout où on peut utiliser une expression. Je pense qu’il ne faut pas te prendre la tête dessus pour l’instant et simplement pratiquer un peu, et c’est quelque chose qui te paraîtra finalement plutôt naturel une fois que tu auras compris comment ça fonctionne.

+3 -0

J’aimerais ajouter un commentaire un peu plus général que ce que dit Eusèbe par rapport à cette phrase :

J’aimerais en faite savoir pourquoi, puisqu’on peut afficher la valeur d’une variable non mutable dans un code OCaml, c’est que le programme a bien enregistré l’adresse mémoire de la variable, et normalement il peut a fortiori la modifiée ?

Le but d’un langage n’est pas d’offrir au programmeur la même interface que le matériel (sinon on ferait tous du langage d’assemblage), mais au contraire d’offrir des abstractions pour exprimer des concepts qui n’existent pas au niveau matériel mais qui sont très utiles (comprendre indispensables) pour écrire des programmes complexes. En pratique, il n’y a rien au niveau matériel qui enforce l’immutabilité d’une variable, c’est pas pour autant que c’est une mauvaise idée d’avoir ce concept au niveau du langage.

+1 -0

Merci beaucoup Eusèbe, je pense avoir compris le truc. D’ailleurs en y repensant, on peut tout simplement dire que les variables en OCaml sont par défaut de type const, et que ref est en gros l’inverse de const.

Aussi je crois que ton message éclaire mieux l’idée de programmation fonctionnelle. En gros tout est une fonction qui return quelque chose. Je pense qu’on peut donc voir les types unit comme des procédures, soit des void en C++.

+0 -0

Merci beaucoup Eusèbe, je pense avoir compris le truc. D’ailleurs en y repensant, on peut tout simplement dire que les variables en OCaml sont par défaut de type const, et que ref est en gros l’inverse de const.

Mieux vaut voir ref comme une manière de définir des pointeurs. Par exemple, écrire !a est similaire à *a en C.

Aussi je crois que ton message éclaire mieux l’idée de programmation fonctionnelle. En gros tout est une fonction qui return quelque chose. Je pense qu’on peut donc voir les types unit comme des procédures, soit des void en C++.

Universite

Je pense qu’il ne faut pas trop essayer de faire de parallèles. Cela pourrait t’induire en erreur. Mais oui, tout expression évaluée retourne quelque chose, et si tant est qu’elle ne retourne rien, alors elle retourne la valeur spéciale "unit".

Merci beaucoup Eusèbe, je pense avoir compris le truc. D’ailleurs en y repensant, on peut tout simplement dire que les variables en OCaml sont par défaut de type const, et que ref est en gros l’inverse de const.

Si ça peut t’aider pour comprendre, pourquoi pas, mais attention à ne pas vouloir faire rentrer à tous les concepts OCaml dans les cases que tu connais des langages impératifs. Le fonctionnel t’offre une manière de penser différente, même s’il y a des choses qui ressemblent à ce que tu connais déjà. Par exemple, une différence fondamentale entre const et les « variables » OCaml, c’est que rien ne t’empêche d’écrire la modification d’un const (mais le compilateur va râler), alors que c’est tout simplement impossible de modifier une variable OCaml, parce que la syntaxe n’existe même pas. Ce n’est pas simplement « une variable un peu comme en C, mais constante par défaut », c’est quelque chose de différent.

Aussi je crois que ton message éclaire mieux l’idée de programmation fonctionnelle. En gros tout est une fonction qui return quelque chose.

Pareil, c’est une façon de voir les choses qui peut t’aider.

Je pense qu’on peut donc voir les types unit comme des procédures, soit des void en C++.

Attention, le type unit en lui-même, c’est un simple type au même titre que int par exemple. int est « habité » par des valeurs comme 1, 2 ou -41, alors que unit n’est habité que par une seule valeur, (). Par contre, tu peux effectivement voir dans un premier temps les fonctions qui renvoient () comme des « procédures », avec deux différences quand même :

  • On peut tout-à-fait écrire une fonction qui renvoie () mais n’a aucun effet. Ça n’a pas un grand intérêt, mais c’est possible, parce qu’unit est un type comme les autres.
  • On peut aussi écrire une fonction qui a un effet et qui renvoie une valeur d’un autre type que unit. Par exemple, une fonction qui afficherait un entier et renverrait le double de cet entier. Ce cas là est assez fréquent (un exemple concret : j’ai une fonction classique qui fait un gros calcul et je veux la débuguer, donc je rajoute un print au milieu pour afficher un des résultats intermédiaires du calcul).

Si je peux apporter mon grain de sel :

Globalement, dans un langage fonctionnel, tout est expression (le cœur du langage en tout cas). Une expression est une construction du langage qui produit une valeur. Typiquement, une bête valeur (43, "salut"…) est une expression, un appel de fonction est une expression (qui produit le résultat de l’appel de la fonction).

1
2
43 (* cette expression renverra 43 *)
sqrt 4 (* cette expression renverra le résultat de sqrt(4), soit 2 *)

Lorsque tu écris un let id = e1 in e2, tu définis un nom, un label id, dont la valeur est celle produite par e1 afin d’être utilisé dans e2. Pour faire un très gros raccourci, tu définis un synonyme à e1 uniquement utilisable dans e2.

1
2
3
4
5
6
7
let x = 5 in
  (* au sein de cette expression, on sait que x correspond à la valeur 5 *)
  let y = 6 in
    (* au sein de celle-ci, y correspond à 6, mais x correspond toujours à 6 
     * ainsi, en remplaçant x et y par leurs valeurs respectives, tu as 5 + 6
     * et le résultat produit par toute cette expresion est 11 *)
    x + y

Au sein d’un langage fonctionnel (OCaml comme Haskell ou même un Lisp), quand tu définis une variable, tu n’alloues pas de la mémoire (en fait si bien sûr, mais ce n’est pas ce que tu cherches à faire), tu donnes un nom à une valeur. Tu associes un nom dans l’environnement d’exécution à une valeur. Et comme OCaml ne te permets pas de modifier cette association, bah effectivement les variables sont immuables.

Histoire de te donner des nœuds au cerveau : seuls les champs de structures (des records) peuvent éventuellement être muables. Du coup quand tu fais une ref 5, tu instancies une valeur de type int ref qui est une structure contenant un champ muable que tu as initialisé à 5.

+1 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte