Scala versus F # question: comment unifier les paradigmes OO et FP?

Quelles sont les principales différences entre les approches adoptées par Scala et F # pour unifier les paradigmes OO et FP?

MODIFIER

Quels sont les avantages et les inconvénients de chaque approche? Si, malgré le support du sous-typage, F # peut déduire les types d’arguments de fonction, alors pourquoi ne pas Scala?

J’ai regardé F #, faisant des tutoriels de bas niveau, donc ma connaissance est très limitée. Cependant, il était évident pour moi que son style était essentiellement fonctionnel, OO se rapprochant davantage d’un système de modules ADT + que de véritables OO. Le sentiment que je ressens peut être mieux décrit comme si toutes les méthodes étaient statiques (comme en Java statique).

Voir, par exemple, tout code utilisant l’opérateur de canal ( |> ). Prenez cet extrait de l’ entrée Wikipedia sur F # :

 [1 .. 10] |> List.map fib (* equivalent without the pipe operator *) List.map fib [1 .. 10] 

La fonction map n’est pas une méthode de l’instance de liste. Au lieu de cela, il fonctionne comme une méthode statique sur un module List qui prend une instance de liste comme l’un de ses parameters.

Scala, par contre, est entièrement OO. Commençons d’abord par l’équivalent Scala de ce code:

 List(1 to 10) map fib // Without operator notation or implicits: List.apply(Predef.intWrapper(1).to(10)).map(fib) 

Ici, map est une méthode sur l’instance de List . Les méthodes de type statique, telles que intWrapper sur Predef ou apply sur List , sont beaucoup plus rares. Ensuite, il y a des fonctions, telles que fib ci-dessus. Ici, fib n’est pas une méthode sur int , mais ce n’est pas non plus une méthode statique. Au lieu de cela, c’est un object – la deuxième différence principale que je vois entre F # et Scala.

Considérons l’implémentation F # de Wikipedia, et une implémentation Scala équivalente:

 // F#, from the wiki let rec fib n = match n with | 0 | 1 -> n | _ -> fib (n - 1) + fib (n - 2) // Scala equivalent def fib(n: Int): Int = n match { case 0 | 1 => n case _ => fib(n - 1) + fib(n - 2) } 

L’implémentation Scala ci-dessus est une méthode, mais Scala la convertit en une fonction pour pouvoir la transmettre à la map . Je vais le modifier ci-dessous pour qu’il devienne une méthode qui renvoie une fonction pour montrer comment les fonctions fonctionnent dans Scala.

 // F#, returning a lambda, as suggested in the comments let rec fib = function | 0 | 1 as n -> n | n -> fib (n - 1) + fib (n - 2) // Scala method returning a function def fib: Int => Int = { case n @ (0 | 1) => n case n => fib(n - 1) + fib(n - 2) } // Same thing without syntactic sugar: def fib = new Function1[Int, Int] { def apply(param0: Int): Int = param0 match { case n @ (0 | 1) => n case n => fib.apply(n - 1) + fib.apply(n - 2) } } 

Ainsi, dans Scala, toutes les fonctions sont des objects implémentant le trait FunctionX , qui définit une méthode appelée apply . Comme montré ici et dans la création de liste ci-dessus, .apply peut être omis, ce qui fait que les appels de fonctions ressemblent aux appels de méthodes.

En fin de compte, tout dans Scala est un object – et une instance d’une classe – et tout object de ce type appartient à une classe, et tout le code appartient à une méthode, qui est exécutée d’une manière ou d’une autre. Même l’exemple dans l’exemple ci-dessus était une méthode, mais a été converti en mot-clé pour éviter certains problèmes il y a un certain temps.

Alors, qu’en est-il de la partie fonctionnelle? F # appartient à l’une des familles les plus traditionnelles de langages fonctionnels. Bien que certaines fonctionnalités ne soient pas importantes pour certains langages fonctionnels, le fait est que F # fonctionne par défaut , pour ainsi dire.

Scala, quant à elle, a été créée dans le but d’ unifier les modèles fonctionnels et les modèles OO, au lieu de simplement les fournir en tant que parties séparées du langage. Le succès dépend de ce que vous considérez comme une functional programming. Voici quelques points sur lesquels Martin Odersky s’est concentré:

  • Les fonctions sont des valeurs. Ce sont des objects – car toutes les valeurs sont des objects dans Scala – mais le concept selon lequel une fonction est une valeur pouvant être manipulée est important, ses racines remontant à l’implémentation originale de Lisp.

  • Forte prise en charge des types de données immuables. La functional programming a toujours porté sur la réduction des effets secondaires sur un programme, les fonctions pouvant être analysées comme de véritables fonctions mathématiques. Donc, Scala a rendu facile de rendre les choses immuables, mais cela n’a pas fait deux choses que les puristes de FP critiquent pour:

    • Cela n’a pas rendu la mutabilité plus difficile .
    • Il ne fournit pas de système d’effets permettant de suivre statistiquement la mutabilité.
  • Prise en charge des types de données algébriques. Les types de données algébriques (appelés ADT, qui représentent également le type de données abstrait, une chose différente) sont très courants dans la functional programming et sont particulièrement utiles dans les situations où l’on utilise couramment le modèle de visiteur dans les langages OO.

    Comme avec tout le rest, les TDA dans Scala sont implémentés comme classes et méthodes, avec des sucres syntaxiques pour les rendre indolores à utiliser. Cependant, Scala est beaucoup plus verbeux que F # (ou d’autres langages fonctionnels d’ailleurs) pour les supporter. Par exemple, au lieu de F # ‘s | pour les déclarations de cas, il utilise le case .

  • Prise en charge de la non-sévérité. La non-ssortingcte signifie uniquement calculer des choses à la demande. C’est un aspect essentiel de Haskell, où il est étroitement intégré au système d’effets secondaires. En Scala, cependant, le soutien de non-rigueur est assez timide et naissant. Il est disponible et utilisé, mais de manière restreinte.

    Par exemple, la liste non ssortingcte de Scala, le Stream , ne prend pas en charge un foldRight vraiment non ssortingct, comme le fait Haskell. De plus, certains avantages de la non-sévérité ne sont obtenus que lorsqu’il s’agit de la langue par défaut, au lieu d’une option.

  • Prise en charge de la compréhension de la liste. En réalité, Scala l’appelle pour compréhension , car la manière dont elle est mise en œuvre est complètement séparée des listes. En termes simples, les compréhensions de liste peuvent être considérées comme la fonction / méthode de map présentée dans l’exemple, mais aussi l’imbrication d’instructions de carte (supports avec flatMap dans Scala) ainsi que le filtrage ( filter ou withFilter dans Scala). sont généralement attendus.

    C’est une opération très courante dans les langages fonctionnels, et la syntaxe est souvent faible, comme dans l’opérateur in de Python. Encore une fois, Scala est un peu plus verbeux que d’habitude.

À mon avis, Scala est incomparable en combinant FP et OO. Il vient du côté OO du spectre vers le côté FP, ce qui est inhabituel. La plupart du temps, je vois les langages de PF avec OO abordés – et cela me semble être abordé. Je suppose que FP sur Scala ressent probablement la même chose pour les programmeurs de langages fonctionnels.

MODIFIER

En lisant d’autres réponses, j’ai réalisé qu’il y avait un autre sujet important: l’inférence de type. Lisp était un langage typé dynamicment, et cela définissait les attentes pour les langages fonctionnels. Les langages fonctionnels modernes de type statique ont tous des systèmes d’inférence de type forts, le plus souvent l’algorithme Hindley-Milner 1 , ce qui rend les déclarations de type essentiellement facultatives.

Scala ne peut pas utiliser l’algorithme Hindley-Milner en raison du support de Scala pour l’ inheritance 2 . Donc, Scala doit adopter un algorithme d’inférence de type beaucoup moins puissant – en fait, l’inférence de type dans Scala est intentionnellement non définie dans la spécification et fait l’object d’améliorations continues (l’amélioration est l’une des plus grandes fonctionnalités de la prochaine version 2.8 de Scala, par exemple).

Cependant, Scala exige que tous les parameters soient déclarés lors de la définition des méthodes. Dans certaines situations, telles que la récursivité, les types de retour pour les méthodes doivent également être déclarés.

