Lors de la lecture du célèbre SICP, j'ai trouvé que les auteurs semblent plutôt réticents à introduire la déclaration de cession au régime au chapitre 3. J'ai lu le texte et le genre de comprendre pourquoi ils le ressentent.
Comme le schéma est le premier langage de programmation fonctionnel que je connaisse quelque chose à propos de quelque chose, je suis un peu surpris qu'il y ait des langages de programmation fonctionnelle (pas de stratagème bien sûr) ne peuvent pas passer sans missions.
Laisser utiliser l'exemple des offres de livre, le bank account
Exemple. S'il n'y a pas de déclaration d'affectation, comment cela peut-il être fait? Comment changer la variable balance
? Je demande donc parce que je sais qu'il y a des langues fonctionnelles soi-disant pures et selon la théorie complète de Turing, cela doit être fait aussi.
J'ai appris C, Java, Python et utilisez beaucoup des missions dans chaque programme que j'ai écrit. C'est donc vraiment une expérience d'ouverture des yeux. J'espère vraiment que quelqu'un peut expliquer brièvement comment les missions sont évitées dans ces fonctions Langages de programmation et quel impact profond (le cas échéant) dans ces langues.
L'exemple mentionné ci-dessus est ici:
(define (make-withdraw balance)
(lambda (amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds")))
Cela a changé le balance
par set!
. Pour moi, cela ressemble beaucoup à une méthode de classe pour modifier l'élément de classe balance
.
Comme je l'ai dit, je ne connais pas de langages de programmation fonctionnelle, donc si je dis quelque chose de mal à leur sujet, n'hésitez pas à signaler.
S'il n'y a pas de déclaration d'affectation, comment cela peut-il être fait? Comment changer la variable de solde?
Vous ne pouvez pas changer de variables sans une sorte d'opérateur d'affectation.
Je demande donc parce que je sais qu'il y a des langues fonctionnelles soi-disant pures et selon la théorie complète de Turing, cela doit être fait aussi.
Pas assez. Si une langue est complète, cela signifie qu'il peut calculer tout ce que tout autre langage complet de Turing peut calculer. Cela ne signifie pas que cela doit avoir toutes les caractéristiques d'autres langues.
Ce n'est pas une contradiction qu'un langage de programmation complète Turing n'a aucun moyen de modifier la valeur d'une variable, à condition que chaque programme pouvant être variables mutables, vous pouvez écrire un programme équivalent qui ne dispose pas de variables mutables (où des moyens "équivalents". que cela calcule la même chose). Et en fait, chaque programme peut être écrit de cette façon.
En ce qui concerne votre exemple: dans une langue purement fonctionnelle, vous ne serez tout simplement pas en mesure d'écrire une fonction qui renvoie un solde de compte différent à chaque appel. Mais vous pourrez toujours réécrire chaque programme qui utilise une telle fonction, d'une manière différente.
Depuis que vous avez demandé un exemple, examinons un programme impératif qui utilise votre fonction de retraite (en pseudo-code). Ce programme permet à l'utilisateur de se retirer d'un compte, de la déposer ou d'interroger le montant de l'argent dans le compte:
account = make-withdraw(0)
ask for input until the user enters "quit"
if the user entered "withdraw $x"
account(x)
if the user entered "deposit $x"
account(-x)
if the user entered "query"
print("The balance of the account is " + account(0))
Voici un moyen d'écrire le même programme sans utiliser de variables mutables (je ne me soucierai pas de transparent référentiellement IO car la question n'était pas à propos de cela):
function IO_loop(balance):
ask for input
if the user entered "withdraw $x"
IO_loop(balance - x)
if the user entered "deposit $x"
IO_loop(balance + x)
if the user entered "query"
print("The balance of the account is " + balance)
IO_loop(balance)
if the user entered "quit"
do nothing
IO_loop(0)
La même fonction pourrait également être écrite sans utiliser de récursivité en utilisant un pli sur l'entrée de l'utilisateur (qui serait plus idiomatique que la récursion explicite), mais je ne sais pas si vous connaissez le pli avec les plis, donc je l'ai donc écrit dans un manière qui n'utilise rien que vous ne connaissez pas encore.
Vous avez raison de ressembler beaucoup à une méthode sur un objet. C'est parce que c'est essentiellement ce que c'est. La fonction lambda
est une fermeture qui tire la variable externe balance
dans sa portée. Avoir plusieurs fermetures qui se ferment sur la même variable externe (s) et que plusieurs méthodes sur le même objet sont deux abstractions différentes pour faire exactement la même chose, et l'une ou l'autre peut être mise en œuvre de l'autre si vous comprenez les deux paradigmes.
La façon dont les langages fonctionnels pure traitent l'état de la triche. Par exemple, dans HASKELLL Si vous souhaitez lire l'entrée d'une source externe (qui est non déterministe, bien sûr et ne donnera pas nécessairement le même résultat deux fois si vous le répétez,) Il utilise un tour de monade pour dire "Nous avons obtenu cette autre variable semblable qui représente l'état de l'ensemble du reste du monde, et nous ne pouvons pas l'examiner directement, mais la lecture d'une entrée est une fonction pure qui prend l'état du monde extérieur et des retours L'entrée déterministe que cet état exact rendra toujours, plus le nouvel état du monde extérieur. " (C'est une explication simplifiée, bien sûr. Lire sur la façon dont cela fonctionne réellement brisera sérieusement votre cerveau.)
Ou dans le cas de votre problème de votre compte bancaire, au lieu d'attribuer une nouvelle valeur à la variable, il peut renvoyer la nouvelle valeur en tant que résultat de la fonction, puis l'appelant doit en faire face dans un style fonctionnel, généralement en recréant toutes les données. Ces références qui valorisent une nouvelle version contenant la valeur mise à jour. (Ce n'est pas aussi volumineux une opération car elle pourrait sonner si vos données sont configurées avec la bonne sorte de structure d'arbres.)
"Les opérateurs d'affectation multiple" est un exemple de caractéristique linguistique qui, d'une manière générale, a des effets secondaires et est incompatible avec certaines propriétés utiles des langues fonctionnelles (telles que l'évaluation paresseuse).
Cela ne signifie toutefois que cette affectation en général est incompatible avec un style de programmation fonctionnel pur (voir cette discussion par exemple), il ne signifie pas non plus que vous ne pouvez pas construire la syntaxe qui permet aux actions qui permettent des actions ressemblent à des missions en général, mais sont implémentées sans effets secondaires. Création de ce type de syntaxe et d'écrire des programmes efficaces, prend du temps, cependant.
Dans votre exemple spécifique, vous avez raison - l'ensemble! opérateur est une mission. C'est non Un opérateur libre d'effet latéral, et c'est un endroit où le schéma se casse avec une approche purement fonctionnelle de la programmation.
En fin de compte, tout langage purement fonctionnel devra se casser avec l'approche purement fonctionnelle, la grande majorité des programmes utiles do ont des effets secondaires. La décision de le faire est généralement une question de commodité et des concepteurs de langues tenteront de donner au programmeur la plus grande flexibilité de décider où briser avec une approche purement fonctionnelle, selon le cas pour leur programme et leur domaine problématique.
Dans une langue purement fonctionnelle, on programmerait un objet de compte bancaire sous forme de fonction de transformateur de flux. L'objet est considéré comme une fonction d'un flux infini de demandes des propriétaires de compte (ou quiconque) à un flux de réponses potentiellement infini. La fonction démarre avec un solde initial et traite chaque requête dans le flux d'entrée pour calculer un nouvel équilibre, qui est ensuite renvoyé à l'appel récursif pour traiter le reste du flux. (Je me souviens que SICP traite du paradigme de transformateur de flux dans une autre partie du livre.)
Une version plus élaborée de ce paradigme s'appelle "Programmation réactive fonctionnelle" discutée ici sur Stackoverflow .
La manière naïve des transformateurs de flux a quelques problèmes. Il est possible (en fait, assez facile) d'écrire des programmes de buggy qui gardent toutes les anciennes demandes autour de l'espace. Plus sérieusement, il est possible de faire la réponse à la demande actuelle dépendent des demandes futures. Des solutions à ces problèmes sont en cours de travail. Neel Krishnaswami est la force derrière eux.
Disclaimer : Je n'appartiens pas à l'Église de la programmation fonctionnelle pure. En fait, je n'appartiens à aucune église :-)
Il n'est pas possible de faire un programme 100% fonctionnel s'il est supposé faire quelque chose d'utile. (Si les effets secondaires ne sont pas nécessaires, alors l'ensemble de la pensée aurait pu être réduit à une heure de compilation constante), comme l'exemple de retrait, vous pouvez apporter la plupart des procédures fonctionnelle, mais que vous aurez finalement besoin de procédures ayant des effets secondaires (entrée de l'utilisateur, sortie à la console). Cela dit, vous pouvez apporter la majeure partie de votre code fonctionnel et que la partie sera facile à tester, même automatiquement. Ensuite, vous faites un code impératif pour effectuer l'entrée/la sortie/la base de données/... qui aurait besoin de déboguer, mais de garder la majeure partie du code propre, cela ne sera pas trop de travail. Je vais utiliser votre exemple de retrait:
(define +no-founds+ "Insufficient funds")
;; functional withdraw
(define (make-withdraw balance amount)
(if (>= balance amount)
(- balance amount)
+no-founds+))
;; functional atm loop
(define (atm balance thunk)
(let* ((amount (thunk balance))
(new-balance (make-withdraw balance amount)))
(if (eqv? new-balance +no-founds+)
(cons +no-founds+ '())
(cons (list 'withdraw amount 'balance new-balance) (atm new-balance thunk)))))
;; functional balance-line -> string
(define (balance->string x)
(if (eqv? x +no-founds+)
(string-append +no-founds+ "\n")
(if (null? x)
"\n"
(let ((first-token (car x)))
(string-append
(cond ((symbol? first-token) (symbol->string first-token))
(else (number->string first-token)))
" "
(balance->string (cdr x)))))))
;; functional thunk to test
(define (input-10 x) 10) ;; define a purly functional input-method
;; since all procedures involved are functional
;; we expect the same result every time.
;; we use this to test atm and make-withdraw
(apply string-append (map balance->string (atm 100 input-10)))
;; no program can be purly functional in any language.
;; From here on there are imperative dirty procedures!
;; A procedure to get input from user is needed.
;; Side effects makes it imperative
(define (user-input balance)
(display "You have $")
(display balance)
(display " founds. How much to withdraw? ")
(read))
;; We need a procedure to print stuff to the console
;; as well. Side effects makes it imperative
(define (pretty-print-result x)
(for-each (lambda (x) (display (balance->string x))) x))
;; use imperative procedure with atm.
(pretty-print-result (atm 100 user-input))
Il est possible de faire la même chose dans presque toutes les langues et de produire les mêmes résultats (moins de bogues), bien que vous puissiez devoir définir des variables temporaires dans une procédure et même la mutation, mais cela n'a pas d'importance tant que la procédure réellement agit fonctionnel (les paramètres à eux-mêmes détergent le résultat). Je crois que vous devenez un meilleur programmeur dans n'importe quelle langue après avoir programmé un peu de LISP :)
L'affectation est une mauvaise opération car elle divise l'espace d'état à deux parties, avant d'assigner et après l'attribution. Cela provoque des difficultés de suivi de la manière dont les variables sont modifiées lors de l'exécution du programme. Les choses suivantes dans les langues fonctionnelles remplacent les affectations: