Beancount ou la comptabilité pour les hackers

En matière de comptabilité personnelle, l'offre en logiciels et autres applications pour smartphones ne manque pas, et pourtant après de nombreuses tentatives je n'ai rien trouvé qui me convenait: les solutions existantes étaient toujours ou trop simplistes ou trop complexes, trop chères, pas assez ergonomiques, trop instables, et j'en passe...

Sur le point de développer ma propre application, j'ai finalement découvert Beancount, qui s'approche de la perfection, en tout cas pour mes besoins. Je vais donc présenter dans cet article le principe de Beancount, et comment l'utiliser pour gérer ses comptes.

Beancount est basé sur la comptabilité en partie double, et repose sur l'enregistrement des transactions dans un simple fichier texte: il est donc possible de tout faire en ligne de commande, ce qui ravira les fans de Linux, et fera certainement fuir les autres utilisateurs habitués aux interfaces graphiques 😉 . Beancount fournit entre autres une interface Web pour explorer ses comptes, un langage de requêtes similaire à SQL,  et permet d'écrire ses propres plugins et scripts en Python pour étendre les fonctionnalités.

La comptabilité en partie double

Avant de comprendre Beancount, il faut d'abord maîtriser les bases de la comptabilité en partie double. Il s'agit de la méthode utilisée pour effectuer la comptabilité d'une entreprise, d'une organisation, ou d'un État dans les règles de l'art, mais on peut très bien l'appliquer pour gérer ses comptes personnels.

Son principe est assez simple: toute transaction (achat, vente, virement, etc.) est représentée par un transfert d'argent entre deux (ou plus) comptes. La notion de compte est ici à prendre au sens large: il peut s'agir d'un compte bancaire, d'un compte de caisse, mais aussi d'un poste de dépenses, d'un compte représentant des stocks, etc.

La règle absolue à respecter dans la comptabilité en partie double est que tout sortie d'argent doit être compensée par une rentrée équivalente (et vice-versa). Par exemple, supposons que vous achetez un lot de ramettes de papier pour 50 euros: votre compte bancaire sera donc diminué de 50 euros, et en contrepartie il faut augmenter votre compte "Fournitures de bureau" de 50 euros. De même, si vous recevez un salaire de 2000 euros, vous augmentez votre compte bancaire de 2000 euros, et en contrepartie vous devez donc diminuer votre compte "Salaires" du même montant.

A ce stade, voici comment on peut enregistrer ces opérations en comptabilité:

Date Compte Libellé Montant
2015-05-12 Banque Achat papier -50 EUR
2015-05-12 Fournitures de bureau Achat papier +50 EUR
2015-05-30 Banque Salaire mai 2015 +2000 EUR
2015-05-30 Salaire Salaire mai 2015 -2000 EUR

Grâce au principe de partie double, c'est-à-dire l'écriture de toute transaction dans deux comptes simultanément, la somme de toutes les opérations est toujours nulle, ce qui permet de s'assurer facilement qu'il n'y a pas d'erreur dans les comptes.

Il peut sembler peu intuitif que le compte "Salaire" soit négatif, puisque c'est de l'argent gagné, mais il suffit de se souvenir que les comptes doivent s'équilibrer, donc si le compte "Banque" augmente, le compte "Salaire" doit diminuer. Une autre façon de voir les choses est de considérer l'argent du salaire comme la contepartie d'un travail: le salarié reçoit de l'argent sur son compte en banque en échange du travail qu'il donne à son employeur. Puisque le travail est donné, c'est donc quelque chose qui "sort", et par conséquent il doit être comptabilisé en négatif.

Après avoir enregistré toutes les opérations, on peut calculer le solde de chaque compte, en additionant tous les montants affectés à ces comptes depuis leur ouverture:

Compte Solde
Banque +1950 EUR
Fournitures de Bureau +50 EUR
Salaire -2000 EUR

Si vous avez compris les explications précédentes, il doit être clair que la somme des soldes de tous les comptes doit être égale à 0, ce qui est une bonne façon de vérifier qu'il n'y a pas d'erreurs.

Pacioli

Actif, Passif, Dépenses et Recettes

Pour organiser la comptabilité, les différents comptes sont répartis en quatre catégories: actif, passif, recettes et dépenses, qu'on peut définir ainsi:

  • Actif: l'argent qu'on possède
  • Passif: l'argent qu'on doit
  • Dépenses (ou Charges): l'argent qui sort
  • Recettes (ou Produits): l'argent qui rentre

Si on reprend l'exemple précédent, on voit facilement que:

  • Le compte "Banque" est un compte d'actif
  • Le compte "Fournitures de bureau" est un compte de dépenses
  • Le compte "Salaire" est un compte de recettes

Les comptes de passif sont utilisés par exemple lorsqu'on achète un bien à crédit. Supposons que vous achetez vos ramettes de papier le 12 mai, et que vous payez le fournisseur (METRO par exemple) une semaine plus tard. Dans ce cas, il y a deux transactions à enregistrer dans la comptabilité:

Date Compte Libellé Montant
2015-05-12 Magasin METRO Achat papier facture 123456 -50 EUR
2015-05-12 Fournitures de bureau Achat papier facture 123456 +50 EUR
2015-05-19 Banque Paiement facture METRO 123456 -50 EUR
2015-05-19 Magasin METRO Paiement facture METRO 123456 +50 EUR

Le compte "Magasin METRO" (compte de fournisseur) est ici un compte de passif, car il correspond à une dette.

Si on fait la somme des opérations, on voit que le tout revient à transférer 50 euros du compte "Banque" vers le compte "Fournitures de bureau", car les opérations sur le compte "Magasin METRO" s'annulent une fois que la facture a été payée. Un des intérêts de la comptabilité en partie double est qu'elle permet de comptabiliser correctement ce genre de dettes, même si elles ne sont que temporaires. Dans le cas d'un crédit immobilier, la dette peut durer de nombreuses années donc il est encore plus important d'en garder une trace !

