Catégories
Actualités Scolaires

Programmation déclarative dans .NET | Blog d'ingénierie grammaticale

La programmation déclarative peut vous aider à écrire du code plus concis, plus facile à lire et sans erreur. En d'autres termes, la programmation déclarative peut améliorer votre code d'une manière similaire à la façon dont le produit Grammarly peut améliorer votre écriture! Avec la programmation déclarative, vous dites au programme quoi faire sans spécifier Comment Ca devrait être fait. En revanche, la programmation impérative se concentre sur le flux de contrôle et les changements d'état d'un programme.

Ces définitions sont assez abstraites et peuvent prêter à confusion. Nous avons donc rassemblé cet article pour fournir un aperçu de nombreux exemples concrets de programmation déclarative, avec un accent particulier sur .NET et comment il fait avancer le paradigme déclaratif avec C # et F #. Nous donnerons également quelques exemples de la façon dont nous utilisons la programmation déclarative chez Grammarly, en particulier au sein de l'équipe qui travaille sur notre complément pour Microsoft Office. Nous espérons qu'après avoir lu, vous comprendrez la puissance de ce paradigme de programmation, apprendrez de nouvelles applications et en saurez suffisamment pour continuer à explorer des sujets connexes qui suscitent votre intérêt.

Le paradigme déclaratif est partout

Pour avoir une mentalité de réflexion déclarative, nous allons commencer par quelques exemples généraux avant de plonger dans le code réel.

Les fonctions

Imaginons d'abord une fonction simple que vous connaissez probablement à l'école: F(x) = péché (x). Qu'imaginez-vous lorsque vous pensez à cette fonction: A ou B?

Vous imaginez probablement A, non? Pour comprendre un concept comme sin (x), vous n'avez pas besoin de le recréer étape par étape. À moins que vous n'ayez besoin d'obtenir une sortie précise, votre cerveau n'a pas à faire de calculs; il connaît la relation qui est représentée, et cela suffit. C'est l'aperçu clé derrière la programmation déclarative.

DSL

Même si certains concepts de programmation déclarative peuvent sembler nouveaux, ce paradigme existe depuis longtemps. Il est probablement sûr de dire que tous les programmeurs modernes connaissent les langages spécifiques au domaine, ou DSL, tels que SQL, HTML et XML. Mais à un moment donné, ces langages étaient tout nouveaux et leurs stratégies déclaratives étaient de pointe.

L'innovation de HTML était de simplement déclarer à quoi devraient ressembler les éléments à l'écran plutôt que d'utiliser une API graphique pour indiquer au navigateur les rendus spécifiques à effectuer. Cela donne au navigateur la possibilité d'appliquer des techniques d'optimisation sous le capot sans casser les pages existantes.

Au début des bases de données, SQL représentait également une percée déclarative. À l'époque, l'interrogation des bases de données nécessitait une connaissance spécifique du fonctionnement de la base de données elle-même. Mais SQL consistait simplement à déclarer les données nécessaires – et rien d'autre. Le reste du travail, comme l'optimisation des requêtes, l'indexation et l'utilisation du cache, est effectué par le moteur de base de données lui-même.

Il y a un schéma ici: bien que la programmation déclarative semble faire moins, nous obtenons en fait plus en termes de flexibilité et d'extensibilité.

MVU

Un exemple plus moderne de programmation déclarative est le modèle Model-View-Update pour UI. Dans ce modèle, la vue est la partie déclarative qui indique à quoi devraient ressembler les comportements et l'interface utilisateur, et elle est distincte du modèle (les données). Cela aide les programmeurs à écrire du code capable de gérer des événements asynchrones complexes tout en parvenant à être lisible, robuste et efficace.

Programmation déclarative en .NET

Passons maintenant à l'introduction de quelques outils pratiques pour utiliser ce paradigme déclaratif dans l'environnement .NET. En cours de route, nous vous montrerons du code qui utilise l'approche impérative plus familière afin que vous puissiez voir les améliorations.

Rendre votre code C # plus déclaratif

Commençons par un exemple trivial en C #:

