Rusticity: convert an integer to an enum

I am learning how to program in Rust. Thinking in Rust is a delightful experience and the more I practice Rust the more I feel how it empowers developers to solve complex problems with confidence.

However, I sometimes get frustrated on my way to rusticity. For instance, when a programming task easily done in C or Python requires more work in Rust. This happened to me not so long ago when I had to convert an integer to an enum. Let's see how this is usually done in C and how this can be done in Rust.

Converting an integer to an enum in C

In C, the enumeration constants have the type int. Thus, an integer value can be directly assigned to an enum.

#include <stdio.h>

enum atomic_number {
    HYDROGEN = 1,
    HELIUM = 2,
    // ...
    IRON = 26,
};

int main(void)
{
    enum atomic_number element = 26;

    if (element == IRON) {
        printf("Beware of Rust!\n");
    }

    return 0;
}

While it is easy to assign an integer value to an enum, the C compiler performs no bounds checking. Nothing prevents us from assigning an impossible value to an atomic_number enum.

Converting an integer to an enum in Rust, the naïve way

Let's write an equivalent program in Rust:

enum AtomicNumber {
    HYDROGEN = 1,
    HELIUM = 2,
    // ...
    IRON = 26,
}

fn main() {
    let element: AtomicNumber = 26;
}

When we try to compile and run the program with cargo run, the Rust compiler reports a mismatched types error:

error[E0308]: mismatched types
 --> src/main.rs:9:34
  |
9 |     let element: AtomicNumber = 26;
  |                                 ^^ expected enum `AtomicNumber`, found integral variable
  |
  = note: expected type `AtomicNumber`
             found type `{integer}`

The compiler error clearly indicates that AtomicNumber and integer are two different types.

To explicitly convert an integer to our AtomicNumber enum, we can write a conversion function that takes an unsigned 32-bits integer as parameter and returns an AtomicNumber.

enum AtomicNumber {
    HYDROGEN = 1,
    HELIUM = 2,
    // ...
    IRON = 26,
}

impl AtomicNumber {
    fn from_u32(value: u32) -> AtomicNumber {
        match value {
            1 => AtomicNumber::HYDROGEN,
            2 => AtomicNumber::HELIUM,
            // ...
            26 => AtomicNumber::IRON,
            _ => panic!("Unknown value: {}", value),
        }
    }
}

fn main() {
    let element = AtomicNumber::from_u32(26);
}

The from_u32() function is an associated function of the AtomicNumber type because it is defined only in the context of this type and unlike a method does not take a first parameter named self.

There are several issues in from_u32():

  • When the given value does not match any variant in the enumeration, the execution is aborted with panic!().
  • The integer value for each enumeration variant is duplicated in the enumeration definition and in the conversion function. We must take care to use the same value in both locations.
  • If the enumeration contains a large number of variants, the conversion function becomes very long.

Since there are more than 100 atomic numbers, implementing a conversion function quickly becomes boring. There should be a better way.

Converting an integer to an enum in Rust with num::Derive

A more elegant solution is to use the FromPrimitive trait from the num crate coupled with syntax extensions from the num-derive crate.

extern crate num;
#[macro_use]
extern crate num_derive;

#[derive(FromPrimitive)]
enum AtomicNumber {
    HYDROGEN = 1,
    HELIUM = 2,
    // ...
    IRON = 26,
}

fn main() {
    let element = num::FromPrimitive::from_u32(26);
    match element {
        Some(AtomicNumber::IRON) => println!("Beware of Rust!"),
        Some(AtomicNumber) => {},
        None => println!("Unknown atomic number")
    }
}

The #[derive(FromPrimitive)] attribute instructs the Rust compiler to generate a basic implementation of the FromPrimitive trait for the AtomicNumber enumeration. Unlike our handwritten from_u32() function, the conversion function generated by the Rust compiler returns an Option which is either Some atomic number or None if the given integer does not match any known atomic number. This is much safer than calling panic!().

With the #[derive(FromPrimitive)] attribute our Rust program is nearly as concise as the equivalent program written in C with the bonus of being safer.

Harder, better, safer, rustier

While I had some hard time figuring how to convert an integer value to an enum variant in Rust, I feel reassured by its type safety and pleased with how its ecosystem of crates can simplify my work as a programmer.

Rust on the pont de Bir-Hakeim

Rust on the pont de Bir-Hakeim

Le paquet targetcli-fb intègre Debian

Depuis quelques mois, le paquet targetcli-fb qui permet de configurer une target iSCSI avec LIO fait partie de Debian Testing (la version en cours de développement de Debian 9 "Stretch").

L'intégration de targetcli-fb vient combler un manque dans Debian. En effet, aucun utilitaire n'est fourni dans l'actuelle Debian Stable (Debian 8 "Jessie") pour configurer une target iSCSI avec LIO.

Je participe à la maintenance du paquet targetcli-fb dans Debian avec deux contributeurs plus expérimentés : Christian Seiler et Ritesh Raj Sarraf. J'utilise Debian depuis de nombreuses années et c'est un juste retour des choses que de contribuer modestement (et librement) au projet.

L'iSCSI sous Linux étant un sujet rarement traité sur le web francophone, j'évoquerai comment s'en servir dans un futur article.

Time-lapse de fleurs de safran

Mi octobre, j'ai eu le plaisir d'assister à l'ouverture des fleurs de safran (Crocus sativus) dans le potager de mes parents. L'occasion était belle de fixer sur la pellicule carte SD les étapes de la floraison pour en faire un time-lapse.

Cette vidéo accélérée 1500 fois a été réalisée en prenant une image toutes les 5 minutes puis en générant une vidéo qui fait défiler 5 images par seconde.

Pour réaliser la vidéo, j'ai employé deux logiciels en ligne de commande : ImageMagick et ffmpeg. ImageMagick a permis de recadrer, redimensionner et légender chaque image tandis que ffmpeg a été utilisé pour combiner les images en une vidéo au format H.264.

Recadrage et redimensionnement

Au moment de la prise de vue, j'ai oublié de configurer mon appareil photo pour prendre des clichés aux dimensions de la vidéo finale : 1920×1080 pixels. J'ai donc enregistré des images dont les dimensions étaient trop grandes : 4608×3456 pixels (ratio 43). Il a fallu recadrer et réduire chaque image à sa dimension finale : 1920×1080 pixels (ratio 169).

Recadrage et redimensionnement

Les deux étapes de recadrage et de redimensionnement ont été réalisées par une seule commande convert en utilisant les options -crop et -resize :

$ convert -crop 3840x2160+0+1130 -resize 50% source.jpg destination.jpg

Cet appel à la commande convert recadre d'abord l'image aux dimensions 3840×2160 en décalant le cadre de 1130 pixels vers le bas (et 0 pixel vers la droite) puis divise ses dimensions par deux pour aboutir à une image de 1920×1080 pixels.

Extraction de l'heure de prise de vue et ajout de la légende

J'ai extrait la date et l'heure de la prise de vue pour chaque image avec le programme exiftool :

$ exiftool -DateTimeOriginal image.jpg
Date/Time Original              : 2016:10:17 08:37:09

Ensuite, j'ai utilisé la commande convert pour ajouter une undercolor box qui contient la légende.

Ajout d'un légende

$ convert source.jpg -pointsize 50 -font Inconsolata -fill white -undercolor '#00000080' -annotate +8+50 " Légende " destination.jpg

La légende est écrite en caractères blancs (-fill white) de 50 points de hauteur (-pointsize 50), dans une police à chasse fixe (-font Inconsolata). Un fond noir en semi transparence (-undercolor '#00000080') augmente la lisibilité.

Création de la vidéo au format H.264 à partir des images

La dernière étape a consisté à combiner les images en une vidéo avec ffmpeg.

$ ffmpeg -framerate 5/1 -i image-%03d.jpg -c:v libx264 -pix_fmt yuv420p -r 30 video.mp4

Les images défilent au rythme de cinq par seconde (-framerate 5/1) et le codec vidéo choisi est x264 pour coder un flux vidéo H.264 (-c:v libx264). J'ai du contraindre le pixel format avec -pix_fmt yuv420p parce que Firefox 45 considère que la vidéo est corrompue quand le pixel format par défaut (yuv422p) est employé.

Mise à jour des fréquences de la TNT pour VLC

Depuis le 5 avril 2016, la haute définition a été généralisée sur la TNT. Il faut donc faire un scan pour rechercher les chaînes. Sous Linux, on emploie la commande w_scan à cet effet :

$ w_scan -c FR -R0 -O0 > mes-chaines.conf

Malheureusement, je constate chez moi que la commande w_scan ne trouve aucune chaîne et affiche des erreurs qui ressemblent fort à celles décrites dans ce bug.

Pour contourner le problème, j'ai utilisé l'application Kaffeine qui identifie correctement les chaînes de la TNT lors d'une recherche. À partir des fréquences détectées par Kaffeine, j'ai écrit un fichier de configuration qui permet de regarder la TNT à Paris avec VLC, parce que je préfère VLC à Kaffeine. Le fichier de configuration pour VLC est le suivant : tnt-paris.conf (émetteur de la tour Eiffel).

Le fichier de configuration doit être donné en paramètre à VLC pour lire la TNT :

$ vlc tnt-paris.conf
L'émetteur de la tour Eiffel

Branchez une antenne plus grande pour mieux recevoir la TNT !
L'émetteur de la Tour Eiffel

Au début était la tortue Logo

Quand j'étais à l'école primaire, notre classe disposait de deux ordinosaures Thomson MO5 et TO7, reliques du plan français informatique pour tous. Ces deux machines étaient déjà dépassées à l'époque et décoraient le fond de notre classe.

Un jour, l'institutrice dépoussiéra un des ordinateurs Thomson et le démarra. Je me souviens que cet ordinateur austère, avec son crayon optique, était particulièrement pénible à utiliser. Son utilisation difficile avait de quoi entamer la curiosité d'un enfant, mais l'ordinateur proposait néanmoins une application amusante : la tortue Logo.

C'est cette tortue Logo que je vais essayer d'animer dans cet article en présentant un script écrit en Python capable d'afficher sa trace. Le tout en moins de 100 lignes de code que je commenterai.

Principe de la tortue Logo

La tortue Logo est un robot qui se déplace à la surface de l'écran. Un crayon est attaché à la tortue et trace son chemin quand il est baissé. La tortue obéit aux commandes formulées par son guide comme AVANCE, REPETE ou TOURNEDROITE.

Voici plusieurs polygones tracés par la tortue Logo, du triangle à l'octogone :

REPETE 3 [ AVANCE 50 TOURNEDROITE 120 ]
REPETE 4 [ AVANCE 50 TOURNEDROITE 90 ]
REPETE 5 [ AVANCE 50 TOURNEDROITE 72 ]
REPETE 6 [ AVANCE 50 TOURNEDROITE 60 ]
REPETE 8 [ AVANCE 50 TOURNEDROITE 45 ]

Mise en oeuvre et contraintes

Je m'impose les contraintes suivantes pour le programme capable d'afficher la trace d'une tortue Logo.

  • Le programme doit peser moins de 100 lignes de code.
  • Le programme est implémenté en Python. Comme chacun sait le python et la tortue sont des reptiles. Plus sérieusement, l'expressivité du langage Python et ses nombreux modules sont de précieux atouts pour respecter l'objectif de concision.
  • Le code du programme est en français (même si les mots clefs sont en anglais). Je ne parlais pas anglais à l'école primaire. De plus, la tortue ne comprend que le français.
  • Le programme enregistre la trace de la tortue dans une image adaptée à la publication sur le Web. Le format choisi est le format vectoriel SVG.
  • Le programme est suffisamment élaboré pour "tracer mon logo en Logo". Mon logo est un space invader façon pixel art.
  • Le programme n'est pas interactif. Il accepte en entrée un fichier contenant les commandes Logo et génère en sortie une image vectorielle SVG.

Ma tortue Logo en Python

Voici les 98 lignes de ma tortue Logo écrite en Python (fichier source). L'implémentation est grandement simplifiée en utilisant les bibliothèques pyparsing et svgwrite.

#!/usr/bin/python

from collections import namedtuple
from math import cos, sin, pi
from pyparsing import *
from svgwrite import Drawing
from svgwrite.path import Path
import sys

av = Group((Keyword('AV') | Keyword('AVANCE')).setParseAction(replaceWith('AV')) + Word(nums))
bc = Group(Keyword('BC') | Keyword('BAISSECRAYON').setParseAction(replaceWith('BC')))
lc = Group(Keyword('LC') | Keyword('LEVECRAYON').setParseAction(replaceWith('LC')))
re = Group((Keyword('RE') | Keyword('RECULE')).setParseAction(replaceWith('RE')) + Word(nums))
td = Group((Keyword('TD') | Keyword('TOURNEDROITE')).setParseAction(replaceWith('TD')) + Word(nums))
tg = Group((Keyword('TG') | Keyword('TOURNEGAUCHE')).setParseAction(replaceWith('TG')) + Word(nums))
commande = av | bc | lc | re | td | tg
repete = Group(Keyword('REPETE') + Word(nums) + Literal('[').suppress() + OneOrMore(commande) + Literal(']').suppress())
logo = OneOrMore(repete | commande)

LARGEUR_CANEVAS = 400

Position = namedtuple('Position', 'x y')

class Tortue:
    def __init__(self):
        self.pos = Position(LARGEUR_CANEVAS / 2, LARGEUR_CANEVAS / 2)
        self.cap = 0  # en degres : 0 => haut, 90 => droite
        self.crayon_bas = True
        self.chemin = Path(d='', stroke = 'black', fill='white')
        self.chemin.push('M{x},{y}'.format(x=self.pos.x, y=self.pos.y))

    def __radian(self):
        return self.cap * pi / 180.0 - pi / 2.0

    def avance(self, nb_pas):
        self.pos = Position(self.pos.x + int(round(cos(self.__radian()) * nb_pas)),
                            self.pos.y + int(round(sin(self.__radian()) * nb_pas)))
        if self.crayon_bas:
            self.__svg_lineto()

    def recule(self, nb_pas):
        self.avance(- nb_pas)

    def tourne_droite(self, angle):
        self.cap = (self.cap + angle) % 360

    def tourne_gauche(self, angle):
        self.tourne_droite(- angle)

    def baisse_crayon(self):
        self.crayon_bas = True
        self.__svg_moveto()

    def leve_crayon(self):
        self.crayon_bas = False

    def trace_commandes(self, commandes):
        for cmd in commandes:
            if cmd[0] == 'AV':
                self.avance(int(cmd[1]))
            elif cmd[0] == 'RE':
                self.recule(int(cmd[1]))
            elif cmd[0] == 'TD':
                self.tourne_droite(int(cmd[1]))
            elif cmd[0] == 'TG':
                self.tourne_gauche(int(cmd[1]))
            elif cmd[0] == 'BC':
                self.baisse_crayon()
            elif cmd[0] == 'LC':
                self.leve_crayon()
            elif cmd[0] == 'REPETE':
                nb = int(cmd[1])
                for i in range(nb):
                    self.trace_commandes(cmd[2:])

    def trace_programme(self, programme):
        self.trace_commandes(logo.parseString(programme))

    def trace_fichier(self, fichier):
        with open(fichier, 'r') as contenu:
            self.trace_programme(contenu.read())

    def __svg_moveto(self):
        self.chemin.push('M{x},{y}'.format(x=self.pos.x, y=self.pos.y))

    def __svg_lineto(self):
        self.chemin.push('L{x},{y}'.format(x=self.pos.x, y=self.pos.y))

    def enregistre_svg(self, fichier):
        dessin = Drawing(fichier, profile='full', fill='white')
        dessin.add(self.chemin)
        dessin.save()


if __name__ == '__main__':
    tortue = Tortue()
    tortue.trace_fichier(sys.argv[1])
    tortue.enregistre_svg(sys.argv[2])

Quelques commentaires

Un peu de trigonométrie

Au départ, la tortue Logo est positionnée au centre de l'écran et pointe vers le Nord. Depuis sa position initiale, elle se déplace pas à pas et peut tourner sur sa droite ou sur sa gauche par degrés.

Par exemple, la tortue Logo trace un triangle de 100 pas de côté quand elle avance de 100 pas et tourne de 120 degrés sur sa droite à trois reprises :

REPETE 3 [ AVANCE 100 TOURNEDROITE 120 ]

Pour calculer la position de la tortue après chaque déplacement, il faut tenir compte :

  • de la position initiale de la tortue ;
  • de son orientation ;
  • du nombre de pas qu'elle doit effectuer.

Un soupçon de grammaire

La tortue Logo obéit à des commandes qui ressemblent au langage naturel (AVANCE, TOURNEDROITE, REPETE, etc.). Une partie de notre programme est donc dévolue à l'analyse syntaxique (parsing, en anglais) des commandes Logo.

La bibliothèque pyparsing permet d'implémenter un analyseur syntaxique avec Python en décrivant une grammaire formelle.

Voici, par exemple, la grammaire formelle du mini langage Logo interprétable par notre tortue.

av ::= "AVANCE" + nombre
bc ::= "BAISSECRAYON"
lc ::= "LEVECRAYON"
re ::= "RECULE" + nombre
td ::= "TOURNEDROITE" + nombre
tg ::= "TOURNEGAUCHE" + nombre
commande ::= av | bc | lc | re | td | tg
repete ::= "REPETE" + nombre + "[" + UnOuPlus(commande) + "]"
logo ::= UnOuPlus(repete | commande)

La transcription en Python avec pyparsing se fait presque directement. Pour chaque commande, on définit aussi un raccourci (e.g. AV pour AVANCE) :

av = Group((Keyword('AV') | Keyword('AVANCE')).setParseAction(replaceWith('AV')) + Word(nums))
bc = Group(Keyword('BC') | Keyword('BAISSECRAYON').setParseAction(replaceWith('BC')))
[...]
commande = av | bc | lc | re | td | tg

Une fois que pyparsing a digéré les commandes Logo, la fonction trace_commandes() identifie chaque commande et ses éventuels paramètres et les transmet à la fonction associée :

def trace_commandes(self, commandes):
    for cmd in commandes:
        if cmd[0] == 'AV':
            self.avance(int(cmd[1]))
[...]
        elif cmd[0] == 'BC':
            self.baisse_crayon()
[...]
        elif cmd[0] == 'REPETE':
            nb = int(cmd[1])
            for i in range(nb):
                self.trace_commandes(cmd[2:])

À noter que la fonction trace_commandes() utilise la récursivité pour gérer la commande REPETE.

Un chemin vers SVG

La trace de la tortue Logo est dessinée dans une image SVG avec la bibliothèque svgwrite. Le programme n'utilise qu'un seul élément de SVG, le path qui est parfaitement adapté à notre usage.

Un path SVG comprend un attribut d (pour data) qui est une liste de commandes L (pour lineto) ou M (pour moveto) suivies des coordonnées cartésiennes. La première commande d'un path SVG doit être une commande moveto.

Par exemple, pour tracer un angle droit, il faut donner les coordonnées de trois points et tracer deux lignes entre ces trois points :

<path d="M10,10  L10,100  L100,100"/>

Dans le cas de notre tortue Logo, il suffit d'un path SVG pour matérialiser sa trace.

  • Quand la tortue reçoit la commande BAISSECRAYON, on ajoute une commande moveto pour débuter sa trace.
  • À chaque déplacement de la tortue, on ajoute une commande lineto au path SVG si le crayon est baissé.

Pixel art vectoriel

Pour conclure, j'ai demandé à la tortue Logo de tracer le space invader qui me sert d'avatar (fichier source).

Enfin, la vidéo suivante montre la tortue Logo filant à toute allure telle un lièvre.

Tortue Logo : LEVECRAYON.