Pour être tout à fait complet, il faut préciser que le passif se compose en réalité de deux parties: les Dettes et le Capital. Dans la comptabilité anglo-saxonne, le capital est une catégorie de compte à part entière, alors que dans la comptabilité française, dettes et capital sont regroupés sous le même terme de "passif". La notion de capital n'est pas forcément intuitive, disons pour faire simple que dans le cas d'une entreprise, le capital représente la valeur de cette entreprise, c'est-à-dire l'actif moins les dettes. Dit autrement, le capital représente la dette de l'entreprise vis-à-vis de ses propres actionnaires (ou associés), c'est d'ailleurs pourquoi il est classé parmi les comptes de passif.

En pratique, il suffit de se souvenir que l'actif doit toujours être égal au passif, car tout ce qu'une société possède est une dette envers quelqu'un d'autre (envers les créanciers ou les associés). Le capital peut donc être considéré comme une "astuce" technique pour équilibrer les comptes: si l'actif est supérieur aux dettes (ce que je vous souhaite), la différence entre les deux se retrouve automatiquement au capital.

Pour finir, il est utile de remarquer que les comptes d'actif et de passif permettent d'évaluer l'état des finances d'une entreprise à n'importe quel instant, et sont appelés comptes de bilan. D'un autre côté, les comptes de recettes et de dépenses traduisent les mouvements financiers de l'entreprise sur une période donnée, et sont appelés comptes de gestion. Dans une entreprise, les comptes de gestion sont remis à zéro (soldés) chaque année lors de la clôture des comptes, et au passage le solde restant (appelé résultat de l'exercice comptable, ou également bénéfice lorsqu'il est positif) est transféré sur un compte de capital.

Débit et crédit

L'approche que j'ai décrite jusqu'ici (et qui est celle utilisée dans Beancount) pourra sembler surprenante à ceux qui ont déjà des notions de comptabilité.

En réalité, les comptables ne représentent pas les transferts entre comptes par des montants positifs ou négatifs, mais utilisent les notions (peu intuitives au premier abord) de débit et crédit:

  • Un débit représente une entrée vers un compte
  • Un crédit représente une sortie depuis un compte

Dans une écriture comptable, on note les débits et crédits dans deux colonnes différentes, le débit étant toujours à gauche, et le crédit à droite, et on utilise seulement des nombres positifs.

Si on reprend notre exemple initial, les transactions s'écrivent dans un comptabilité traditionnelle sous la forme suivante:

Date Compte Libellé Débit Crédit
2015-05-12 Banque Achat papier 50 EUR
2015-05-12 Fournitures de bureau Achat papier 50 EUR
2015-05-30 Banque Salaire mai 2015 2000 EUR
2015-05-30 Salaire Salaire mai 2015 2000 EUR

Le principe est le même que précédemment, mais au lieu d'utiliser des montants positifs pour les entrées d'argent, on écrit les montants dans la colonne "débit", et au lieu d'utiliser des montants négatifs pour les sorties, on écrit les montants dans la colonne "crédit", toujours en utilisant des nombres positifs.

Avec cette méthode, le calcul du solde des comptes est plus compliqué, puisqu'il faut manipuler des débits et des crédits au lieu de faire une simple addition. Si la somme des débits est supérieure à la somme des crédits, le solde du compte est débiteur, et si la somme des crédits est supérieure à la somme des débits, le solde du compte est créditeur.

Avec notre exemple, on obtient:

Compte Total débits Total crédits Solde débiteur Solde créditeur
Banque 1950 EUR 1950 EUR
Fournitures de Bureau 50 EUR 50 EUR
Salaire 2000 EUR 2000 EUR

Tout serait bien plus simple en se limitant à l'utilisation des nombres positifs et négatifs (comme le fait Beancount), mais malheureusement des siècles d'histoire de la comptabilité en ont décidé autrement !

Pour ajouter à la confusion, la terminologie est exactement l'inverse de ce que vous pouvez trouver chaque mois sur votre relevé bancaire, puisqu'un compte en banque est toujours censé être créditeur et non débiteur ! En fait le paradoxe s'explique facilement: du point de vue de votre banque, l'argent disponible sur votre compte est de l'argent qu'elle vous doit, c'est donc un crédit pour elle. Par contre de votre point de vue, la situation est exactement l'inverse et cet argent est donc compté comme un débit.

À noter que les comptes d'actif et de dépenses sont en principe toujours débiteurs (donc "positifs"), et les comptes de passif et de recettes sont en principe toujours créditeurs (donc "négatifs").

Plan de comptes

Le dernier point à aborder est la notion de plan de comptes. C'est tout simplement la liste de tous les comptes utilisés pour effectuer la comptabilité.

En France, il est obligatoire de suivre une réglementation appelée Plan Comptable Général (PCG) pour faire la comptabilité d'une entreprise. Le PCG définit un plan de compte standard, qui classe les différents comptes en 7 catégories principales, et attribue un numéro à chaque type de compte. Le compte "Fournitures consommables" porte par exemple le numéro 6022, et il contient (entre autres) un sous-compte "Fournitures de bureau" qui porte le numéro 60225.

Pour une comptabilité personnelle, il est inutile de suivre les recommendations du PCG, mais il faut tout de même réfléchir à la liste des comptes qu'on doit utiliser, et à la manière des les classer.

Dans Beancount, chaque compte doit être affecté à une des 5 catégories suivantes:

  • Assets (Actif)
  • Liabilities (Passif)
  • Equity (Capital)
  • Income (Recettes)
  • Expenses (Dépenses)

Comme expliqué précédemment, le Capital est une catégorie à part dans le système anglo-saxon, qui est utilisé par Beancount.

