Aller au contenu

Décidabilité et Calculabilité

Programme

Notions Compétences Remarques
Notion de programme en tant que donnée.
Calculabilité, décidabilité.
Comprendre que tout programme est aussi une donnée.
Comprendre que la calculabilité ne dépend pas du langage de programmation utilisé.
Montrer, sans formalisme thĂ©orique, que le problĂšme de l’arrĂȘt est indĂ©cidable.
L’utilisation d’un interprĂ©teur ou d’un compilateur, le tĂ©lĂ©chargement de logiciel, le fonctionnement des systĂšmes d’exploitation permettent de comprendre un programme comme donnĂ©e d’un autre programme.

Programme en tant que donnée

Les théories de la calculabilité et de la complexité sont deux pans de la théorie du calcul :

Que veut dire calculer ? Un ordinateur peut-il tout calculer ?

C'est en 1936 que Allan Turing (1912-1954) a apporté des réponses à ces questions, et la conclusion de ses travaux... va vous étonner :)

Nous allons tout d'abord expliciter un point important qui sera le fondement de la théorie de la calculabilité : un programme est aussi une donnée.

Cela peut paraßtre étonnant à premiÚre vue puisqu'on est habitué à traiter :

  • les programmes dans des fonctions
  • les donnĂ©es dans des variables

Fonctions et variables sont des objets de nature différente en apparence : si on se raccroche à ce que l'on connaßt en python, une fonction se déclare avec le mot clé def et une variable s'initialise avec l'opérateur d'affectation =.

Prenons en exemple l'algorithme d'Euclide, un algorithme vieux de plus de 2500 ans permettant de calculer le PGCD de 2 nombres. On peut l'Ă©crire Ă  l'aide d'une fonction Python:

def euclide(a, b):
    if a < b: 
        a, b = b, a
    while b: 
        a, b = b, a%b
    return a

Dans ce programme Python, euclide est une fonction et a et b sont des donnĂ©es. Ils ne semblent pas ĂȘtre de nature comparable :

>>> euclide(35, 49)
7

Et pourtant, Ă  y regarder de plus prĂšs, notre algorithme programmĂ© dans la fonction euclide n'est rien d'autre qu'une succession de caractĂšres. On peut mĂȘme pousser la rĂ©flexion jusqu'Ă  crĂ©er une chaĂźne de caractĂšre contenant ce programme :

mon_programme = "def euclide(a, b):\n\tif a < b: a, b = b, a\n\twhile b: a, b = b, a%b\n\treturn a"

Maintenant mon algorithme est devenu une variable. On peut alors construire une machine universelle capable d'évaluer n'importe quelle donnée contenant un algorithme formalisé dans le langage Python :

def universel(algo, *args):
    exec(algo)
    ligne1 = algo.split('\n')[0]
    nom = ligne1.split('(')[0][4:]
    return eval(f"{nom}{args}")

A présent je peut invoquer ma machine universelle en lui passant en données :

  • la variable contenant mon algorithme
  • les arguments sur lequel celui-ci va travailler et obtenir la rĂ©ponse :
    >>> universel(mon_programme,35,49)\
    7
    

Dans l'exemple ci-dessus, vous pouvez constater que le programme et les donnĂ©es sur lesquelles il agit sont de mĂȘme nature : ce sont 3 variables passĂ©es en paramĂštres Ă  ma fonction universelle.

En conclusion

Un programme est aussi une donnée.

Activité 1 - Le programme comme donnée

Citez d'autres exemples oĂč un programme est considĂ©rĂ© comme une donnĂ©e?

Correction

