Les bases de la programmations

Hello World! Ici Switch. Aujourd'hui, je vais vous initier a la programmation en me basant sur quelque langages de programmation. Je ferais de mon mieux pour vous expliquer comment écrire de simple programme et transcrire du code d'un langage a un autre.

Dans cet article, j'utiliserai: C, OCaml, Golang, Python et un pseudo-code personnalisé pour résoudre quelques problèmes basique.

Bases de l'organisation de code

Lorsque l'on apprend a coder, il est fréquent de regrouper tous le code au même endroit, résolvant le problème maladroitement. Par exemple, il est fréquent de commencer par écrire :

afficher("Hello World!")
afficher("Hello Switch!")
afficher("*")
afficher("**")
afficher("***")
afficher("****")

Lorsqu'il est demandé d'afficher :
Hello World!
Hello Switch!
*
**
***
****

Et cela fonctionne.

Bien que cet example soit exagéré, il représente assez bien ce que les débutants auront du mal a équilibré: la séparation du code.

Les langages de programmation proposent différentes solutions pour nommer des valeurs ou définir des actions répétées. Nous pouvons simplifier l'example précédent en définissant une fonction pour dire bonjour

fonction Bonjour(à):
  message = "Hello" + à + "!"
  afficher(message)

Afin d'éviter la répétition de l'affichage Hello+nom+!.

Ils apportent également des solutions pour les tâches répétées. La majorité des langages actuel intègrent les boulces for et while ou acceptent les fonctions récursives (une fonction s'appelant elle même). Ainsi, nous pouvons afficher le triange d'* grâce a la boucle

stars = ""
pour i allant de 0 à 4 faire:
    stars = stars + "*"
    afficher(stars)

L'une des première difficulté est de savoir si un ensemble de ligne de code doit être isolé dans une fonction dédié ou si un process est suffisamment répétitif pour être intégrer dans une boucle d'itération.

Il existe de nombreuses architecture pour les projets informatique, chacune ayant ses forces et faiblesses. Dans ce blog, j'utiliserai principalement une séparation modulaire simple (chaque fonctionnalité a son propre module sans règles réelle) et la Clean Architecture pour les bases de code plus complexes (séparation claire entre les objectifs du code, la gestion des données, l'interface avec l'utilisateur et les éléments fonctionnels).

Passons à la résolution de problèmes.

Inversion de variables (C - OCaml)

Commençons par écrire un algorithme permettant d'inverser deux variables. La solution la plus simple utilise une variable complémentaire comme tampon pour conservé la valeur initiale d'une des deux variables, ce qui peut s'exprimer ainsi :

a = 125
b = "trust"
c = b
b = a
a = c

Maintenant, supposons que notre system n'a plus la place nécessaire a l'attribution d'une tierce variable, et proposons un algorithme ne reposant pas sur un tampon.

Pour simplifier le problème, supposons que nos variables soit nécessairement des nombres. Si j'ai une variable A ayant pour valeur 5 et une variable B équivalent à 2, comment puis-je avoir A = 2 et B = 5 sans variable complémentaire. Une solution peut être dérivée des opérations conservatives entre deux valeurs. En les utilisant, je peux combiner les deux valeurs en une et récupérer les valeurs non combinées depuis la combinaison obtenue. Disons que nous sommions A et B dans B. B prend alors la valeur 7 tandis que A contient toujours 2. Lorsque nous retirons A de B dans A, nous obtenons A = B - A = 7 - 2 = 5 qui est la valeurs initiale de B. Et si nous retirons de nouveau A de B, nous récupérerons la valeur initiale de A car A est la valeurs initiale de B et B la somme des valeurs initiales. Cette transformation résulte des propriétés de transitivité et de symétrie de l'addition et de la soustraction. A = A + B - B et B = A + B - A, aussi, en faisant notre suite d'opération, nous réalisons l'inversion en attribuant à A la valeur de B A + B - A et inversement pour B.

a = 7
b = 2
b = a + b
a = b - a
b = b - a

Maintenant, codons.
Notre première implémentation sera en C. C est un des premiers langages haut niveau. Créé en 1972, il reste une référence pour l'informatique embarquée (tableau de bord des voitures par example) et est la base de nombreux langages.

Tout programme implémenté en C doit fournir une fonction main renvoyant un type int. Cette fonction peut ne pas prendre d'arguments (void) ou avoir un coupe de paramètres (int argc, char *argv[]). main représente la fonctionnalité et en est le seul et unique point d'entré. L'entier retourné par la fonction permet de gérer les erreurs. Si 0 est retourné, il n'y a pas eu d'erreur, sinon une erreur a eu lieu.

#include LIB; // include a library to use its functions

TYPE NAME(T1 P1, T2 P2, ...) {} // declare a function named NAME returning type TYPE having parameters P1 of type T1 AND P2 of type T2

TYPE NAME; // declare a variable NAME of type TYPE

return SMTG; // stop function and make it returns SMTG

funcName(P1,P2,...); // Make a function call to funcName with parameters

Tout programme C doit fournir une fonction main renvoyant une valeur de type int. Cette fonction peut soit ne pas prendre d'argument (type void) soit prendre un couple d'argument (int argc, char *argv[]). Elle représente la fonctionnalité et en est le seul point d'entrée. La valeur entière renvoyée par main indique si des erreurs ont eu lieu lors de l'exécution. 0 signifie qu'il n'y a pas eu d'erreur, toute autre valeur permet de documenter les erreurs.

Définissons la fonctionnalité d'inversion swap comme bloc unique dans la fonction main. Nous devons déclarer deux variables numériques ou deux chaines de caractères (ici: défini signifie qu'elles ont un nom et une valeur) puis réaliser l'inversion.