Par défaut ces noms sont en anglais dans Beancount, mais il est possible de définir une option pour les renommer en français (ou en n'importe quoi).

Un plan de compte utilisable dans Beancount pourrait ressembler à ceci:

  • Actif:LCL:CompteCheques
  • Actif:LCL:LivretA
  • Passif:LCL:CreditImmobilier
  • Recettes:Salaire
  • Recettes:Loyer
  • Depenses:Charges:RemboursementCredit
  • Depenses:Charges:Electricite
  • Depenses:Charges:Telephone
  • Depenses:Charges:Imports
  • Depenses:Alimentation
  • Depenses:Loisirs
  • Depenses:Sante
  • Depenses:Vetements
  • Depenses:Transports
  • Depenses:Transports:Voiture
  • Depenses:Logement
  • Depenses:Autres

L'utilisation du séparateur ":" permet de définir une hiérarchie de comptes, par exemple "Depenses:Transport:Voiture" est un sous-compte du compte "Depenses:Transport". Ce n'est pas obligatoire, mais permet de mieux s'y retrouver quand on utilise un grand nombre de comptes (l'exemple précédent est très simplifié; j'utilise personnellement une centaine de comptes différents).

Une petite limitation de Beancount est qu'il n'est pas possible d'utiliser des espaces dans un nom de compte, il faut donc utiliser des tirets à la place, ou coller les mots comme je l'ai fait dans l'exemple.

Fonctionnement général de Beancount

Grâce à ces notions de base de comptabilité, il est maintenant possible de rentrer dans le vif du sujet, à savoir le fonctionnement de Beancount.

Contrairement à la majorité des logiciels de comptabilité, Beancount se lance en ligne de commande, et n'a donc pas besoin d'interface graphique. Son auteur, Martin Blais, s'est inspiré de Ledger, qui fonctionne sur le même principe.

L'idée principale est d'utiliser un langage spécifique (un Domain Specific Language ou DSL, en anglais) pour décrire ses transactions dans un simple fichier texte. Une fois que ce fichier est écrit (avec n'importe quel éditeur de texte), Beancount fournit différents outils pour en tirer divers rapports et statistiques, sans jamais modifier le fichier original. Il est également possible d'accéder à certains rapports avec une interface Web, qui est très pratique pour naviguer dans les comptes, mais ne permet pas de toute façon de modifier le fichier de comptes.

Le format utilisé pour décrire les transactions a été conçu pour être facilement manipulable à la fois par un humain ou par un programme, contrairement à la quasi-totalité des autres formats existants.

Il est temps de passer à la pratique ! En langage Beancount, notre exemple d'achat de papier s'écrit comme ceci:

Le format est extrêmement simple: chaque transaction commence par une ligne avec la date et une description, puis en dessous on écrit les opérations pour les différents comptes impactés par la transaction. On remarque qu'il n'est pas question ici de débits et crédits: la notation utilise des montants positifs et négatifs comme expliqué dans l'introduction.

Étant donné que la somme des écritures d'une même transaction doit toujours être nulle, il est possible de laisser Beancount calculer automatiquement le montant d'une des écritures, ce qui donne la forme simplifiée suivante:

En fait avant d'enregistrer des transactions, il faut d'abord ouvrir les comptes utilisés, grâce à la directive "open". Par exemple on ouvre le compte "Banque" comme ceci:

L'ordre des écritures dans le fichier n'a pas d'importance, par contre les dates doivent être cohérentes, il faut donc évidemment que le compte soit ouvert avant d'apparaître dans une transaction.

Voici maintenant un exemple complet de fichier Beancount, qu'on appelera par exemple "compta.beancount":

Les options ne sont pas nécessaires si vous souhaitez garder les noms par défaut en anglais.

Cet exemple montre également une fonctionnalité extrêmement pratique de Beancount, qui est le remplissage automatique des "trous" dans la comptabilité, grâce aux directive "pad" et "balance":

  • "balance" permet d'indiquer le solde connu d'un compte à une certaine date
  • "pad" permet d'insérer automatiquement une écriture pour que le solde du compte soit respecté

Dans cet exemple, la directive "pad" est équivalente à la transaction suivante:

Le solde initial du compte est donc calculé automatiquement, alors qu'on ne connaissait que le solde final.

Au passage on remarque que le solde initial du compte "Banque" est pris depuis le compte "Capital:SoldeOuverture", qui est dédié à ce genre d'opérations. En effet l'argent doit toujours venir de quelque part, et ce compte représente le capital que l'on possédait avant de démarrer sa comptabilité sous Beancount.

Voilà, maintenant que nous avons un fichier Beancount, il faut bien faire quelque chose avec ! Pour commencer, nous pouvons utiliser la commande "bean-report" pour produire différents rapports, par exemple le solde courant de chaque compte, ce qu'on appelle la balance des comptes:

On peut aussi par exemple afficher le journal (l'ensemble des écritures) d'un compte donné, comme ceci:

La façon la plus simple d'explorer ses comptes reste encore l'interface Web, qui se lance avec la commande "bean-web":

Par défaut, l'interface est accessible à l'adresse http://127.0.0.1:8080/. Elle permet de visualiser le journal, le bilan, la balance des comptes, et le compte de résultat, éventuellement par année.

Voici par exemple le bilan (balance sheet en anglais) de notre exemple pour l'année 2015, c'est-à-dire le tableau récapitulant le solde de tous les comptes d'actif et passif:

beancount-bilan

Importation automatique des transactions

Bien sûr il n'est pas très pratique de rentrer à la main toutes les transactions, surtout en l'absence d'interface graphique pour faciliter la tâche.

La solution la plus pratique consiste à exporter ses comptes depuis le site de sa banque (au format CSV, Quicken, OFX, ou autre) et de convertir les fichiers automatiquement au format Beancount, par exemple grâce à l'outil LedgerHub (du même auteur).

Il est toujours possible de rectifier le fichier obtenu après coup, par exemple pour ajouter des remarques ou éliminer des doublons.

Requêtes personnalisées avec bean-query

Si on veut obtenir des rapports plus précis, il est possible d'utiliser l'outil "bean-query", qui fournit un langage de requêtes similaire à SQL:

Si on veut par exemple retrouver toutes les transactions dont la description contient le texte "123456" (un numéro de facture en l'occurence) pendant l'année 2015, on peut lancer la requête suivante:

On peut aussi regrouper les résultats par compte, en affichant uniquement la somme des écritures pour chaque compte:

Avec cet exemple, on vérifie ainsi que le compte "Passif:MagasinMETRO" a un solde nul, ce qui signifie que la facture a bien été payée.

Les possibilités sont sans fin, il ne reste plus qu'à faire preuve d'imagination !

Scripts et plugins en Python

Pour finir, voyons comment écrire ses propres programmes en utilisant l'API Python de Beancount.

Rien ne vaut un exemple, donc voici un petit programme Python qui ouvre un fichier Beancount, et affiche la date et la description de toutes les transactions:

L'exécution du programme produit le résultat suivant:

Et voilà ! J'espère vous avoir donné un bon aperçu des possibilités de Beancount, et pour plus de détails il vous reste à lire la documentation sur le site du projet !

Introduction à Docker

Depuis son lancement en 2013, Docker est un projet qui fait beaucoup parler de lui, et pour de bonnes raisons !

docker

Ceci est une révolution

En très très bref, Docker est un outil permettant de déployer facilement des applications sur un serveur Linux.

En un peu moins bref, Docker fournit un moyen d'encapsuler des applications dans des containers isolés, et de démarrer ces containers sur n'importe quel machine Linux, quelle que soit la distribution installée. En ce sens, Docker couvre les mêmes besoins qu'une machine virtuelle (VM), mais comme nous allons le voir, il y a tout de même des différences importantes entre containers et VMs.

Alors où est la révolution dans tout ça ? Après tout, n'importe quelle distribution Linux permet d'installer une nouvelle application en quelques clics, ou avec une simple ligne de commande. Mais que se passe-t-il si l'application qu'on souhaite installer n'est pas disponible dans la distribution, ou si seulement une vieille version est disponible ? Il faut alors récupérer un package par ses propres moyens, et c'est généralement là que les ennuis commencent:

  • Le package peut avoir des dépendances incompatibles avec les autres packages déjà installés, et c'est l'impasse...
  • Pire, il se peut qu'aucun package ne soit disponible, il faut alors télécharger le code source et passer des heures à essayer de compiler l'application soi-même (avec un fort risque de retomber sur les problèmes de dépendances)
  • En supposant que l'application démarre, elle n'a jamais été testée et validée sur sa propre machine, et donc des nouveaux bugs peuvent apparaitre. Même avec de la bonne volonté, l'auteur de l'application peut avoir beaucoup de mal à reproduire (et donc à corriger) le problème puisque son environnement de travail est différent.

Containers et union mount

Avec Docker, chaque application est exécutée à l'intérieur d'un container, qui contient l'ensemble des dépendances requises par cette application. Chaque container Docker possède son propre système de fichier, qui embarque l'intégralité d'une distribution Linux (par exemple une Ubuntu), avec les packages nécessaires pour faire tourner l'application. Il n'y a donc plus aucun problème de dépendances, car l'environnement d'exécution est complètement maitrisé et déterministe.

Mais alors si on souhaite lancer plusieurs dizaines (ou centaines) d'applications sur une même machine, l'espace disque ne risque-t-il pas d'exploser, avec toutes ces distributions Linux installées en même temps ? En fait non ! Docker utilise une astuce appelée union mount, qui consiste à découper un système de fichiers en plusieurs couches (layers), et à pouvoir partager ces couches entre plusieurs systèmes de fichiers. En pratique, tous les containers basés sur la même distribution Linux vont en fait partager les mêmes fichiers de base (les bibliothèques et binaires système, les fichiers de configuration, etc.), et seule la différence entre ces containers est stockée sur disque. C'est une des grandes différences entre container Docker et VM: il est littéralement possible de lancer des milliers de containers sur la même machine, tout en utilisant très peu d'espace disque si ces containers sont similaires.

sandwich

Un autre aspect fondamental du fonctionnement de Docker est qu'un container n'est rien de plus qu'un process Linux (en réalité, un arbre de process) isolé. La conséquence est que démarrer un container revient en fait simplement à exécuter un process, ce qui est pratiquement aussi rapide que lancer ce même process en dehors d'un container. Au contraire d'une VM qui doit lancer un système d'exploitation complet au démarrage, ce qui peut prendre plusieurs dizaines de secondes, le démarrage d'un container Docker est donc quasiment instantané (tout dépend de l'application, évidemment) !

Docker fournit donc un moyen beaucoup plus léger qu'une VM pour encapsuler une application, mais les innovations ne s'arrêtent pas là !

Images, repositories et registry

Une grande idée de Docker est d'avoir repris les mêmes concepts que les systèmes de gestion de versions modernes, comme Git. Le principe est qu'un container Docker est toujours créé à partir d'une image Docker, qui est en quelque sorte un modèle (template) de container. Ces images sont conservées, et versionnées, dans un dépôt (repository) stocké en local sur la machine. Il est donc possible à tout instant de recréer un container Docker à partir d'une image d'une version précédente, de passer facilement d'une version à l'autre, ou même d'exécuter simultanément des containers basés sur des versions différentes.

Là où Docker rejoint encore Git, c'est qu'il est possible d'envoyer des images Docker sur un repository distant, appelé registry, ce qui permet facilement de partager et échanger des images entre plusieurs personnes. De même que Github permet de publier son code source sous Git sur Internet, Docker fournit le Docker Hub, qui est un registry public contenant des milliers d'images Docker prêtent à l'emploi. Il est par exemple possible de télécharger l'image officielle de WordPress sur le Docker Hub, ce qui permet de lancer Worpdress (avec serveur Web et PHP déjà intégrés) sur sa machine en quelques secondes !

Encore plus fort: le concept de Dockerfile définit un standard simple pour créer une image Docker soi-même. Un Dockerfile n'est rien d'autre qu'un fichier texte, contenant quelques commandes à exécuter pour construire une image Docker à partir d'une autre image (généralement venant du Docker Hub).

Voici par exemple un Dockerfile minimaliste permettant de créer une image Docker Ubuntu contenant un serveur MySQL:

Du container au navire

Le nom de "Docker" vient de l'analogie entre les containers Linux et le transport de marchandises. Grâces aux conteneurs (ceux de la vraie vie, cette fois), il est possible de transporter n'importe quelle marchandise sur n'importe quel bateau, train ou camion, n'importe où dans le monde. Comment ce miracle de la logistique est-il possible ? Tout simplement parce que les conteneurs ont des dimensions standard, ce qui permet de les transporter toujours de la même façon, quel que soit leur contenu. Ainsi, le fournisseur n'a pas à se soucier du mode de transport de ses marchandises, il lui suffit de tout mettre dans un conteneur, et le transporteur fera le reste. De la même façon, le transporteur n'a pas besoin de savoir ce que contiennent les conteneurs pour les transporter, du moment que le standard est respecté.

bateau-containers

Il se passe exactement la même chose avec les containers Docker, sauf qu'ici, le fournisseur est le développeur de l'application, et le transporteur est l'hébergeur. Il suffit au développeur d'encapsuler son application dans un container Docker, sans se soucier de la plateforme technique de l'hébergeur, et l'application peut être ainsi déployée de la même façon sur la propre machine du développeur, dans un data center privé, dans un cloud public, etc. De son côté, l'hébergeur peut manipuler des applications "enfermées" dans des containers Docker sans se soucier de leur contenu: différentes applications développées en Java, PHP, Python, Go, Node.js pourront donc être déployées et opérées exactement de la même façon sur un même serveur.

11 reasons to love C++ 11

C++ has gone a long way since the 80's, and a lot of significant improvements have been brought to the language with the C++11 standard, released in 2011.

As a C++ developer, you will definitely like the 11 new features presented below (but there are many other ones!).

1) auto type

Tired of painful variable declarations such as std::map<std::string, std::pair<int, int> >::const_iterator iter;  ?

C++11 introduced the auto type, which automatically guesses the type of a variable based on the context. Enjoy:

2) Range-based "for" loop

All modern languages have a kind of "foreach" construct, to iterate easily over a collection. Now C++ has one too:

3) Extended initializer lists