Les fonctions dans Scala peuvent souvent avoir leurs types inférés au lieu d’être déclarées, cependant. Par exemple, aucune déclaration de type n’est nécessaire ici: List(1, 2, 3) reduceLeft (_ + _) , où _ + _ est en fait une fonction anonyme de type Function2[Int, Int, Int] .

De même, la déclaration de type des variables est souvent inutile, mais l’inheritance peut en avoir besoin. Par exemple, Some(2) et None ont une Option super-classe commune, mais appartiennent en réalité à des sous-classes différentes. Donc, on déclare habituellement var o: Option[Int] = None pour s’assurer que le bon type est atsortingbué.

Cette forme limitée d’inférence de type est bien meilleure que ce que les langages OO statistiquement typés offrent habituellement, ce qui donne à Scala un sentiment de légèreté et bien pire que les langages de type FP habituellement proposés, ce qui donne à Scala une impression de lourdeur. 🙂

Remarques:

  1. En fait, l’algorithme provient de Damas et Milner, qui l’ont appelé “Algorithme W”, selon le wikipedia .

  2. Martin Odersky a mentionné dans un commentaire que:

    La raison pour laquelle Scala n’a pas d’inférence de type Hindley / Milner est qu’il est très difficile de combiner avec des fonctionnalités telles que la surcharge (la variante ad-hoc, pas les classes de type), la sélection des enregistrements et le sous-typage.

    Il continue en déclarant que ce n’est peut-être pas impossible, et que cela s’est traduit par un compromis. S’il vous plaît, allez sur ce lien pour plus d’informations, et si vous présentez une déclaration plus claire ou, mieux encore, du papier d’une manière ou d’une autre, je vous serais reconnaissant de la référence.

    Permettez-moi de remercier Jon Harrop de l’ avoir cherché, car je supposais que c’était impossible . Eh bien, peut-être que oui, et je n’ai pas trouvé de lien approprié. Notez, cependant, que ce n’est pas l’inheritance seul à l’origine du problème.

F # est fonctionnel – Il permet assez bien OO, mais le design et la philosophie sont néanmoins fonctionnels. Exemples:

  • Fonctions de style Haskell
  • Curry automatique
  • Génériques automatiques
  • Type d’inférence pour les arguments

Il est relativement maladroit d’utiliser F # d’une manière principalement orientée object, de sorte que l’on pourrait décrire l’objective principal d’ intégrer OO dans la functional programming .

Scala est multi-paradigme en mettant l’accent sur la flexibilité. Vous pouvez choisir entre un style FP authentique, une POO et un style procédural en fonction de ce qui convient le mieux actuellement. Il s’agit vraiment d’ unifier les OO et la functional programming .

Il y a plusieurs points que vous pouvez utiliser pour comparer les deux (ou trois). Tout d’abord, voici quelques points importants auxquels je peux penser:

  • Syntaxe
    Syntactiquement, F # et OCaml sont basés sur la tradition de functional programming (espace séparé et plus léger), tandis que Scala est basé sur le style orienté object (bien que Scala le rende plus léger).

  • Intégration d’OO et de FP
    F # et Scala intègrent très bien OO avec FP (car il n’y a pas de contradiction entre les deux!) Vous pouvez déclarer des classes contenant des données immuables (aspect fonctionnel) et fournir aux membres liés au travail avec les données, des interfaces pour abstraction (aspects orientés object). Je ne suis pas aussi familier avec OCaml , mais je pense que cela met plus l’accent sur le côté OO (par rapport à F #)

  • Style de programmation en F #
    Je pense que le style de programmation habituel utilisé dans F # (si vous n’avez pas besoin d’écrire une bibliothèque .NET et que vous n’avez pas d’autres limitations) est probablement plus fonctionnel et que vous utiliseriez les fonctionnalités OO uniquement lorsque vous en avez besoin. Cela signifie que vous regroupez les fonctionnalités à l’aide de fonctions, de modules et de types de données algébriques.

  • Style de programmation à Scala
    Dans Scala, le style de programmation par défaut est plus orienté object (dans l’organisation), cependant vous écrivez toujours (probablement) des programmes fonctionnels, car l’approche “standard” consiste à écrire du code qui évite les mutations.

Quelles sont les principales différences entre les approches adoptées par Scala et F # pour unifier les paradigmes OO et FP?