Les programmes qui manipulent d'autres programmes en tant que données sont courants. On peut citer :

  • Les compilateurs qui prennent en entrĂ©e un texte et le transforment en une suite de 0 et 1 exĂ©cutable par le microprocesseur de l'ordinateur
  • L'interprĂ©teur python fait de mĂȘme, c'est d'ailleurs lui que nous avons mis Ă  contribution dans notre machine universelle
  • un systĂšme d'exploitation peut ĂȘtre vu comme un programme qui fait "tourner" d'autres programmes
  • pour tĂ©lĂ©charger un logiciel on utilise un gestionnaire de tĂ©lĂ©chargement qui est lui-mĂȘme un logiciel.
  • Le navigateur internet en est un autre exemple : il reçoit un lot de donnĂ©es d'internet via le protocole http et interprĂšte certaines d'entre elles comme programme (javascript) et d'autres comme donnĂ©es (html) etc...

Décidabilité

Une propriĂ©tĂ© est dĂ©cidable si l'on peut dĂ©terminer en un nombre fini d'Ă©tapes si elle est vraie ou fausse quel que soit le contexte de dĂ©part. (On parle de problĂšme de dĂ©cision, Ă  rĂ©ponse oui ou non) Attention, cela ne veut pas dire que la propriĂ©tĂ© doit ĂȘtre toujours fausse ou vraie. Donnons, pour illustrer la dĂ©finition, des problĂšmes dĂ©cidables.

  • Savoir si un nombre est premier est dĂ©cidable. La rĂ©ponse sera soit 'vrai', soit 'faux' et un algorithme simple est de diviser ce nombre par les entiers infĂ©rieurs Ă  lui mĂȘme. Il y a donc un nombre fini d'Ă©tapes et une rĂ©ponse qui est soit vrai soit faux.
  • Dire si un nombre est pair (on regarde le reste de la division Euclidienne par 2)

Les exemples ne manquent pas. On peut alors se poser la question: "tout est-il décidable?". La réponse est non. Si c'était le cas, cela voudrait dire que l'on pourrait prouver qu'une propriété mathématique est vraie ou fausse avec un algorithme !

Donnons des exemples de problĂšmes non dĂ©cidables. Je parcours un rĂ©seau alĂ©atoirement, est-ce que je vais atteindre une cible donnĂ©es en un nombre fini d'Ă©tapes? Pas forcĂ©ment, mĂȘme si la probabilitĂ© d'arriver Ă  destination tend vers 1 quand le nombre d'Ă©tapes tend vers l'infini!

Un autre exemple plus connu: le problĂšme de l'arrĂȘt d'un programme est-il dĂ©cidable? Est-ce que je peux Ă©crire un programme qui me dira si un programme va s'arrĂȘter ou non (selon les valeurs d'entrĂ©es)? Nous verrons que l'on peut prouver que ce problĂšme n'est pas dĂ©cidable: il n'existe pas d’algorithme capable de prĂ©dire si n'importe quel programme va s'arrĂȘter ou non. Cela vous plairez bien...n'avez vous jamais fait de boucle infinie?