Standard STL container can now be initialized easily with the curly braces syntax (which was only possible with POD objects before):

or even:

4) Uniform initialization

In C++11, curly braces can actually be used in the same way to initialize anything, being a STL container, an object, an array, an array of objects, etc.

When the type of the object can be guessed by the compiler, for instance for the return value of a function, you don't even need to specify the type in the initialization!

5) Lambda expressions and closures

A lambda expression is simply an anonymous function, that can be used anywhere a pointer to a function would have been required.

Lambda expression are very useful for functional programming, to avoid defining very small functions that would be used only once.

For instance, to increment all the elements of a vector, you can do:

Here, [] is equivalent to lambda in Python or function in Javascript.

Why this strange [] syntax? Because you can actually put something inside the brackets, to create a closure. 

A closure is a lambda expression which captures some local variables defined outside its own scope. In Python and Javascript, all local variables defined in the same scope as a lambda expression are implicitly captured, but as C++ developers like to control things (otherwise they wouldn't code in C++), the list of captured variables in a C++11 closure has to be specified explicitly inside the brackets.

For instance, if we want to increase all elements of our vector by a given number delta, we can do:

Note that if delta variable is not specified in the capture list,  the compiler produces an error.

By default, variables are captured by value, but they can also be captured by reference. For instance, we could compute the sum of all the elements of a vector by using a closure like this:

6) Clever right angle brackets