C'est une bonne vieille programmation impérative: pour parcourir une collection, nous spécifions manuellement chaque étape et opération. En examinant de plus près cette approche, nous pouvons voir les risques que l'on voudrait généralement éviter.

  • Utilisation d'une variable d'itération: La variable i pourrait avoir la mauvaise initialisation ou le mauvais incrément, et si nous sommes dans une boucle imbriquée, nous pourrions accidentellement mélanger nos variables.
  • Accès à l'élément par la variable d'itération: Pendant l'accès, nous pourrions utiliser la mauvaise variable ou la modifier accidentellement. Nous pourrions également obtenir un System.IndexOutOfRangeException si nous ne faisions pas assez attention à définir notre boucle.
  • Accumuler la somme manuellement: Ce que nous voulons, c'est additionner les éléments qui ont une certaine propriété, mais peu nous importe comment cela se fait. En fait, notre ordinateur sait déjà comment procéder. Alors pourquoi faisons-nous le travail pour l'écrire manuellement dans le code, nous ouvrant aux erreurs humaines ou aux stratégies sous-optimales?

La première chose que nous pouvons faire pour améliorer ce code est de supprimer la variable d'itération – nous ne nous en soucions pas vraiment, après tout. Son seul but était de nous donner accès à l'article actuel de la collection.

Pour aller encore plus loin, on peut éliminer l'impératif if ainsi que la sommation manuelle. Notre objectif est simplement de additionner les montants des articles d'une collection qui ont une certaine propriété. Le code suivant déclare cette intention sans aucun détail inutile:

Le passage au C # fonctionnel

Lorsque nous regardons au-delà d'un simple exemple, nous pouvons voir encore plus de possibilités. De puissants outils émergent en C # pour rendre l'ensemble du langage plus fonctionnel (la programmation fonctionnelle est un type de programmation déclarative qui exprime les calculs directement comme une pure transformation de données). C # a toujours eu une relation étroite avec le paradigme déclaratif. Beaucoup de ses fonctionnalités familières, comme LINQ, async/await, et les fonctions d'ordre supérieur – et plus tard, la mise en correspondance de modèles, etc. – sont issues des travaux de Microsoft Research sur les langages fonctionnels comme Haskell, F # et d'autres. Maintenant que la programmation déclarative en C # devient de plus en plus populaire, C # lui-même évolue rapidement pour devenir un langage plus déclaratif.

Voici quelques nouveaux concepts à rechercher en C # qui peuvent rendre votre programmation plus déclarative.

Expressions

C # effectue un changement important d'une syntaxe plus basée sur des instructions à une syntaxe plus basée sur des expressions. Les instructions, qui sont impératives, produisent exclusivement des effets secondaires, tandis que les expressions, qui sont déclaratives, utilisent des constructions de langage limitées pour produire une nouvelle valeur (et peut-être aussi des effets secondaires).

Il est généralement plus facile de composer des parties de programme à partir d'expressions plutôt que d'instructions, et les expressions sont plus universelles (vous ne pouvez pas passer une instruction à une fonction lambda, par exemple). Voici un exemple de la différence.

Déclarations et expressions

Les fonctions ci-dessous prennent un argument et stockent le résultat dans la variable sum. Toutes les fonctions qui commencent par Plus sont des déclarations – elles ne produisent pas de valeur mais créent plutôt des effets secondaires en modifiant sum:

Et ci-dessous est exactement la même idée rendue avec des expressions:

Les avantages sont clairs. Nous n'avons pas besoin de garder une trace d'un sum variable ou toute autre variable, car nous utilisons des expressions imbriquées et nous ne devons pas nous inquiéter de résultats intermédiaires. Nous pouvons représenter cette idée dans 9 lignes de code au lieu de 32.

Correspondance de motifs

La correspondance de motifs est une autre idée qui vient des langages fonctionnels. Jetons un œil au type de correspondance de motifs le plus récent et le plus avancé en C #.

Voici un exemple de code déclaratif pour la sortie d'informations sur le succès ou l'échec de certaines demandes à l'aide d'expressions et de la nouvelle syntaxe de correspondance de modèle en C #.

Définition du type:

Usage:

Cette nouvelle syntaxe, y compris la nouvelle forme d'expression de switch, déclare ce que nous voulons que le code fasse plutôt que comment le faire. Quand Code est 200, nous utilisons la correspondance de motifs pour analyser les données en un entier. Nous pouvons également récupérer les data sans avoir besoin d'utiliser d'autres variables pour la référencer manuellement.

Comparons cela à la façon dont le code aurait pu être écrit avant la nouvelle syntaxe déclarative de correspondance de modèle:

Cette version impérative est beaucoup plus verbeuse, et elle est également beaucoup plus sujette aux erreurs. Pour illustrer cela, nous avons omis un return sur la ligne 9 à dessein, et le compilateur n'a rien dit à ce sujet. Cette erreur ne sera remarquée qu'au moment de l'exécution – et s'il n'y a pas de tests, elle pourrait même persister en production. Voici un exemple de la façon dont la programmation déclarative peut vous aider à détecter les bogues plus tôt, alors qu'ils sont beaucoup moins chers à corriger.

Programmation déclarative avancée avec F #

F # est un langage open source multiplateforme «fonctionnel d'abord». Les langages déclaratifs comme F # empruntent à des recherches mathématiques éprouvées comme le calcul lambda et la recherche sur les systèmes de types (et la théorie des catégories), mais cela ne signifie pas qu'ils sont uniquement destinés aux applications mathématiques. F # est un langage à usage général qui peut vous aider à écrire de nombreux types d'applications logicielles.

Après avoir examiné certaines des dernières fonctionnalités C # pour la programmation déclarative, voyons maintenant comment F # peut nous fournir des moyens encore plus puissants de tirer parti de ce paradigme.

Fonctions de première classe

Pour programmer de manière déclarative, vous souhaiterez souvent traiter les fonctions comme des valeurs de «première classe», ce qui signifie que les fonctions peuvent être passées en tant que paramètres, acceptées en tant qu'arguments, affectées à des variables, etc. C # a des fonctions de première classe, mais F # les rend beaucoup plus flexibles et pratiques à utiliser.

Voici quelques exemples de fonctions de première classe en C #:

Ce même code semble beaucoup plus agréable en F #:

Passons en revue une partie de la syntaxe F # de base. En F #, vous pouvez considérer l'espace blanc entre une fonction et ses arguments comme un «opérateur» avec la priorité la plus élevée: il applique la fonction à l'argument de droite. Toutes les fonctions en F # sont des fonctions à argument unique. Mais bien sûr, les fonctions doivent pouvoir prendre plus d'un argument – alors comment cela fonctionne-t-il? En F #, toute fonction avec plus d'un argument est, sous le capot, un générateur de fonctions. Il prend le premier argument et retourne la fonction qui prend l'argument suivant et retourne la fonction qui prend l'argument suivant… et ainsi de suite (il fonctionne jusqu'en bas!).

Tuyauterie

«Appeler des fonctions avec des arguments est tellement ennuyeux», pourraient penser certains d'entre vous. «Et si on pouvait appeler un argument avec une fonction? Ce serait amusant!"

Eh bien, F # vous protège. Rencontrez l'opérateur de pipe-forward, |>.

Ci-dessus, nous avons le littéral booléen true et la fonction not qui l'inverse. Il ressemble au booléen et la fonction a changé de place, et l'opérateur de pipe-forward les a aidés à le faire. En fait, l'opérateur pipe-forward n'est rien de plus qu'une fonction régulière qui prend l'argument et les fonctions dans l'ordre inverse et, sous le capot, applique la fonction à l'argument.

Vous vous demandez peut-être pourquoi quelqu'un voudrait une telle chose. Pour une seule opération, ce n'est pas si bénéfique. Mais l'opérateur pipe-forward fournit un moyen très propre de structurer des chaînes de calculs. Si vous avez travaillé sur des systèmes Unix, vous savez probablement comment la tuyauterie aide à transférer des informations textuelles d'une commande à une autre. En utilisant l'exemple ci-dessous, par exemple, nous pouvons facilement isoler toutes les lignes liées à la métrique que nous voulons examiner:

Composition des fonctions

Une autre technique puissante en F # est la possibilité de combiner facilement des fonctions en utilisant l'opérateur de composition de fonction, >>. Dans l'extrait de code suivant, nous essayons de mapper une liste de nombres de manière à ce que chaque élément soit incrémenté de 2, puis multiplié par 3.

L'opérateur >> prend essentiellement deux fonctions et les exécute dans un ordre séquentiel. Il génère une nouvelle fonction qui applique d'abord la fonction de gauche, puis prend le résultat et lui applique la fonction de droite. En utilisant l'opérateur de composition de fonctions, nous pouvons facilement déclarer notre intention d'une manière assez intuitive.

Remarque: Il existe d'autres opérateurs pour les fonctions en F #, mais à la base ce ne sont que différentes variantes des principales idées que nous avons examinées. Pour plus de lecture, vous pouvez regarder Documentation F #.

Exemple: opérations de collecte avec F #

Revenons à l'exemple avec lequel nous avons commencé en C #. Nous avons réécrit un extrait de code impératif, qui résumait les éléments d'une collection avec une certaine propriété afin d'être plus déclaratif. Rappelez-vous le résultat?

En F #, ce code déjà simple devient encore plus simple. Il se réduit à une seule ligne:

Décrivons ce qui se passe ici.

  • Item.Priority’ et Item.Amount’ ne sont que des fonctions statiques qui renvoient ces propriétés, à peu près les mêmes que celles que nous avons sur la version C # à l'intérieur des lambdas.
  • (=) 5 crée la fonction d'égalité à l'aide d'une application partielle.
  • >> compose la fonction à gauche avec la fonction à droite.

Vous remarquerez peut-être qu’il n’y a aucun argument de fonction réel, nous n’en avons pas besoin! Nous avons éliminé un autre défaut potentiel: utiliser la mauvaise variable dans la fonction lambda.

Unions discriminées et invariants de domaine

F # a quelques types très utiles appelés Discriminated Unions. À première vue, ils ressemblent à des énumérations sur les stéroïdes et peuvent contenir des informations personnalisées. Leur structure est assez simple. À gauche se trouve le nom du type et à droite, spécifié par le of sont des balises qui peuvent ou non contenir des informations supplémentaires, jointes par le | opérateur.

Voici quelques exemples, y compris un type commun pour représenter une valeur manquante, un type qui peut être utilisé pour indiquer une opération qui peut échouer et un type qui montre comment on peut facilement coder une structure de données plus complexe:

Comme vous pouvez le voir, avec Discriminated Unions, nous pouvons décrire complètement la structure des types simples sur une seule ligne intuitive.

En utilisant des types Union discriminés, il est facile de décrire des modèles de domaine et, encore plus important, des invariants de domaine. Avec des invariants de domaine établis à l'avance, des problèmes sont susceptibles d'être détectés au stade de la compilation. Il y a un dicton ironique dans la communauté de programmation fonctionnelle: s'il compile, il n'a pas de bogues.

Voyons un exemple simple avec FirstName et LastName types et un register pour voir comment nous pouvons programmer de manière plus sûre avec Discriminated Unions:

Traditionnellement, nous aurions pu mettre en œuvre le register fonctionner avec des chaînes régulières pour les paramètres de nom, mais si nous le faisions, nous pourrions finir par échanger les prénoms et les noms de famille, ou même passer une chaîne sans rapport comme argument. En utilisant les types d'unions discriminées, nous avons réalisé une version qui spécifie quel nom est lequel. Ce qui est bien avec cette approche déclarative, c'est qu'une fois que les arguments sont initialisés (ici c'est fait simplement, mais nous pourrions également appliquer une logique de validation), nous savons que nous n'avons pas à nous soucier des données invalides ou échangées.

Comment nous utilisons les approches déclaratives chez Grammarly

Pour conclure notre discussion, nous allons maintenant couvrir plusieurs façons dont la programmation déclarative a aidé notre équipe à résoudre des problèmes du monde réel tout en créant notre complément pour Microsoft Office.

Vérification des données aux limites du système

Comme c'est souvent le cas, certaines des parties les plus délicates de notre système existent aux frontières entre les domaines. Des problèmes ont tendance à survenir lors de la gestion des réponses des serveurs, de la communication avec les bases de données, de la lecture des fichiers, etc. Le principal problème est toujours: comment nous assurer que les données de la dépendance externe sont au format correct afin que nous puissions procéder à l'application de certaines affaires logique? Vous imaginez probablement déjà la réponse. Validez simplement les données, non?

Bon type de. Imaginons que nous voulons configurer un point de terminaison pour enregistrer les e-mails des utilisateurs. (Dans notre équipe, nous rencontrons des problèmes similaires, bien que sans rapport avec le courrier électronique.)

