Modèles de programmation alternatifs

7.3 Modèles de programmation alternatifs

Objectifs d'apprentissage

À la fin de cette section, vous serez capable de :

  • Discuter des caractéristiques de la programmation fonctionnelle
  • Expliquer les caractéristiques de la programmation déclarative
  • Distinguer les caractéristiques de la programmation orientée objet
  • Expliquer les constructions des HLL utilisées pour prendre en charge la concurrence et le parallélisme
  • Résumer quand utiliser les langages de script

Comme nous l'avons appris, les HLL peuvent être classés en divers paradigmes, dont deux sont impératifs et déclaratifs. Il existe d'autres paradigmes qui entrent dans ces catégories principales. Parmi ceux qui méritent d'être étudiés figurent la programmation fonctionnelle, la programmation déclarative, la programmation orientée objet et la programmation parallèle utilisant la concurrence.

Programmation fonctionnelle

En 1930, Alonzo Church a développé le modèle de calcul lambda, qui tire son nom de la lettre grecque lambda (λ). Dans ce modèle, chaque paramètre était introduit par le symbole lambda et le calcul était effectué en plaçant des paramètres dans des expressions (expression) de la même manière que les programmes de haut niveau transfèrent des arguments (argument) à des fonctions (function). La Figure 7.20 met en évidence divers HLL.