When using nested templates, you can now write std::map<std::string, std::pair<int, int>>  instead of   std::map<std::string, std::pair<int, int> > .

Looks like a minor improvement, but who has never been bothered by these silly spaces?

7) Regular expressions

Regular expression are now provided by the standard <regex> library.

Here is an example showing how to match a word and a number from an input string:

This produces the following output:

By the way, note the new string literal syntax R"( ... )" to avoid double escaping (similar to r'...' in Python for instance).

8) Smart pointers

Proper memory management is one of the most complex aspects of C++, but fortunately smart pointers are here to make life easier.

The principle of smart pointers is to make sure objects are destroyed automatically when they are not needed anymore. C++11 introduced two kinds of smart pointers: unique_ptr and shared_ptr (and its twin brother weak_ptr).

unique_ptr is a replacement for auto_ptr, which is now deprecated because it can be dangerous to use, especially inside STL containers. A unique_ptr holds the ownership of a pointer, and as the name suggests, it's guaranteed that a single pointer is owned by one and only one unique_ptr at a given time. However, this ownership can be transferred explicitly to another unique_ptr, using the move function:

shared_ptr , borrowed from Boost library, is a reference counting pointer: multiple instances of shared_ptr can share the ownership of the same object, and this object is destroyed only when the last reference to it is deleted:

9) Thread support

The STL in C++11 contains a <thread> library, with all the basic tools needed for multi-threading: threads, mutexes, lock guards, and even futures.

Here is just a simple example launching a thread which just sleeps for n seconds:

This program will print "one", and then "two" 5 seconds later.

10) Hash tables

In the STL, maps and sets are implemented as binary trees, which are ordered containers, meaning that they can efficiently find keys in a range of values.

When order is not required, it's usually more efficient to use hash tables instead of binary trees (in average, a lookup in a hash table has a O(1) complexity, while a lookup in a binary tree has a O(log n) complexity).

In C++11, hash tables are implemented by new containers: unordered_map, unordered_setunordered_multimap and unordered_multiset.

11) Rvalue references

The concept of rvalue reference is quite technical, and this may not be the first reason why you would love C++11, but still, it can be useful to at least understand how it works.

First, a bit of vocabulary: an lvalue is a variable, while an rvalue is a temporary object. For instance:

Here, myString is an lvalue, and foo() is an rvalue, as the foo function returns a temporary string object.

In C++, it's perfectly valid to create a reference to an lvalue, but it's forbidden to create a (non-const) reference to an rvalue:

So, C++ left us with two choices: we could either copy a temporary value (i.e. an rvalue) into another variable, or we could assign it to a const reference.

In C++11, it's also possible to create a non-const reference to a temporary value (i.e an rvalue reference), thanks to the && operator:

Thanks to the rvalue reference, we avoid a copy of the return value of foo function, which is better in terms of performance.

However in practice, this trick is usually not needed, as the compiler is most of the times smart enough to perform Return Value Optimization (RVO), which means that  std::string myString = foo(); doesn't actually copy the return value, but reuses the temporary object created in foo function.

So, rvalue references are mostly useful in libraries such as the STL itself, in which it's important to ensure good performance even when the compiler cannot apply the Return Value Optimization. For instance, the implementation of std::unique_ptr and std::move relies on the usage of rvalue references.