int main(void) {
  int a, b; // declare two integer variables

  a = 2;
  b = 5;

  printf("First value is: %d and second value %d\n", a, b); // show before swap
  b = a + b; // get sum of A and B
  a = b - a; // A + B - A = B -> A is now B
  b = b - a; // A + B - B = A -> as A is B, and B is A+B, B is now A
  printf("First value is now: %d and second value %d\n", a, b); // swap done

  return 0; // return 0 cause all went well :)
}

Nous avons désormais une première version du code de swap. Isolons le code fonctionnel réalisant l'inversion :

void swap(int a, int b) {
  b = a + b; // get sum of A and B
  a = b - a; // A + B - A = B -> A is now B
  b = b - a; // A + B - B = A -> as A is B, and B is A+B, B is now A
  printf("First value is now: %d and second value %d", a, b); // swap done
}

Nous définissons une méthode swap ayant deux arguments entiers qui inverse les valeurs fournies et affiche le résultat à l'utilisateur.

Proposons également à l'utilisateur du programme de saisir ses propres valeurs. Nous avons deux options pour ce faire. Soit nous laissons l'utilisateur entrer les valeurs pendant l'exécution du programme, soit nous lui demandons de les fournir au lancement de ce dernier. La première solution se base sur la libraire standard (proposée nativement par le langage mais non incluse par défaut) stdio qui fournie des méthodes de lecture et écriture vers les entrées standard. La seconde utilise les arguments spécifiques à la function main (couple argc, argv).

Le code ci-dessous s'attend à ce que l'utilisateur fournisse deux arguments au programme. Si c'est le cas, il applique la méthode swap a ces arguments.