Hiérarchie de la programmation. Impérative (divisée en Procédurale (divisée en FORTRAN, COBOL) et Orientée objet (divisée en Java, C++)) et Déclarative (divisée en Logique (divisée en Prolog, Loglan) et Fonctionnelle (divisée en Haskell, Erlang).
Figure 7.20 Les langages de programmation fonctionnelle peuvent être différenciés par leurs paradigmes. (source : Copyright Rice University, OpenStax, sous licence CC BY 4.0)

Concepts de programmation fonctionnelle

Le concept clé du modèle de programmation fonctionnelle est que les programmes sont construits en composant et en appliquant des fonctions. Une fonction pure (pure function) est une fonction qui retournera (return) le même résultat à chaque fois pour les mêmes arguments, et il n'y a pas de lecture d'état mutable partagé. Dans ce contexte, les données partagées (shared data) signifient que les données sont disponibles à plusieurs emplacements de programme ou portées (scope), l'état (state) représente les données qui sont mémorisées au fil du temps, et mutable (mutable) signifie qu'il est modifiable. Si les données utilisées par la fonction sont partagées et mutables, il est possible de retourner des résultats différents pour les mêmes arguments. Une fonction pure ne doit pas avoir d'effets secondaires, ce qui signifie qu'elle ne peut pas modifier un état qui ne lui est pas local. Pour cette raison, les programmes fonctionnels n'ont pas d'instructions d'affectation (assignment statement), ce qui est également connu sous le nom de transparence référentielle.

D'autres caractéristiques et fonctionnalités nécessaires qui peuvent manquer dans certains langages impératifs (imperative language) incluent les fonctions de première classe et les fonctions d'ordre supérieur. Une fonction de première classe (first-class function) est une fonction qui peut être assignée comme valeur à une variable (variable). Une fonction d'ordre supérieur (high-order function) est une fonction qui peut prendre une ou plusieurs fonctions de première classe comme arguments. Elle peut également retourner des fonctions comme résultats, ce qui donne à une fonction la capacité d'appliquer des fonctions à ses arguments une par une. Ceci est très utile pour rendre le code à la fois plus flexible et plus simple. Cela encourage la création de fonctions plus petites qui ne s'occupent que d'une partie d'une tâche plus importante, ce qui contribue également grandement à la maintenabilité (maintainability).

Évaluation de la programmation fonctionnelle

De la même manière que vous effectuez des tâches répétitives chaque jour, la programmation fonctionnelle traite les fonctions utilisées pour exécuter ces tâches comme des citoyens de premier ordre. Parmi les avantages de la répétition, on peut citer le fait de rendre les programmes plus faciles à comprendre pour le programmeur et de les raccourcir. Certains inconvénients de la programmation fonctionnelle sont la nécessité de déplacer les données à travers les fonctions, une implémentation efficace est difficile (mais pas impossible), et de nouveaux tableaux (array) doivent être créés lorsque les données d'un élément (element) sont modifiées.

Programmation orientée objet

Un certain nombre de langages impératifs sont considérés comme orientés objet, et la plupart des HLL modernes prennent en charge ce paradigme à un certain degré. Sa caractéristique fondamentale est de concevoir des logiciels autour du concept d'une classe (class), qui est le plan à partir duquel les objets sont construits. La construction d'un objet est appelée instanciation (instantiation) ou la création d'une instance d'une classe. Les objets sont composés d'attributs (caractéristiques) et de comportements (méthodes ou fonctions).

Les trois principes principaux de la POO sont l'encapsulation, l'héritage et le polymorphisme, qui offrent une très grande capacité d'abstraction (abstraction). Avec l'encapsulation (encapsulation), les attributs et les comportements des classes et des objets sont autonomes et les accompagnent tout au long de leur durée de vie. Cela signifie également que l'implémentation interne est cachée de l'extérieur de la classe ou de l'objet. Dans l'héritage (inheritance), les objets reprennent des attributs spécifiés de leurs ancêtres. Un exemple est qu'un objet Chien hérite d'un objet parent Animal. Le polymorphisme signifie que les comportements hérités peuvent s'exécuter de différentes manières selon leur contexte. Un Chien est un objet Animal et fait du bruit. Un Serpent est aussi un objet Animal qui fait du bruit — un bruit différent.

Le degré auquel un HLL se conforme à ces principes mesure son orientation objet. Java et C# sont entièrement orientés objet. C/C++ est un hybride de caractéristiques procédurales et de caractéristiques de la POO. Des langages tels que Python, JavaScript et PHP adhèrent partiellement à ces caractéristiques. Par conséquent, ils sont parfois appelés basés objet.

Encapsulation

Les langages POO contiennent des fonctionnalités pour implémenter l'encapsulation ; ils sont autonomes et permettent le masquage des données. Ces fonctionnalités incluent des règles de portée, par exemple, la définition de variables et de méthodes au sein de classes et l'utilisation de modificateurs d'accès (access modifier).

Les langages orientés objet ont souvent des constructeurs, et certains ont des destructeurs qui supportent l'encapsulation. Un constructeur (constructor) est une méthode spécialisée qui est appelée pour instancier l'objet. Très souvent, les constructeurs ont des paramètres qui peuvent être utilisés pour initialiser les valeurs des attributs avec des arguments qui leur sont passés afin que les variables ne soient pas directement accédées ou assignées. Un destructeur (destructor) est utilisé pour détruire l'objet instancié et récupérer sa mémoire comme en C++. Java et les HLL modernes disposent d'un programme d'arrière-plan pour le ramasse-miettes (garbage collection) qui détruit automatiquement les objets et récupère la mémoire lorsqu'il n'y a plus de références à ceux-ci dans le code.

Ils emploient également l'utilisation de méthodes pour définir et retourner les valeurs des variables afin qu'elles ne soient pas directement accédées. Le masquage des données est en outre activé dans certains HLL modernes tels que Java. Cela implique un processus d'arrière-plan pour le ramasse-miettes qui détruit automatiquement les objets et récupère la mémoire lorsqu'il n'y a plus de références à ceux-ci dans le code.

Le masquage des données est l'un des principaux objectifs de la POO qui relève de l'encapsulation. Son but est de traiter les détails d'implémentation dans une classe ou un objet qui sont sans importance pour les utilisateurs de la classe ou de l'objet. Ils n'ont besoin de savoir que comment les utiliser ; par conséquent, les détails sont cachés et ne fournissent que ce qui est nécessaire dans son interface publique (public interface).

La plupart des langages POO ont les mots-clés (keyword) public et private. Ils sont utilisés pour marquer la visibilité et l'accès aux attributs et méthodes par des zones de code qui sont en dehors du code de la classe elle-même. Nous les appelons modificateurs d'accès. Un membre (member) est un attribut ou une méthode encapsulé au sein d'une classe ou d'un objet. Un membre public fait partie de l'interface publique, et un membre privé est caché. Le code Java suivant est une définition de classe abrégée qui démontre le concept :

public class Rectangle {
   // attributes
   private double length;
   private double width;
   // constructors
   public Rectangle(double length, double width) {
      . . . . .
   }
   // methods
   public double getLength() {
      return length;
   }
   . . . . .
   public double getArea() {
      . . . . .
   }
}

La classe elle-même est publique, accordant l'accès à elle et à son contenu. Les attributs sont tous privés, ne faisant pas partie de l'interface publique. Dans ce contexte, le concepteur a décidé de ne pas autoriser l'accès direct à ces attributs de l'extérieur. Au lieu de cela, ils sont accédés par des méthodes publiques, dont certaines ne sont pas montrées. Les méthodes que nous employons, appelées accesseurs (getters) et mutateurs (setters), peuvent présenter nos données au monde extérieur de la manière que nous choisissons. La méthode peut être décrite comme une méthode utilitaire qui peut fournir des données utiles au public afin qu'il n'ait pas à les calculer lui-même.

Héritage

La Figure 7.21 illustre l'implémentation de l'héritage simple (single inheritance), la méthodologie utilisée par la plupart des HLL POO modernes. Les classes et les objets ne peuvent hériter que d'un seul parent.

Capture d'écran de l'héritage simple.
Figure 7.21 Ce diagramme UML montre l'héritage simple en Java. (source : Copyright Rice University, OpenStax, sous licence CC BY 4.0)

Nous appelons la classe parente la super-classe (superclass) dans une relation d'héritage et les classes enfants une sous-classe (subclass) qui contient les attributs et les méthodes de sa classe parente. Dans cette illustration, tous les attributs de Véhicule sont hérités par la Voiture et les autres sous-classes avec les mêmes modificateurs d'accès exacts. Il en va de même pour toutes les méthodes.

Seul C++ implémente l'héritage multiple (multiple inheritance), héritant des attributs et des méthodes de plus d'un parent. Ce concept est extrêmement complexe et peut causer des problèmes majeurs d'implémentation. Un exemple est lorsque les deux parents ont une méthode du même nom et qu'il ne peut être spécifié laquelle est héritée ou redéfinie. Pour implémenter un comportement partagé par plusieurs classes parentes, Java a introduit l'interface, qui peut être implémentée en C++ pour résoudre les problèmes d'héritage multiple.

Une interface (interface) ressemble à une définition de classe, mais c'est une collection de méthodes uniquement. Ces méthodes sont construites comme des méthodes abstraites. Une méthode abstraite (abstract method) est déclarée sans bloc de code (code block) pour l'implémentation. Une classe peut utiliser une ou plusieurs interfaces. Voici une définition de classe Java qui utilise une interface :

public class Rectangle implements geometryMath{
}

L'interface agit comme un contrat avec le programmeur. Si le programmeur déclare qu'une classe implémente une interface, il est requis que la classe fournisse le code d'implémentation pour les fonctions qui composent l'ensemble de l'interface. Une redéfinition (override) définit une autre méthode avec le même nom et modifie sa signature. La redéfinition forme une instance concrète de ce qui était une méthode abstraite dans l'interface. Les interfaces apparaissent en Java, C#, Go ou Ruby, et sont l'approche dominante, supplantant le véritable héritage multiple.

Polymorphisme

La caractéristique de la POO selon laquelle les méthodes héritées peuvent s'exécuter de différentes manières dans différentes sous-classes en fonction de leur contexte est appelée polymorphisme (polymorphism). Ceci est implémenté en redéfinissant les méthodes héritées dans une sous-classe ou celles d'une interface implémentée. Dans le diagramme UML, le comportement de lancement d'une voiture est très différent de celui d'un voilier ou d'une fusée. Le comportement résultant dépend du contexte car il n'est pas défini au moment de la compilation. C'est plutôt l'exécution qui décide quoi faire lorsqu'elle rencontre un objet basé sur sa classe. Le comportement se transforme et produit des résultats différents pour les mêmes appels de méthode selon l'objet en question, ce qui est la liaison dynamique de méthode (dynamic method binding) ou la liaison tardive (late binding), ce qui signifie que toutes les méthodes sont résolues dynamiquement à l'exécution, et non par le compilateur (compiler).

Évaluation de la POO

Rappelez-vous que certains langages comme Java et C# sont entièrement orientés objet ; d'autres comme C++ sont des hybrides de procédural et d'orienté objet, et certains sont en voie de devenir entièrement orientés objet comme JavaScript, PHP et Python. Toute évaluation du paradigme orienté objet doit être effectuée dans le contexte de la position d'un langage particulier sur l'échelle de l'orientation objet.

Avantages de la POO :

  • Forte abstraction (abstraction) avec la capacité de réutiliser le code efficacement
  • Productivité accrue du développement logiciel et maintenabilité (maintainability)
  • Développement logiciel plus rapide, donc coût de développement inférieur car il est très efficace pour le développement parallèle
  • S'adapte bien au parallélisme (parallelism), où les programmes peuvent avoir plus d'une partie du code s'exécutant simultanément
  • Logiciel plus cohérent — la liaison dynamique de méthode produisant le polymorphisme est un mandat selon lequel les sous-classes implémentent les mêmes comportements avec des différences

Inconvénients de la POO :

  • Courbe d'apprentissage abrupte pour maîtriser la POO
  • La création de programmes peut être complexe
  • Exécution plus lente due à un plus grand nombre d'instructions générées par le compilateur

Concurrence et programmation parallèle

Un processus, ou un fil d'exécution, est un contexte d'exécution actif, ce qui signifie qu'il s'agit d'un bloc de code exécutable. La capacité d'une application à être multitâche est appelée concurrence. Lorsqu'une application peut être multitâche (multitask), elle peut traiter plus d'une tâche simultanément. C'est le cas de leurs exécutions qui se chevauchent dans le temps. En concurrence, nous avons l'illusion d'une exécution simultanée. Un processeur monocœur ne peut exécuter qu'une seule tâche à la fois tandis que le processeur bascule rapidement entre les processus concurrents, créant ainsi l'illusion. Nous appelons l'unité exécutable un fil d'exécution (thread of control) parce que le processeur contrôle quel fil d'exécution est exécuté à un moment désigné.

En parallélisme, les programmes peuvent avoir plus d'un fil d'exécution s'exécutant simultanément. Cela n'est possible que si nous avons des processeurs multiples ou multicœurs qui nous permettent de produire une exécution simultanée. Nous devons programmer pour la concurrence et le parallélisme de la même manière car la logique d'exécution est la même, seul le matériel physique est différent. Le processus de codage pour ces environnements est la programmation concurrente ou la programmation parallèle (parallel programming) dans laquelle un fil d'exécution peut être considéré comme une abstraction d'un processeur physique.

Conditions de concurrence

Lorsque l'état d'un programme et ses comportements ne sont pas synchronisés, une condition de concurrence (race condition) se produit. Les conditions de concurrence sont une question de synchronisation car des anomalies peuvent survenir lorsque nous ne savons pas quel processus se terminera en premier ou quelle partie du programme est en cours d'exécution. Nous pouvons exercer un contrôle sur le moment où un processus se met en pause ou se termine dans le scénario "exécuter jusqu'à blocage" où un certain mécanisme met un fil d'exécution en pause. Cette situation n'est pas toujours négative ; les conditions de concurrence peuvent parfois être positives en permettant au multithreading de concourir sans restriction pour l'attention du processeur.

Dans de nombreuses situations multitâches, nous voulons éviter les conditions de concurrence et exercer un certain degré de contrôle de l'exécution ; en d'autres termes, nous voulons synchroniser les fils d'exécution dans des situations entrelacées ou parallèles.

Synchronisation

L'établissement de la coopération entre les fils d'exécution, ou synchronisation, est souvent géré en déterminant d'abord quels segments de code forment une section critique. Une section critique est un bloc de code qui influence les résultats de la concurrence ou du parallélisme. Un exemple simple pourrait être un programme d'employé dans lequel une fonction (function) concurrente calcule le salaire de l'employé et une autre imprime le chèque de paie. La fonction d'impression ne peut pas procéder à l'inscription du montant sur le chèque tant que la fonction de calcul ne retourne (return) pas le montant. D'autres situations nécessitant l'utilisation de sections critiques sont les suivantes :

  • Lorsque plusieurs fils d'exécution ont besoin d'accéder à de la mémoire partagée ou à des ressources, et que le moment de l'accès peut être critique pour le résultat final
  • Lorsque les processus doivent communiquer entre eux pour avancer, souvent implémenté comme un passage de messages entre processus
  • Cas où tous les processus concurrents doivent se terminer avant que l'exécution du programme ne se poursuive
  • Cas où un ou plusieurs des processus ont besoin d'un résultat d'un autre processus

Comme visible dans la Figure 7.22, lorsque les processus s'exécutent dans un ordre demandé, la synchronisation se produit.

Illustration de quatre fils d'exécution menant à une section critique.
Figure 7.22 Lorsque plusieurs fils d'exécution cherchent à accéder à une section critique, ils ont besoin de synchronisation. (source : Copyright Rice University, OpenStax, sous licence CC BY 4.0)

Puisque l'objectif est d'éviter les conditions de concurrence négatives, nous ne voulons pas sur-synchroniser car cela diminue le degré de parallélisme dont nous avons besoin pour la performance.

Implémentation de la synchronisation

La synchronisation doit être implémentée avec soin pour éviter les situations que la synchronisation elle-même peut causer :

  • famine (starvation) : lorsqu'un processus doit attendre pour entrer dans une section critique, mais que les autres processus monopolisent la section, et que le processus en attente n'obtient pas de temps processeur.
  • attente active (busy waiting) : lorsqu'un processus cherche continuellement à savoir s'il a accès à une section critique, prenant du temps processeur à tous les processus.
  • interblocage : lorsque plusieurs fils d'exécution ont besoin d'une section critique qui est monopolisée par l'un des processus.

Java est connu pour sa forte implémentation de la synchronisation. Lorsqu'une méthode ou un bloc de code est marqué comme synchronisé, il est garanti qu'un seul fil d'exécution peut s'y exécuter simultanément. Tout autre fil d'exécution essayant d'entrer dans la méthode ou le bloc de code est bloqué jusqu'à ce que l'occurrence en cours quitte la méthode ou le bloc. Le code Java suivant montre la syntaxe d'utilisation du mot-clé (keyword) sur une méthode :

public synchronized void calcAverage() {
   // all of the method code
}

Une méthode synchronisée est coordonnée sur l'objet qui possède la méthode. En POO, chaque instance de l'objet peut avoir sa propre synchronisation. Par exemple, nous pourrions avoir une classe Voiture qui possède une méthode appelée start(). Chaque instance de voiture que nous construisons a sa propre copie de la méthode ; par conséquent, si nous démarrons une voiture, aucun autre processus ne peut démarrer cette voiture particulière. Mais si nous avons plus d'une voiture, une autre peut être démarrée concurremment.

Le code Java suivant montre la syntaxe d'utilisation du mot-clé sur un bloc de code au sein d'une méthode :

synchronized (this) {
   // critical section
}

Ce code s'exécutera exactement comme s'il s'agissait d'une méthode synchronisée. Ce mot-clé fait référence à l'objet courant, donc ce code se synchronise également sur l'objet qui possède le bloc de code. Cette utilisation offre une granularité et des temps d'exécution beaucoup plus efficaces.

Un autre outil puissant pour la synchronisation en Java est de déclarer une classe entière comme un fil d'exécution, ce qui signifie que toute la classe se trouve dans un seul fil d'exécution. Il existe deux façons de le faire. La première consiste à faire hériter la classe de la classe Thread de l'API Java :

public class SomeThread extends Thread {
}

L'autre façon est de faire implémenter une interface de l'API par la classe :

public class SomeThread implements Runnable {
}

L'une ou l'autre de ces méthodologies donne accès aux méthodes utiles de la classe Thread qui sont héritées et peuvent être redéfinies pour prendre en charge la situation particulière. L'interface forme un contrat avec le programmeur pour implémenter ces méthodes afin de prendre en charge la tâche particulière.

Un objet Thread est contrôlé par un ordonnanceur basé sur les priorités. Certaines des méthodes les plus importantes à la disposition du programmeur sont les suivantes :

  • start() : provoque le démarrage de l'exécution du fil d'exécution
  • yield() : indique à l'ordonnanceur de fils d'exécution que le fil d'exécution actuel est prêt à céder son contrôle
  • sleep(int time) : fait hiberner temporairement le fil d'exécution en cours d'exécution pendant un nombre spécifié de millisecondes
  • setPriority(int priority) : modifie la priorité du fil d'exécution actuel
  • join(int time) : attend le nombre de millisecondes indiqué avant de commander l'arrêt du fil d'exécution

Concepts en pratique

Quel HLL fonctionne le mieux ?

Les HLL modernes combinent une variété de modèles de programmation pour faciliter la résolution de problèmes dans divers domaines et industries. Deux des modèles que nous avons étudiés sont la programmation orientée objet et la concurrence.

Le besoin d'un modèle orienté objet peut être dicté par la nécessité d'une grande capacité d'abstraction (abstraction). D'autres raisons incluent une productivité améliorée dans le développement logiciel, le coût, l'extensibilité (scalability) et la maintenabilité (maintainability).

La POO s'adapte également bien au parallélisme, qui est un autre paradigme à considérer. Un besoin de vitesse peut exiger la sélection (selection) d'un HLL qui supporte bien la concurrence et/ou le parallélisme. Il se peut que l'application que nous construisons nécessite les deux.

Nous avons des choix, dont les plus populaires sont C++, Java et C#. Réduire nos choix est utile et nous met en excellente position pour examiner chacun afin de prendre une décision éclairée.

Parfois, la POO est souhaitable, mais la concurrence peut ne pas l'être. Cela se produirait lors de la programmation de systèmes d'exploitation tels que Windows avec C++. Il a une interface utilisateur orientée objet, mais il doit également manipuler directement les éléments (element) de la machine. Dans ce cas, la concurrence peut ne pas être souhaitable, donc certains modules (module) devraient être programmés sans concurrence en les codant uniquement dans les aspects C du langage.

Programmation avec les langages de script

Les langages de script ont leurs racines dans le script shell (shell scripting), désignant à l'origine l'enchaînement d'un groupe de commandes pour effectuer des tâches sur les interfaces utilisateur de divers systèmes d'exploitation tels que les systèmes pré-GUI, Unix DOS et CPM. Les systèmes d'exploitation modernes offrent toujours cette fonctionnalité dans des programmes tels que Windows PowerShell. Vous pouviez enchaîner (string) un groupe de commandes pour effectuer des tâches, soit directement sur le terminal, soit plus efficacement dans des fichiers batch que les shells pouvaient exécuter. Les fichiers batch sont créés dans un éditeur de texte pour contenir le script. Ils ont généralement une extension de fichier que le shell peut reconnaître, comme backup.bat, qui pourrait contenir des commandes pour sauvegarder un ordinateur MS-DOS. Ces langages sont interprétés à l'exécution et ne sont pas compilés.

Les premiers langages de script (scripting language) incluent les suivants :

  • Interpréteur de commandes MS-DOS
  • Unix : l'interpréteur de ligne de commande standard lors de l'exécution du shell Bourne d'Unix
  • Microsoft PowerShell : utilisé pour l'automatisation et la gestion de la configuration sur les systèmes Windows

La plupart de nos langages de script HLL utilisés aujourd'hui ont évolué à partir de ceux-ci, en particulier pour la programmation du Web. La Figure 7.23 illustre également certains de ces langages.

  • Perl est un langage de script plus ancien principalement utilisé pour le scripting web qui utilise l'interface de passerelle commune (CGI) (Common Gateway Interface (CGI)), une spécification industrielle pour la communication des serveurs web avec les navigateurs web. Les deux entités différentes ont besoin de règles pour « parler » entre elles, connues sous le nom de protocoles tels que HTTP. Perl a été principalement remplacé par PHP, ASP.NET et d'autres langages de script de serveur web.
  • Python est un HLL à usage général pour la programmation cœur et web.
  • JavaScript est un langage de script web à usage général qui est presque universel. Il se concentrait à l'origine sur les navigateurs web frontaux et détient maintenant une grande part de marché sur les serveurs dorsaux avec la nouvelle version ES6.
  • PHP est un langage de script de très haut niveau pour les serveurs web qui est indépendant de la plateforme.
Illustration du langage de script divisé en Côté client (Ajax, Java Script, JQuery, VB Script) et Côté serveur (ASP, Java Script, JSP, Perl, Python, Ruby).
Figure 7.23 Les langages de script peuvent être catégorisés davantage selon qu'ils sont côté client ou côté serveur. (source : Copyright Rice University, OpenStax, sous licence CC BY 4.0)

Les scripts d'interface de passerelle commune sont le mécanisme original pour le scripting web côté serveur. Un script d'interface de passerelle commune (CGI) (Common Gateway Interface (CGI)) est un programme exécutable résidant dans un répertoire spécial connu du programme de gestion du serveur web. Lorsqu'un client demande une adresse web connue sous le nom d'Uniform Resource Indicator (URI), le serveur exécute le programme et produit du HTML sur un écran lisible par le navigateur, qui le rend à un utilisateur. Par exemple, lorsque vous effectuez un achat en ligne, le serveur web lance plusieurs scripts en coulisses pour recueillir des informations sur les produits, des images et des prix. La plupart des sites web utilisent des écrans de chargement pour permettre l'utilisation d'un ou plusieurs langages de script. Bien que largement utilisés, les scripts CGI présentent plusieurs inconvénients, le principal étant la lenteur des temps de chargement des pages web due au fait de devoir lancer chaque script comme un programme distinct.

Un script côté serveur (server-side script) s'exécute sur un serveur dans le domaine d'un concepteur web et est généralement plus rapide qu'un script CGI grâce à sa capacité à compiler en temps réel ; par conséquent, en PHP, Python, Ruby et JavaScript ES6 dorsal, le client ne voit que le HTML standard.

Un script côté client (client-side script) s'exécute sur un ordinateur client, généralement sous le contrôle d'un navigateur web et nécessite un interpréteur sur la machine du client. Les scripts côté client utilisent souvent un script embarqué (embedded script) dans lequel un langage est intégré à un autre. Un exemple fréquent est JavaScript intégré dans le HTML. JavaScript pour les fonctionnalités interactives sur une page web est presque le standard universel. Microsoft produit un langage appelé TypeScript qui est un sur-ensemble de JavaScript. C'est un langage compilé qui génère du JavaScript. Il a les avantages d'être fortement typé (strongly typed) et possède des fonctionnalités POO plus robustes. Il peut être utilisé sur le frontal (front end) et le dorsal (back end), bien qu'il ne soit pas aussi populaire que JavaScript ES6 côté serveur.

Les langages côté client et côté serveur comme JavaScript et PHP peuvent être intégrés dans le code HTML avec des éléments embarqués. Un élément embarqué est un programme conçu pour s'exécuter à l'intérieur d'un autre programme comme le HTML. Côté client, nous intégrons JavaScript dans les éléments <script> HTML et un navigateur appelle l'interpréteur JavaScript lorsqu'il en rencontre un. Côté serveur, nous pouvons intégrer PHP de la même manière avec quelques règles. Le script doit être installé sur le serveur et doit avoir une extension de fichier .php.