Mesurer la complexité des programmes grâce au λ-calcul binaire

Le titre de cet article peut vous sembler bien mystérieux, mais ne soyez-pas effrayés, tout vous sera bientôt expliqué !

Dans l'article précédent sur la notion d'exergie, je vous ai parlé de l'entropie de Shannon, qui permet de mesurer la quantité d'information d'une source de données. Pour rappel, l'entropie (mesurée en bits) d'une source pouvant prendre N valeurs possibles est donnée par la formule:

H = - \sum_{i=1}^N P_i.log_2 P_i

P_i est la probabilité d'apparition de chaque valeur.

Dans le même ordre d'idées, on peut se demander quelle est la quantité d'information contenue dans un nombre, par exemple 1073741824. La notion d'entropie est ici inutilisable, car en dehors de tout contexte, ce nombre ne peut pas être associé à une probabilité. Il est cependant possible de compter la quantité de bits nécessaire pour représenter ce nombre en binaire, donnée par log_2 (1073741824) = 30. En première approche, la quantité d'information dans 1073741824 serait donc de 30 bits. Cependant, si on remarque que 1073741824 = 2^{30}, on voit que ce nombre peut s'écrire sous une forme beaucoup plus simple, qui est simplement sa décomposition en facteurs premiers. D'une certaine façon, cette expression "2^{30}" est la représentation d'un algorithme qui s'écrirait en français "multiplier 30 fois par lui-même le nombre 2".

Prenons maintenant un cas plus intéressant: le nombre \pi (3,14159...). Ce nombre possède une infinité de décimales à première vue aléatoires, et contient donc a priori une quantité d'information infinie, d'un point de vue numérique. Cependant de nombreux algorithmes existent pour calculer les n premières décimales de \pi, donc l'information contenue dans \pi peut se résumer à la description de l'un de ces algorithmes. La question devient maintenant: quelle est la quantité d'information (en bits) de cet algorithme ? Le but de l'article est justement d'apporter une réponse à cette question !

memoriserpi

On définit la complexité de Kolmogorov (du nom du mathématicien russe Андрей Колмогоров) d'un nombre comme étant la longueur du plus petit programme nécessaire pour calculer ce nombre. Un nombre complètement aléatoire ne peut être décrit par aucun programme plus court que le nombre lui-même, et possède donc une complexité de Kolmogorov maximale (on rejoint ici le concept d'entropie, qui est maximale quand les données sont aléatoires). La définition s'étend bien sûr à d'autres types de données, comme du texte. Par exemple il est clair que le texte "la la la la la la la la la la la la la la la la la la la la" contient assez peu d'information, résumée dans l'algorithme "écrire 20 fois le mot 'la'".