Je rebondis aussi sur la phrase "Ă©crire un programme qui me dira si un programme va s'arrĂȘter ou non". Cela signifie que je considĂšre que le programme Ă  tester est une donnĂ©e du programme testeur. Est-ce choquant? Non, lorsque vous utilisez l'idle de python, vous tapez un programme, qui va s'executer grace au programme de Python (votre programme est dĂ©jĂ  une donnĂ©e puisque vous l'enregistrez), qui lui mĂȘme est Ă©crit en C et un programme sert Ă  compiler le C en langage assembleur puis va l'Ă©crire en pur code binaire (suite de 0 et de 1). Autre remarque lorsque qu'un compilateur s'arrĂȘte pour vous indiquer une erreur, ou qu'un Ă©diteur de texte destinĂ© Ă  la programmation vous indique qu'il y a une erreur dans votre programme....c'est bien un programme qui vĂ©rifiĂ© votre programme. Bref des programmes qui ont pour donnĂ© des programmes c'est trĂšs frĂ©quent !

Calculabilité

La calculabilité est la branche de l'algorithmique qui s'intéresse aux questions suivantes

  • Peut-on tout calculer Ă  l'aide d'un ordinateur ?
  • Que signifie calculer Ă  l'aide d'un ordinateur ?

Un programme est aussi une donnée. Il est donc manipulable par des algorithmes. Peut-on concevoir un algorithme permettant de savoir si un programme passé en paramÚtre :

  • va se terminer ? c'est Ă  dire renvoyer un rĂ©sultat
  • va provoquer une erreur ?
  • est correct et ne contient pas de bugs ?

Ce sont des questions fondamentales au coeur de l'algorithmique et de l'informatique en gĂ©nĂ©ral. Imaginez un programme capable de dire si un autre programme est correct ou bugguĂ© ! vous commencez Ă  sentir que cela ne va pas ĂȘtre possible, hĂ©las ...

ThéorÚme fondamental de la calculabilité

Il existe des problĂšmes non calculables par des fonctions.

L'argument central pour justifier cette affirmation peut s'Ă©noncer en deux phrases :

  • l'ensemble des algorithmes que l'on peut programmer par des fonctions (en python par exemple) est dĂ©nombrable puisqu'une fonction est une suite finie de caractĂšres
  • l'ensemble des problĂšmes est un ensemble indĂ©nombrable (cela peut ĂȘtre dĂ©montrĂ©) Il n'y a donc tout simplement pas assez d'algorithmes pour rĂ©soudre tous les problĂšmes.

Pour rappel, un infini dénombrable est un infini que l'on peut compter : \(\mathbb{N}\), \(\mathbb{Z}\), \(\mathbb{Q}\) sont dénombrables par opposition à \(\mathbb{R}\) qui est indénombrable. Il est impossible de numéroter les nombres réels. L'infini indénombrable des nombres réels est bien plus grand que l'infini dénombrable des entiers.

De mĂȘme que la majoritĂ© des nombres sont des nombres rĂ©els (bien plus nombreux que les entiers), la majoritĂ© des problĂšmes ne sont pas calculables !

Le problĂšme de l'arrĂȘt

Le premiĂšr problĂšme explicite non calculable a Ă©tĂ© dĂ©crit par Turing en 1936 : Il s’agit du problĂšme de l’arrĂȘt qui s'Ă©nonce ainsi : Ă©tant donnĂ© un algorithme A et une prenant en paramĂštre m, existe t-il un algorithme permettant de dĂ©cider si A(m) s'arrĂȘte ou pas.

C'est une question cruciale et pourtant, elle est indĂ©cidable : Il n'existe aucun moyen de savoir en gĂ©nĂ©ral si un algorithme quelconque va s'arrĂȘter ou non.

La conjecture de Syracuse

Un exemple de cette indécision est la conjecture de Syracuse, encore non résolue par les mathématiciens à ce jour. Et pourtant son énoncé est trÚs simple :

Soit un entier \(n\). On définit la suite \(u_{n}\) par récurrence ainsi :

  • si \(n\) est pair, \(u_{n+1}=\dfrac{n}{2}\)
  • si \(n\) est impair, \(u_{n+1}=3n+1\)

\(u_{0}\) étant donné, on peut ainsi calculer les termes de proche en proche. Par exemple si on part de 7, on obtient la suite suivante :

7, 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1

Les derniers termes sont 4, 2, 1. On s'arrĂȘte ici dans l'Ă©numĂ©ration des termes car cela constitue un cycle. En effet 1 est impair donc on fait \(3 \times 1+1=4\) ce qui tourne en boucle.

A ce jour, tous les nombres essayés conduisent inévitablement à ce cycle 4, 2, 1 mais nul n'a été capable de le démontrer.

S'il existait une solution au problĂšme de l'arrĂȘt, alors la conjecture de Syracuse serait rĂ©solue car on saurait prĂ©dire l'arrĂȘt de l'algorithme. Ce n'est pas le cas.

Activité 2 - Fonction Syracuse

Programmez cet algorithme en Python. Vous Ă©crirez une fonction syracuse prenant en paramĂštre un nombre et renvoyant la liste des termes de la suite jusqu'Ă  ce qu'elle atteigne 1.

Correction
def syracuse(n):
    L = [n]
    while n != 1:
        if n%2 == 0:
            n = n//2
        else:
            n = 3*n+1
        L.append(n)
    return L

syracuse(7)

Preuve de Turing

Pour prouver que le problĂšme de l'arrĂȘt n'est pas calculable, Turing en 1936 a fait ce raisonnement :

On raisonne par l'absurde et on suppose qu'il existe une fonction arrĂȘt qui prend en paramĂštres un algorithme A et des arguments m (on se rappelle qu'un algorithme est une donnĂ©e). Cela pourrait prendre cette forme en Python :

def arret(A, m):
   ... # Tout est ici !
   if ...:
      return True 
   else:
      return False

Le vrai problĂšme est bien sĂ»r de complĂ©ter les ... mais on suppose ici que quelqu’un a su le faire.

On peut alors créer un paradoxe à l'aide de l'algorithme suivant (écrit en pseudo python approximatif...):

def paradoxe():
   if arret(paradoxe): # paradoxe est une donnĂ©e passĂ©e en paramĂštre Ă  arrĂȘt
       while True:
          pass # on fait une boucle infinie si paradoxe s’arrĂȘte
    else:
       return True # On s'arrĂȘte si paradoxe ne s'arrĂȘte pas

Que donne la fonction arret(paradoxe) (on cherche Ă  savoir si paradoxe s'arrĂȘte) ?

  • Si paradoxe s'arrĂȘte, alors paradoxe ne s'arrĂȘte pas car on rentre dans la boucle infinie while True.
  • Si paradoxe ne s'arrĂȘte pas, alors paradoxe s'arrĂȘte car le return True met fin au programme.

Il est donc impossible de connaĂźtre le rĂ©sultat de l'appel arret(paradoxe, paradoxe) car il ne peut valoir True et False en mĂȘme temps !

Cette contradiction montre donc que le problĂšme de l'arrĂȘt est indĂ©cidable. Il en est de mĂȘme pour la majoritĂ© des problĂšmes en algorithmique !

Retrouvez cette preuve dans cette vidéo (en anglais, facile à comprendre) :

Activité 3 - Division par 0

Démontrez qu'il est impossible de créer une fonction div0 qui détecte les divisions par 0 dans une fonction.

Correction

Si une telle fonction existe :

def div0(A, m) :
    ...
    if division par zero existe :
        return True
    else :
        return False

Au moment d’exĂ©cuter ce programme, il faut passer la ligne if division par zero existe , et donc tester dans le programme A un calcul menant Ă  une division par 0, ce qui mathĂ©matiquement renvoie forcĂ©ment une erreur, et donc interrompt le programme div0.

Conclusion

La dĂ©tection de bugs (comme des boucles infinies par exemple) est une activitĂ© cruciale en informatique. Il existe des programmes capables de dĂ©tecter des erreurs dans d'autres programmes : les environnements de dĂ©veloppement modernes comme VScode ou PyCharm sont capable de souligner vos erreurs en python alors mĂȘme que vous tapez le programme, et cela constitue un grand gain de temps pour le dĂ©veloppeur.

En France, depuis l'accident du vol 501 d'Ariane 5 en 1996 dû à une erreur de programmation, le développement de programmes de preuves de correction a connu une forte croissance.

Un bel exemple de progrÚs réalisé grùce à ces programmes est le logiciel de la ligne de métro automatique 14 (Météor) à Paris: cette ligne a été mise en service sans test en condition réelles : son programme a été mathématiquement prouvé sans faute. Depuis sa mise en service, aucun bugs n'a été à déplorer.

Malheureusement, un algorithme gĂ©nĂ©ral permettant de prouver qu'un programme fonctionne n'existe pas, cela fait partie des trĂšs nombreux problĂšmes indĂ©cidables. Il n'y aura donc jamais de systĂšme permettant de s'assurer que n'importe quel programme est fiable, mĂȘme si c'est rĂ©alisable pour quelques exemples particuliers comme le MĂ©tĂ©or.