La principale différence est que Scala essaie de mélanger les paradigmes en faisant des sacrifices (généralement du côté FP) alors que F # (et OCaml) tracent généralement une ligne entre les paradigmes et permettent au programmeur de choisir entre eux pour chaque tâche.

Scala a dû faire des sacrifices pour unifier les paradigmes. Par exemple:

  • Les fonctions de première classe sont une caractéristique essentielle de tout langage fonctionnel (ML, Scheme et Haskell). Toutes les fonctions sont de première classe en F #. Les fonctions membres sont de deuxième classe à Scala.

  • La surcharge et les sous-types empêchent l’inférence de type. F # fournit un grand sous-langage qui sacrifie ces fonctionnalités OO afin de fournir une inférence de type puissante lorsque ces fonctionnalités ne sont pas utilisées (nécessitant des annotations de type lorsqu’elles sont utilisées). Scala pousse ces fonctionnalités partout afin de maintenir une OO cohérente au prix d’une inférence de type partout.

Une autre conséquence est que F # est basé sur des idées testées et éprouvées alors que Scala est pionnière à cet égard. Ceci est idéal pour les motivations derrière les projets: F # est un produit commercial et Scala est en train de programmer des recherches en langage.

En passant, Scala a également sacrifié d’autres caractéristiques essentielles de la planification familiale telles que l’optimisation des appels pour des raisons pragmatiques en raison des limites de leur machine virtuelle de choix (JVM). Cela fait également de Scala beaucoup plus de PO que de FP. Notez qu’il existe un projet visant à mettre Scala en .NET qui utilisera le CLR pour effectuer un véritable TCO.

Quels sont les avantages et les inconvénients de chaque approche? Si, malgré le support du sous-typage, F # peut déduire les types d’arguments de fonction, alors pourquoi ne pas Scala?

L’inférence de type est en contradiction avec les fonctionnalités centrées sur OO telles que la surcharge et les sous-types. F # a choisi l’inférence de type sur la cohérence par rapport à la surcharge. Scala a choisi une surcharge et des sous-types omniprésents sur l’inférence de type. Cela rend F # plus comme OCaml et Scala plus comme C #. En particulier, Scala n’est pas plus un langage de programmation fonctionnel que C # is.

Ce qui est mieux est bien sûr tout à fait subjectif, mais personnellement, je préfère beaucoup la brièveté et la clarté énormes qui découlent d’une inférence de type puissante dans le cas général. OCaml est un langage formidable, mais le manque de surcharge des opérateurs a forcé les programmeurs à utiliser + pour ints, +. pour les flottants, +/ pour les rationnels et ainsi de suite. Encore une fois, F # choisit le pragmatisme par rapport à l’obsession en sacrifiant l’inférence de type pour la surcharge spécifiquement dans le contexte des numériques, non seulement sur les opérateurs arithmétiques, mais aussi sur les fonctions arithmétiques telles que sin . Chaque coin du langage F # est le résultat de compromis pragmatiques soigneusement choisis comme celui-ci. Malgré les incohérences qui en résultent, je pense que cela rend F # beaucoup plus utile.

A partir de cet article sur les langages de programmation:

Scala est un remplacement robuste, expressif et ssortingctement supérieur à Java. Scala est le langage de programmation que j’utiliserais pour une tâche telle que l’écriture d’un serveur Web ou d’un client IRC. Contrairement à OCaml [ou F #], qui était un langage fonctionnel avec un système orienté object, Scala ressemble davantage à un véritable hybride de programmation orientée object et fonctionnelle. (C’est-à-dire que les programmeurs orientés object devraient pouvoir commencer à utiliser Scala immédiatement, en ne récupérant que les parties fonctionnelles de leur choix.)

J’ai d’abord entendu parler de Scala à POPL 2006 lorsque Martin Odersky a donné une conférence à ce sujet. À l’époque, je considérais que la functional programming était ssortingctement supérieure à la programmation orientée object, et je ne voyais donc pas le besoin d’un langage fusionnant la functional programming et la programmation orientée object. (C’était probablement parce que tout ce que j’avais écrit à l’époque étaient des compilateurs, des interprètes et des parsingurs statiques.)

Le besoin de Scala ne s’est pas fait sentir jusqu’à ce que je rédige un HTTPD simultané à partir de zéro pour prendre en charge AJAX depuis longtemps pour yaplet. Afin d’obtenir un bon support multicœur, j’ai écrit la première version en Java. En tant que langage, je ne pense pas que Java soit si mauvais, et je peux apprécier une programmation orientée object bien faite. En tant que programmeur fonctionnel, cependant, le manque de prise en charge (ou de verbose inutile) des fonctionnalités de functional programming (comme les fonctions d’ordre supérieur) me gêne lorsque je programme en Java. Donc, j’ai donné une chance à Scala.

Scala fonctionne sur la JVM, donc je pourrais progressivement transférer mon projet existant dans Scala. Cela signifie également que Scala, en plus de sa propre bibliothèque plutôt grande, a également access à toute la bibliothèque Java. Cela signifie que vous pouvez faire du vrai travail à Scala.

Lorsque j’ai commencé à utiliser Scala, j’ai été impressionné par la manière dont les mondes fonctionnels et orientés object se mélangeaient intelligemment. En particulier, Scala dispose d’un puissant système d’appariement de classes de cas / modèles qui s’attaque à mes expériences avec Standard ML, OCaml et Haskell: le programmeur peut décider quels champs d’un object doivent être compatibles (au lieu d’être forcés de correspondre) sur chacun d’entre eux), et les arguments d’arité variable sont autorisés. En fait, Scala permet même des modèles définis par le programmeur. J’écris beaucoup de fonctions qui fonctionnent sur des nœuds de syntaxe abstraite, et il est intéressant de pouvoir faire correspondre uniquement les enfants syntaxiques, mais il y a toujours des champs pour des éléments tels que des annotations ou des lignes dans le programme d’origine. Le système de classes de cas permet de diviser la définition d’un type de données algébrique en plusieurs fichiers ou en plusieurs parties du même fichier, ce qui est très pratique.

Scala prend également en charge l’inheritance multiple bien défini grâce à des périphériques de type classe appelés traits.

Scala permet également une surcharge considérable; Même les applications de fonctions et les mises à jour de tableaux peuvent être surchargées. Dans mon expérience, cela a tendance à rendre mes programmes Scala plus intuitifs et concis.

Une des caractéristiques qui permet de gagner beaucoup de code, de la même manière que les classes de type enregistrent le code dans Haskell, est implicite. Vous pouvez imaginer des implicites en tant qu’API pour la phase de récupération d’erreur du vérificateur de type. En bref, lorsque le vérificateur de type a besoin d’un X mais a un Y, il vérifiera s’il y a une fonction implicite dans la scope qui convertit Y en X; s’il en trouve un, il “lance” en utilisant l’implicite. Cela permet de donner l’impression que vous étendez n’importe quel type dans Scala, et cela permet un enchaînement plus ssortingct des DSL.

De l’extrait ci-dessus, il est clair que l’approche de Scala pour unifier les paradigmes OO et FP est de loin supérieure à celle d’OCaml ou de F #.

HTH.

Cordialement,
Eric.

La syntaxe de F # a été extraite d’OCaml mais le modèle d’object de F # provient de .NET. Cela donne à F # une syntaxe légère et concise qui est caractéristique des langages de programmation fonctionnels et permet en même temps à F # d’interopérer de manière très fluide avec les langages .NET et les bibliothèques .NET existants à travers son modèle d’object.

Scala fait un travail similaire sur la JVM que F # fait sur le CLR. Cependant, Scala a choisi d’adopter une syntaxe plus Java. Cela peut faciliter son adoption par les programmeurs orientés object, mais pour un programmeur fonctionnel, cela peut sembler un peu lourd. Son modèle d’object est similaire à celui de Java, ce qui permet une interopération transparente avec Java, mais présente des différences intéressantes telles que la prise en charge de traits.

Si la functional programming signifie la programmation avec des fonctions , alors Scala le plie un peu. En Scala, si je comprends bien, vous programmez avec des méthodes plutôt que des fonctions.

Lorsque la classe (et l’object de cette classe) derrière la méthode n’a pas d’importance, Scala vous laissera supposer que ce n’est qu’une fonction. Peut-être qu’un avocat de langue Scala peut élaborer sur cette distinction (si c’est même une distinction), et ses conséquences.