Cette définition est intéressante mais théorique, car d'une part il a été prouvé qu'il est impossible de trouver un algorithme permettant de calculer la complexité de Kolmogorov, et d'autre part elle ne dit pas vraiment comment calculer la longueur d'un programme. Il est clair que le choix du langage de programmation a une influence sur la longueur du programme minimal, mais on peut également prouver assez facilement (théorème d'invariance) que quelque soit le langage choisi, la complexité de Kolmogorov est la même, à une constante près.

Bien qu'il soit impossible de calculer de façon automatique la complexité de Kolmogorov d'un objet (nombre ou texte), on peut obtenir une borne supérieure de cette complexité si on arrive à trouver au moins un programme permettant de produire cet objet. Il ne reste plus qu'à trouver un langage de programmation suffisamment puissant pour écrire ce programme avec le moins de bits possible, pour s'approcher au plus près de la complexité de Kolmogorov. C'est dans cet objectif que John Tromp a mis au point le λ-calcul binaire, qui est simplement une variante du λ-calcul.

y_combinator

 Le λ-calcul (ou "lambda-calcul"), est un formalisme inventé dans les années 1930 par le mathématicien Alonzo Church, bien avant l'apparition des premiers ordinateurs, et est l'ancêtre de tous les langages de programmation fonctionnelle (Lisp, Haskell, Clojure, etc.). La puissance du λ-calcul vient du fait qu'il permet d'écrire n'importe quel programme, à partir d'une syntaxe extrêmement simple. En λ-calcul, tout programme est une expression, qui ne peut être que l'une des 3 formes suivantes:

  1. Une variable, de la forme x, y, z ou n'importe quelle autre lettre
  2. Une abstraction, de la forme (λx.a), où x est une variable et a est une autre expression
  3. Une application, de la forme (a b), où a et b sont deux autres expressions

Et voilà c'est tout ! Mais alors, comment écrire un programme en λ-calcul ? Pour cela, il faut d'abord comprendre quelle est la signification de ces différentes formes.

 Une abstraction (λx.a) est en fait simplement la définition d'une fonction, qui prend la variable x en paramètre, et renvoie l'expression a. Par exemple l'expression (λx.x) correspond à la définition de la fonction "identité", qui renvoie toujours sa valeur d'entrée. L'expression (λx.y) définit la fonction "constante y", qui renvoie toujours y quelque que soit la valeur d'entrée

Une application (a b) correspond en fait à un appel de fonction, et sa valeur est le résultat de la fonction a appliquée au paramètre b (en mathématiques on écrirait plutôt "a(b)"). 

 Le cœur du λ-calcul est la règle suivante, dite de β-réduction, qui permet de calculer le résultat d'une application: (λx.a b) = a[x := b]. En français, cette règle signifie: "Le résultat de l'application de la fonction (λx.a) à l'expression b est égal à l'expression a dans laquelle on a remplacé toutes les occurences de la variable x par l'expression b". Cette règle en apparence complexe est en fait ce qu'on fait tous les jours en mathématiques: par exemple si on a une fonction f définie par "f(x) = x+2", l'application de f au nombre 4 se calcule par "f(4) = 4+2 = 6"; on a simplement remplacé x par 4 dans la définition de f. En λ-calcul, la fonction f s'écrirait "(λx.(+ x 2))", et le calcul s'écrit: "((λx.(+ x 2)) 4) = 6)". Attention, on écrit bien "+ x 2" et non pas "x + 2", car la fonction à appliquer doit toujours apparaître en premier dans une expression (on parle de notation préfixe). A noter que le nom des variables n'a pas d'importance, on peut tout aussi bien écrire la fonction f comme "(λy.(+ y 2))".

Comme cette notation introduit beaucoup de parenthèses, on prend également quelques conventions pour simplifier l'écriture: 

  • (a b c) = ((a b) c)
  • a b = (a b)
  • λa.b c = (λa.(b c))

Avec ces conventions, le calcul précédent s'écrit plus simplement "(λx.+ x 2) 4 = 6".

Dernier détail: une fonction à deux variables peut s'écrire sous la forme λx.λy.a, une fonction à trois variables λx.λy.λz.a, etc.

 Croyez-le ou non, avec ces quelques règles il est possible d'écrire n'importe quel programme !

Par exemple on définit la constante logique "VRAI" par l'expression "λx.λy.x", qui est une fonction qui prend deux variables x et y en paramètre, et renvoie toujours x. De même on définit la constante "FAUX" par "λx.λy.y", et l'opérateur logique "ET" par "λx.λy.y x y". Avec ces conventions, on peut vérifier par exemple que "ET VRAI FAUX = FAUX", grâce à la règle de β-réduction. Allons-y !

ET VRAI FAUX = (λx.λy.y x y) VRAI FAUX = FAUX VRAI FAUX = (λx.λy.y) VRAI FAUX = FAUX

De la même façon on vérifie facilement que "ET VRAI VRAI = VRAI":

ET VRAI VRAI = (λx.λy.y x y) VRAI VRAI = VRAI VRAI VRAI = (λx.λy.y) VRAI VRAI =VRAI

Remarquez que j'ai utilisé dans les exemples précédents des nombres entiers et l'opérateur +, alors que ceux-ci ne sont absolument pas définis par le λ-calcul. Bien sûr il est difficile d'écrire des programmes sans pouvoir utiliser les nombres entiers... Church a donc imaginé une façon de définir les nombres dans le λ-calcul:

  • 0 := λx.λy.y
  • 1 := λx.λy.x y
  • 2 := λx.λy.x (x y)
  • 3 := λx.λy.x (x (x y))
  • n+1 := λx.λy.x (n x y)

Comme les opérateurs logiques, les nombres sont donc définis par des fonctions à deux variables. On remarque d'ailleurs que 0 = FAUX, ce qui est assez logique et habituel dans les langages de programmation. Avec cette convention, l'addition se définit par:

+ := λa.λb.λx.λy.a x (b x y)

Avec ces définitions (bien compliquées), on peut vérifier que 2+1 = 3:

+ 2 1 = (λa.λb.λx.λy.a x (b x y)) 2 1 = λx.λy.2 x (1 x y) = λx.λy.x (x (1 x y)) = λx.λy.x (x (x y)) = 3

 Ouf ! De la même façon on peut définir toutes les structures de données et de contrôle utilisées habituellement en programmation: paires, ensembles, listes, boucles, récursivité, tests, etc. !

binary-number-tunnel-1080p-hd-wallpaper

Le λ-calcul est donc un langage de programmation qui permet d'écrire n'importe quel algorithme, mais pour en revenir au but initial, comment peut-on mesurer la longueur en bits d'un programme λ-calcul ? Il s'agit de trouver un codage pour écrire une expression en binaire, de la façon la plus efficace possible.

La première chose à faire est d'utiliser la notation des index de De Bruijn, qui consiste à remplacer les noms de variable par des nombres. Dans une fonction à n variables, la première variable est notée 1, la deuxième variable est notée 2, etc. Il faut également rétablir les parenthèses, pour ne garder que des applications de la forme (a b).

Par exemple la fonction "identité" λx.x s'écrit simplement "λ1", le nombre 1 (λx.λy.x y) s'écrit "λλ(1 2)", l'opérateur + (λa.λb.λx.λy.a x (b x y)) s'écrit "λλλλ(1 (3 ((2 3) 4)))", etc.

La deuxième étape est de convertir l'expression en binaire, grâce au codage suivant:

  • λa = 00 a'   (où a' est le codage binaire de l'expression a)
  • (a b) = 01 a' b'
  • 1 = 10
  • 2 = 110
  • 3 = 1110
  • etc.

Avec ce codage, l'identité (λ1) devient donc "00 10", le nombre 1 (λλ(1 2)) devient "00 00 01 10 110", et l'opérateur + (λλλλ(1 (3 ((2 3) 4))) devient "00 00 00 00 01 10 01 1110 01 01 110 1110 11110".

Si on utilise les nombres de Church, on peut montrer que l'opérateur "exposant" (x^y) s'écrit tout simplement λx.λy.y x, autrement dit λλ(2 1), qui se code en "00 00 01 110 10". Grâce au λ-calcul binaire, on peut donc coder l'algorithme "exposant" en seulement 11 bits. L'opérateur x^{x^x}, qui s'écrit λx.x x x, se code lui en "00 01 10 01 10 10", soit 12 bits.

Prenons maintenant le nombre 1 suivi de 10 milliards de zéro. Ce nombre gigantesque peut s'écrire 10^{10^{10}}, et est donc le résultat de l'opérateur x^{x^x} appliqué à 10. On peut montrer que le nombre 10 s'écrit avec 47 bits en λ-calcul binaire, donc le programme qui permet de calculer 10^{10^{10}} a une longueur de 2+47+12=61 bits, alors que si on écrit simplement 10^{10^{10}} en binaire, on obtient un nombre de plus de 332 milliards de bits !

La complexité de Kolmogorov de 10^{10^{10}} est donc au maximum de 61 bits; il resterait à prouver qu'il n'existe pas d'algorithme plus efficace (en utilisant le λ-calcul) pour calculer ce nombre, pour montrer que la complexité est exactement égale à 61 bits. Si vous trouvez mieux, ou si vous êtes capable de calculer la complexité du nombre π, vous pouvez partager vos découvertes dans les commentaires !

Entropie, exergie et information

Pour ce premier article, j'ai décidé de parler d'une notion assez peu connue du grand public: l'exergie, et de son lien avec l'informatique.

L'exergie est une grandeur thermodynamique, liées aux notions d'énergie et d'entropie. Il en existe plusieurs définitions, mais la plus intuitive est la suivante: "l'exergie d'un système est le travail utile maximal que peut fournir ce système dans un environnement donné". Le point important est que l'exergie est définie par rapport à l'environnement, contrairement à l'énergie qui est une grandeur intrinsèque du système. Dit autrement, l'exergie est une mesure du déséquilibre d'un système avec son environnement: un système en équilibre avec son environnement ne peut fournir aucun travail utile, son exergie est donc nulle. L'exergie s'exprime en Joules (J), tout comme l'énergie.

Un exemple pour comprendre: une casserole d'eau à température ambiante de 20°C possède une certaine énergie interne (proportionnelle à la température), mais son exergie est nulle car l'eau est en équilibre thermique avec l'environnement, et son énergie est donc inutilisable. Une casserole chauffée à 100°C possède, elle, une exergie (énergie utilisable) capable de faire cuire un oeuf ou de réchauffer - un peu - la cuisine. La même casserole placée dans un four déjà chauffé à 100°C aurait la même énergie, mais par contre une exergie nulle, car elle serait cette fois en équilibre avec son environnement. On comprend avec cet exemple que l'exergie a une utilité pratique beaucoup plus importante que l'énergie, bien que cette notion soit beaucoup moins connue. On comprend également que l'exergie peut se détruire: si on laisse la casserole se refroidir, l'énergie totale de la cuisine ne change pas, mais son exergie totale diminue car l'eau de la casserole et l'air de la cuisine se rapprochent de leur état d'équilibre.

Boiling-Water

Autre exemple: le chauffage électrique. L'électricité est de l'exergie pure, car l'énergie électrique est théoriquement entièrement récupérable en travail (par exemple dans un moteur électrique sans aucun frottement). Dans un radiateur électrique, cette énergie est convertie en chaleur, utilisée pour réchauffer l'air de la pièce. Par contre une grande partie de cette énergie est "perdue" car si l'air chaud de la pièce devait être utilisé dans un moteur thermique pour produire de l'énergie, seule une petite partie (en pratique moins de 10%) de l'énergie de départ pourrait être récupérée. Plus de 90% de l'exergie de l'électricité a donc été définitivement perdue dans l'opération. Et le problème n'est pas que les technologies actuelles ne permettent pas de récuperer toute l'énergie de l'air chaud; c'est en fait une limitation théorique due à la nature même de l'énergie thermique. On a coutume de dire que le chauffage électrique a un rendement énergétique de 100%, ce qui est vrai puisque toute l'énergie électrique dépensée dans le radiateur est récupérée en énergie thermique, mais par contre le rendement exergétique est extrêmement mauvais.

Vous me direz: très bien mais quel rapport avec l'informatique ? J'y viens. L'exergie est en fait directement liée à la notion d'entropie qui est, elle, bien plus connue. En thermodynamique, l'entropie d'un système est une mesure de son degré de désordre; elle ne peut qu'augmenter si le système est isolé (second principe de la thermodynamique) et atteint son maximum lorsque le système est dans son état d'équilibre. Si T_0 est la température de l'environnement, S^{tot} l'entropie totale du système et de l'environnement, et S^{tot}_{eq} l'entropie totale quand le système est à l'équilibre avec l'environnement, on peut montrer que l'exergie du système s'exprime par:

B = T_0 (S^{tot}_{eq} - S^{tot})

Cette relation montre bien que l'exergie est une mesure du déséquilibre entre le système et son environnement, et diminue à mesure que l'entropie augmente. Mais cette relation devient encore plus intéressante si on regarde de plus près la définition de l'entropie. Comme l'a montré Boltzmann, si un système peut prendre W états possibles, avec une probabilité P_i de se trouver dans l'état i, alors l'entropie est égale à

S = - k \sum_{i=1}^W P_i.ln P_i

où k est la constante de Boltzmann (1,381.10^{-23}J.K^{-1}). On considère que lorsque le système est à l'équilibre, tous les états sont equiprobables, et alors l'entropie devient maximale et égale à S = k.ln W.

Le lien avec l'informatique commence à apparaître si on remarque que la définition de l'entropie donnée par Boltzmann ressemble beaucoup à une autre formule découverte par Shannon 75 ans plus tard:

H = - \sum_{i=1}^N P_i.log_2 P_i

Cette formule donne la quantité d'information contenue dans une source quelconque de données, pouvant prendre N valeurs (états) possibles, avec une probabilité P_i pour chaque valeur. Le nombre H s'exprime en bits, et s'appelle l'entropie de Shannon, par analogie. Si les valeurs sont équiprobables, la formule devient H = log_2 N, ce qui est assez logique car une information aléatoire représentée par n bits peut prendre N=2^n valeurs possibles. Si les valeurs ne sont pas équiprobables, alors il est possible de compresser les données, et l'entropie de Shannon permet de calculer le nombre minimal de bits théoriquement requis pour coder l'information sans perte de données.

Et que devient l'exergie dans tout ça ?  Si on combine les équations précédentes, on montre que l'exergie peut s'exprimer par:

B = k.T_0.ln2.H

H est la quantité d'information (en bits) nécessaire pour décrire l'écart entre l'état du système et son état d'équilibre. Cette relation fait donc un lien entre la théorie de l'information et la thermodynamique ! Ce lien peut sembler purement formel et abstrait, mais on peut le comprendre intuitivement si on considère que l'information doit toujours à un moment ou à un autre être représentée par un support physique.

Si on applique cette formule, une exergie de 1 Joule à température ambiante de 20°C correspond à une quantité d'information de 3,6.10^{20} bits ! Pour donner un élément de comparaison, une mémoire flash permet au mieux d'écrire 10 Gbit de données avec 1 Joule d'énergie; on est donc encore très loin du maximum théorique. D'un autre côté, la réplication de l'ADN dans les cellules permet de copier de l'ordre de 2.10^{19} bits d'information par Joule consommé, ce qui extrêmement proche de la limite théorique ! En matière de consommation d'énergie (ou plutôt, d'exergie), la vie est bien plus efficace que les ordinateurs...