Une fois que nous validons le email chaîne à cet endroit particulier, nous la transmettons à la SaveEmail une fonction. Mais il y a un problème. Imagine ça SaveEmail invoque un SendEmail fonction qui accepte également la chaîne email. Et si nous oublions de faire une validation dans le SendEmail fonctionner parce que nous pensons que le email l'argument est déjà valide, mais notre coéquipier appelle SendEmail d'ailleurs sans valider d'abord l'e-mail? Si nous oublions une seule validation quelque part, nous pourrions avoir des problèmes sur la route. Comment pouvons-nous faire mieux?

Analyser, ne pas valider

La devise "analyser, ne pas valider»Est devenu très populaire dans la communauté de programmation fonctionnelle. En d'autres termes, pour vous assurer que certaines données sont valides dans votre domaine, vous devez les analyser dans votre objet de domaine. L'exemple le plus simple pourrait être l'analyse de chaînes en nombres entiers:

Avant d'analyser, vous ne pouvez pas être sûr si la chaîne est un nombre ou non. L'analyse est un moyen de déclarer que certaines données sont une partie valide du modèle de domaine. Il peut être utile de penser de cette façon: Parsing = validation + transformation.

Construire un analyseur JSON en F #

L'équipe de Grammarly qui travaille sur le complément Microsoft Office fonctionne avec de nombreuses spécifications JSON différentes dans notre backend, et nous voulions gérer les erreurs plus gracieusement lors de la réception de messages avec la mauvaise structure. Nous avons donc construit une DSL qui nous a permis de spécifier de manière déclarative la structure que nous attendons d'un message JSON et d'analyser les données dans le message Entity lui-même, en utilisant les opérateurs F # décrits ci-dessous.

Comme vous pouvez le voir, lorsque nous appliquons la configuration à une chaîne afin d'extraire l'entité, il n'y a que deux résultats possibles: succès (Ok) ou l'échec (Error). Ainsi, nous garantissons que toutes les entités ont réussi l'analyse et nous pouvons compter sur leurs données à l'intérieur de notre domaine.

Distribution des versions

Nos utilisateurs devraient toujours obtenir la bonne version du complément Grammarly pour Microsoft Office, en fonction des spécifications de leur appareil, des opt-ins, etc.

Nous voulions des règles de mise à jour qui nous donneraient un contrôle précis sur la distribution des versions et un moyen clair de spécifier quelle version devrait aller à quel utilisateur. Mais parmi les outils existants pour .NET, nous n'avons pas pu trouver la solution «goldilocks» qui convenait parfaitement. Certaines solutions impliquaient une syntaxe gonflée et impérative que notre équipe devrait apprendre. D'autres nécessitaient des déploiements de code pour chaque règle, ou disposaient de machines plus lourdes que ce dont nous avions besoin pour la tâche.

Nous avons donc utilisé la programmation déclarative pour développer nos propres outils déclaratifs de versioning:

Par conséquent, nous avons des règles de mise à jour de version qui sont similaires à JSON, avec une syntaxe intuitive et facile à retenir. Il est sûr d'ajuster le comportement sans déployer de code, et il est simple de régler les règles sans changer la configuration.

Conclusion

Nous avons exploré de nombreuses façons dont la programmation déclarative produit un code plus sûr, plus propre et plus efficace, réduisant les erreurs et améliorant la lisibilité et la maintenabilité. Bien que certains de ces concepts aient pu être nouveaux pour vous, vous pourriez dire que beaucoup d'idées dont nous avons parlé ne sont que de vieilles abstractions, et vous ne vous tromperiez pas. L'idée derrière la programmation déclarative est simple. Définissez le strict minimum dont vous avez besoin pour vous concentrer sur votre domaine problématique lui-même et laissez les détails de l'implémentation prendre soin d'eux-mêmes – vous ferez plus avec moins!

Si vous avez apprécié cette discussion, vous voudrez peut-être en savoir plus sur Grammarly’s pile technologique et consultez notre rôles ouverts. Si vous êtes passionné par l'écriture de code propre et que vous souhaitez aider des millions de personnes à travers le monde à écrire avec clarté et confiance, contactez-nous!

Laisser un commentaire

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