int main(int argc, char *argv[]) {
  int a, b;

  if (argc != 2) { // check that we received 2 arguments
    printf("Swap expect 2 arguments, %d received!", argc)
    return 1 // return 1 cause we received an unexpected number of arguments witch is an error
  }

  a = atoi(argv[0])
  b = atoi(argv[1])

Afin de laisser l'utilisateur saisir les valeurs qu'il désire au vol, nous pouvons proposer une méthode qui lui demandera d'entrer un entier et renverra la valeur saisie au programme.

int getInt(void) {
  int a;
  printf("Enter an integer:");
  scanf("%d", &a); // scan user input and store it in a variable. The & is required      here to let scan method modify a values (pointer concept)
  return a;
}

Supposons à présent que je désire fournir cette même fonction dans un autre langage. Puis-je conserver de bloc, que dois-je adapter ou supprimer afin de conserver un code fonctionnel.

Tout d'abord, l'algorithme utilisé peut toujours être conservé. Son implémentation sera à revoir, mais la méthode en elle même reste valable quelque soit le langage. Ensuite, je dois déterminer les éléments fonctionnels externe que le langage me fournit comme les conversions de valeurs ou la récupérations d'entrées utilisateurs. La majorité des langages de programmations intègre des solutions pour les tâches basiques mais certains n'en proposent qu'une partie ou aucune. Je peux alors réécrire mon code pour le nouveau langage choisit. Ré-implémentons notre inversion en OCaml.

OCaml est un langage dérivé de C mais ça syntax est assez différente. C'est également un langage fortement typé mais il utilise un typage dynamique plutôt que statique (il est possible de forcer le type des fonctions via des fichiers spécifique mais si ce n'est pas fait, OCaml déterminera lui-même le type le plus adapté).

Traduisons notre code C en OCaml. Si C requiert une fonction main, ce n'est pas le cas en OCaml. Les fichiers de code sont exécuté tel quel. Nous pouvons donc simplement créer notre fonction et son appel dans un fichier.

let swap a b = 
  let b = a + b in 
  let a = b - a in 
  let b = b - a in 
  (a, b);;

swap 2 5;;

Notre code en OCaml fait exactement la même chose qu'en C, si ce n'est que nous retournons le couple inversé plutôt que de l'afficher. L'utilisation de let ... in permet de redéfinir localement les variables a et b avant de renvoyer le couple inversé (a, b). L'appel swap 2 5 nous fournit le couple 5, 2 en réponse.

Vous trouverez d'autres adaptations du même programme pour d'autres langages directement sur github: https://github.com/switchelven/programming-bases. Je ne les détaillerais pas car sur un code aussi simple, elle sont équivalente a la transition de C vers OCaml. Passons plutôt au problème suivant.

Min and max (Go)

Intéressons nous maintenant a une fonction permettant de récupérer le minimum et le maximum d'une liste. Par example, en l'appliquant à la liste 2,4,1,3,5,7,9, je devrais obtenir 1 pour le minimum et 9 pour le maximum. Encore une fois, il existe de nombreuses solutions pour résoudre le problème. La plus simple compare chaque élément de la liste 1 a 1 au maximum et minimum actuels connus. Une autre trierai les données dans un ordre connue pour récupérer les éléments directement. Il y a également plusieurs façons de comparer les valeurs.

Commençons par la partie comparaison. La majorité des langages fournissent des opérateurs de comparaison directs pour leurs types basiques. Mais voyons comment s'en passer pour le plaisir de l'exercice. Pour être plus précis, voyons comment calculer directement le minimum et le maximum entre deux entiers. En se basant sur l'égalité A+B+A-B=2A et les valeurs absolues, nous devrions pouvoir retrouver notre comparaison. Nous savons que |A-B|=A-B lorsque A > B et que |A-B|=B-A si B > A. Ainsi, nous pouvons en déduire que A+B+|A-B| donnera 2A si A > B et 2B si B > A. Nous pouvons donc proposer l'expression suivante pour calculer le minimum et le maximum:

fonction max(a, b) {
  return (a+b+|a-b|)/2
}

fonction min(a, b) {
  return (a+b-|a-b|)/2
}

Implémentons ces fonctions. Cette fois, je vous propose le code en Go, à vous de le transcrire.


func Min(a int, b int) uint {
   return uint(a+b-abs(a-b)) / 2
}

func Max(a int, b int) uint {
   return uint(a+b+abs(a-b)) / 2
}

func main() {
   fmt.Println("Min of", 135, 209, "is", Min(135, 209))
   fmt.Println("Max of", 178, 179, "is", Max(178, 179))
}

Vous remarquerez que les méthodes Min et Max retournent des entiers non typés (uint) plutôt que typés. J'ai fait ce choix pour indiquer que l'algorithme ne fonctionne pas correctement avec des nombres négatifs. Utiliser des entiers non typés indique que les méthodes ne renverrons jamais d'entier négatif et donc ne les gèrent pas correctement. Si vous testez les méthodes avec un retour d'entier typé, vous constaterez que la méthode fonctionne comme attendu pour des entiers positifs ou de type différent. Mais entre deux entiers négatifs, les réponses entre Min et Max seront inversées car la différence utilisée pour trouver l'ordre est une valeur absolue.

Appliquons maintenant ces méthodes a une liste de valeurs. Je présentais plus haut deux idées pour trouver les valeurs extrêmes d'une liste. Comparer les éléments un a un est une meilleur idée que de trier la liste si la liste en question est aléatoirement triée. Si la liste est assez bien triée, finir de la trier sera beaucoup plus rapide. Comme nous ne pouvons pas déterminer à quel point une liste est triée à l'exécution (sans parcourir l'intégralité de la liste), il nous faut choisir l'algorithme en fonction de préconditions qui peuvent être assurer si la données est interne ou en se basant sur une structure de donnée spécifique. Nous pourrions par example définir une liste qui serait toujours triée en forçant l'ajout d'un nouvel élément a sa place triée.

En supposant que la liste soit aléatoirement triée, l'implémentation la plus simple pour récupérer la plus grande et la plus petite valeur dans une liste d'entier en Go est

func MinMaxList(intList []int) (min uint, max uint) {
    // Set default value if slice is empty
    min = 0
    max = 0

    // Search for min and max in slice
    for i, x := range intList {
        // Set max and min on list fist element. 
        if i == 0 { // i is the index of element. Go starts indexing slices at 0
            max = x
            min = x
            continue
        }

        // Check if current min/max are still min or max comparing to current value
        max = Max(int(max), x)
        min = Min(int(min), x)
    }

    return // return computed min, max. The empty return in go will automatically returns named return parameters
}

Puissance (Python, OCaml)

Parlons puissance. La notion mathématique de puissance permet d'écrire simplement la multiplication répétée d'un nombre (puissance(2,3)=2*2*2=8). Nous connaissons déjà une méthode pour répéter un nombre connu de fois une opération en se basant sur les boucles for. Nous pouvons l'exprimer ainsi en pseudo-code:

function pow(a, b) {
  res = 1 // pow(number, 0) = 1
  for _ from 0 to b do:
    res = a * res
  return res
}

Une autre solution se base sur la notion de récursion. Les fonctions récursives définissent des cas simple dont la résolution est connue (généralement appelé cas simple ou cas de sortie). Pour la fonction puissance, nous retenons généralement un cas simple unique: puissance(n, 0) = 1. Il reste alors a trouver des méthodes dites de simplifications qui permettent d'aller de n'importe quel cas vers les cas de sorties. Dans notre programme, nous utiliserons la construction naturelle de la puissance : puissance(a, n) = a * power(a, n-1). Notre fonction récursive peut donc se décrire ainsi :

function pow(a, n) {
  if n == 0 {
    return 1
  }
  return a * pow(a, n-1)
}

Les fonctions récursives sont un outils puissant mais il faut être vigilant vis à vis de sa complexité spatiale (l'espace mémoire requis pour l'exécuter). Si nous décomposons l'exécution de la fonction power(10,4), nous obtenons le processus suivant :

appeler puissance(10, 4):
puissance(10,4) = 10 * puissance(10,3)
  appeler puissance(10, 3):
  puissance(10,3) = 10 * puissance(10,2)
    appeler puissance(10,2):
    puissance(10,2) = 1O * puissance(10,1)
      appeler puissance(10,1):
      puissance(10,1) = 10 * puissance(10,0)
        appeler puissance(10,0):
        puissance(10,0) = 1
        renvoyer 1
     puissance(10,1)=10 * puissance(10, 0) = 10 * 1 = 10
     renvoyer 10
   puissance(10,2) = 10*puissance(10,1) = 10 * 10 = 100
   renvoyer 100
  puissance(10,3) = 10 * puissance(10,2) = 10 * 100 = 1000
  renvoyer 1000
puissance(10, 4) = 10 * puissance(10,3) = 10 * 1000 = 10000
renvoyer 10000

Nous pouvons constater que chaque appel récursif garde en mémoire une opération a exécuter en attendant que l'état suivant soit calculé. Lorsque l'état suivant a fini sont cycle de vie et a renvoyé sa valeur, l'opération mise de côté sera exécutée avec la valeur obtenue. Une erreur basique lorsque l'on travaille sur des fonctions récursives est de définir des cas de simplification incorrect. Ces étapes ont pour objectif d'exécuter une opération incomplète afin de se rapprocher des cas simples. Si les simplifications n'aident pas a se rapprocher des cas de sorties, ils ne seront jamais atteint et la méthode tournera dans le vide jusqu'à remplir la mémoire conduisant à une erreur. Une autre erreur fréquente consiste a mal construire les résultats. La fonction atteint bien ses cas simples, mais le retour est vide ou incorrecte car la simplification n'associe pas correctement le résultat. Maîtrisées, les fonctions récursives permettent d'exprimer clairement des algorithmes complexes. Nous pouvons désormais implémenter les deux méthodes. Je vous les présente en OCaml pour la méthode récursive et en Python pour la boucle.

def pow(a, n):
    res = 1
    for _ in range(0, n):
        res = res * a
    return res

if __name__ == "__main__":
    pow(3,10)
let rec pow a n = 
  match n with
  | 0 -> 1
  | _ -> a * pow a (n-1);;

pow 3 10;;

Définition du Pseudo Langage

Le pseudo-langage utilisé dans cet article se définit ainsi:

X = Value -> stock Value dans la variable X la rendant accessible via X directement dans le code (ex: etoile = "*" permet de récupérer le caractère * sous le nom etoile)

afficher(QQCHOSE) -> affiche QQCHOSE a l'utilisatieur

+ -> correspond a l'addition sur des nombres, a la concaténation des objects sinon (ex: 1+4=5, "to"+"to"="toto"

Définition du langage OCaml

OCaml a été introduit en 1996 par l'institut français INRIA. Il est pensé pour aider à démontrer des théorèmes mathématiques et à une syntaxe singulière dans le monde de la programmation.

open LIB;; (* include a library to use its functions *)

let NAME  P1 P2 = (* declare a function named NAME having parameters P1 and P2 *)
let NAME = VAL;; (* declare a variable NAME having value VAL *)
let rec NAME P1 P2 = (* declare a recursive function named NAME having parameters P1 and P2 *)

funcName(P1 P2 ...);; (* Make a function call to funcName with parameter *)

match (P1, P2, ...) with (* match P1, P2, ... with values)
| A, B, ... ->           (* P1=1, P2=B, ... then execute *)
| _, C, ... ->           (* P1 is any, P2=C, ... then execute *) 

Définition du langage Go

Go est un langage développé par Google et apparu en 2009. C'est un langage classique avec un typage fort et statique.

import MODULE // import module MODULE

var NAME TYPE // declare a variable named NAME of type TYPE

func NAME(P1 T1, P2 T2) (RT1, RT2) // define a function NAME having parameters P1 of type T1 and P2 of type T2 returning a couple of type RT1, RT2

Définition du langage Python

Python est un langage complet qui cherche la simplicité et l'accessibilité pour l'homme. La première version est sortie en 1980 et c'est un langage faiblement typé voir non typé. Il est très souple et ça syntaxe est permissive, ce qui le fait paraitre comme un bon langage d'apprentissage. Mais c'est plutôt le contraire. Le langage ne propose aucune sécurité contre des erreurs communes telles que les interférence due a des mélanges de type, et c'est une erreur très fréquente lorsque l'on débute. Il est cependant un excellent langage, particulièrement apprécié en data science.

import MODULE // importe le module MODULE
import MODULE from path.to.file // importe un module précise depuis un fichier/module

def FUNC(P1, P2): // définit une fonction FUNC acceptant P1 et P2 comme paramètres

A = VAL // définit une variable A valant VAL

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur la façon dont les données de vos commentaires sont traitées.

fr_FR