La pente est raide mais la route est longue

Vu tout ce à quoi peut servir python, pas facile de trouver un «nouveau» langage. J’ai jeté mon dévolu sur rust, histoire de voir s’il se passait enfin quelque chose d’intéressant côté langages compilés, vu les échecs totaux de C++, D, go et consorts. Je fais un grand sac, hein, je peux rajouter C# et Java si on me chauffe un peu. Mais là, je me tiens bien, les oreilles couchées, bien droit sur ma chaise, la raie de côté.

Et que dire si ce n’est que, apprendre le rust, ça me permet de prendre des coups de pelle plein fer à un rythme assez soutenu. Et pas que à cause des points-virgules ! Ne me lancez pas sur les points-virgules. S’il-vous-plaît.

Mon projouet : une classe «vecteur à deux dimensions»

Pour résumer : un machin qui contient un x et un y, qu’on peut additionner ou modifier par addition, pour utiliser sur codingame. En Python, ça donnerait ça :

class V2:

  def __init__(self, x, y):
    self.x = x
    self.y = y

  def __add__(self, autre):
    return V2(self.x + autre.x, self.y + autre.y)

Ce n’est pas avec ça qu’on révolutionnera les Internets, mais bon, il faut bien commencer quelque part. Pour l’histoire, mon premier projouet était un conteneur générique à références croisées ; force est de constater que j’avais été un peu trop ambitieux. Je n’ai plus fait de rust pendant six mois.

La gifle : choisir un type de base

Alors, hardi petit, et on tapote en rust :

struct V2 {
  x: usize,
  y: usize,
}

Le connaisseur en gestion de configuration appréciera la virgule à la troisième ligne.

- Ha ! Pourquoi usize et pas u64 ?

- Disons, l’expérience professionnelle.

- Oui mais, des fois on va faire des calculs dans tous les sens, on va perdre de la précision partout, c’est trop horrible. Faudrait faire une classe patron, comme ça je dirai en fonction des usages si j’ai besoin d’entiers ou de flottants.

- Tout doux, Bijou ! Les années 2000 sont terminées. Bataille pas, colle des flottants partout, on verra plus tard.

- Oui mais…

- Oui mais rien du tout, tu fais ce que je dis, on est la même personne, je te rappelle.

- Bon.

Alors, je m’obéis, et je change :

#[derive(Debug)]
struct V2 {
  x: f64,
  y: f64,
}

Bon. J’ai déjà envie d’une douche, mais je sais que ce genre de choix est inhérent au type du langage. il faut choisir. L’annotation initiale (« [derive(Debug)]« ), c’est pour pouvoir afficher le contenu de mes objets histoire de déboguer à la console comme un cochon, hein. Pas d’inquiétude. Ou presque.

La béquille : surcharger un opérateur arithmétique

Surcharger les opérateurs arithmétiques, mais pourquoi faire ? En premier lieu : du code lisible. Alors, je me paluche la documentation, et finis par tomber sur les traits «std::ops». C’est ça qu’il me faut ! Je tape :

impl std::ops::Add for V2 {
  type Output = V2;
  fn add(self, autre: V2) -> V2 {
    V2 { x: self.x + autre.x, y: self.y + autre.y }
  }
}

Bon, ça a l’air facile comme ça, mais une lecture attentive nous montre que ça ne va pas marcher. Si par exemple, j’utilise tout ça dans une belle fonction principale :

fn main() {
  let v1 : V2{ x: 15.0, y: -3.0 };
  let v2 : V2{ x: -4.0, y: 27.0 };
  let v3 = v1 + v2;
}

Jusqu’ici, tout va bien, mais si  je rajoute juste avant la fin :

  println!("{:?}", v1);

Et bien paf ! ça ne compile pas, et pourquoi ? Parce que la propriété de v1 a été transférée à la fonction d’addition, donc n’est plus disponible. C’est logique, c’est le langage qui veut ça. Pour que ça, ça marche, j’aurais du écrire :

  let v3 = &v1 + &v2;

C’est bien ce que je veux : utiliser v1 sans le modifier, pour pouvoir faire mon calcul. Du coup, l’opérateur que je dois implémenter n’est pas une addition à base de valeurs, mais de références. Et donc, en m’inspirant de BigInt, je rajoute, attention les yeux :

impl<'a, 'b> std::ops::Add<&'b V2> for &'a V2 {
  type Output = V2;

  fn add(self, autre: &V2) -> V2 {
    V2::new(self.x + autre.x,
            self.y + autre.y)
  }
}

Ça a l’air compliqué comme ça (bouh !), mais ça ne fait que l’addition de deux références aux cycles de vie différents. J’ai donc deux opérations d’addition :

- une entre deux valeurs (qui seront donc définitivement perdues ensuite) ;

- une entre deux références.

Je pourrais aussi avoir besoin d’autres variantes pour les fois où je ferais des trucs plus compliqués, comme additionner trois vecteurs sur une seule ligne : une addition entre une valeur et une référence, et entre une référence et une valeur. Soit deux surcharges supplémentaires :

// &v1 + v2
impl<'a> std::ops::Add<V2> for &'a V2 {
  type Output = V2;

  fn add(self, autre: V2) -> V2 {
    V2 { x:self.x + autre.x,
         y: self.y + autre.y }
  }
}

// v1 + &v2
impl<'b> std::ops::Add<&'b V2> for V2 {
  type Output = V2;

  fn add(self, autre:&V2) -> V2 {
    V2 { x: self.x + autre.x,
         y: self.y + autre.y}
  }
}

Ouf ! Ça commence à faire des kilomètres de code pour pas grand chose… La robustesse du logiciel a un prix : le voici ! Sans compter qu’on a l’impression de retomber dans le C++, ambiance référence sur objet temporaire et compagnie… Il doit y avoir moyen de faire plus simple, parce que sinon, quand il faudra rajouter les autres opérations, je vais décéder ! J’ai cru voir tout un système de macros, on ira voir plus tard.

La variante : le poisson chinois

Je viens tout de même d’écrire quatre variantes pour un même opérateur, soit quatre sources de bogues assez subtiles, opérateur que j’ai favorisé sur une banale méthode pour écrire «du code lisible», et j’en viens à devoir écrire «v3 = &v1 + &v2» pour écrire du code correct et sûr. Comme une impression d’avoir laché la proie pour l’ombre, pas vous ? Mais il y a une autre option : le trait «std::marker::Copy». Qu’est-ce ? Si je suis près à faire une croix sur l’unicité des références aux instances, tout est plus simple. En dérivant ce trait, seule la première implémentation naïve de l’addition suffit.

Fort bien, mais qu’est-ce que le trait Copy ? De ce que j’en comprends, une directive de compilation (puisqu’il n’y a rien à implémenter de spécifique) pour dire que toutes les copies physiques d’une instance sont équivalentes. Est-ce que du coup le compilateur produit effectivement des copies quand j’écris «v3 = v1 + v2» ? Mystère. Je sens que ce simple trait est un sujet de thèse à part entière. Alors j’assume : je ne le dérive pas, et j’implémente mes quatre variantes de la même addition en attendant d’en savoir plus.

Un coude, une cloison nasale : deux raisons de saigner

Maintenant qu’on titube de douleur, on se dit : «Tiens, et si je mettais mes vecteurs dans des conteneurs ? Ça pourrait arriver, dans la vraie vie.» Ni une, ni une, je tapote :

let mut s = std::collections::HashSet::new();
s.insert(v3);
println!("{}", s.len());

Las ! Je reprends une mandale : comment voulez-vous distinguer deux instances si vous ne dites pas comment les hacher ? Et le trait «std::hash::Hash» alors ? Bon, je ne peux pas franchement donner tort au compilateur. Allons au plus vite : dérivons le trait standard :

#[derive(Hash)]

Le compilateur n’en veut pas : le type «f64» n’est pas hachable par défaut. J’imagine que c’est à cause de la norme IEEE-754 et de ses classes d’équivalence. J’ai donc le choix entre écrire comment hacher un «f64», mais ça risque d’être valable pour tout le programme, soit limiter ma façon de hacher du f64 aux bornes de mon vecteur. Je choisis cette dernière façon de pourrir le code, et j’implémente explicitement le trait std::hash::Hash plutôt que de le dériver :

impl std::hash::Hash for V2 {
  fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
    self.x.to_bits().hash(state);
    self.y.to_bits().hash(state);
  }
}

En passant par la représentation physique (avec «f64::to_bits», j’obtiens un entier parfaitement hachable ! C’est moche mais c’est isolé, alors ce n’est pas grave. Bon, ça passe presque : maintenant qu’on sait faire de la bouillie de vecteur, il faut aussi introduire une relation d’équivalence (le trait std::cmp::Eq). Ce n’est pas moi, hein, c’est le compilateur, n’oubliez pas. Histoire d’assurer que deux instances équivalentes aient même haché (cf. ). N’imaginez même pas dériver du trait Eq ! J’imagine que c’est pour les mêmes raisons que f64 n’est pas hachable. Par contre, on peut au moins dériver le trait std::cmp::PartialEq, ce qui est déjà pas mal. Bref, il ne reste qu’à rajouter :

#[derive(PartialEq)]
…
impl std::cmp::Eq for V2 {
}

Et là, enfin ! Ça compile, ça se lance, ça ne fait pas n’importe quoi ! Il serait temps me direz-vous. Je peux même insérer deux fois v3, ça passe… Enfin, deux références à v3, bien sûr.

[…à suivre…]


Rambo chez Lenôtre

C’est toujours au moment où je m’y attends le moins que mes collègues m’épatent. Je rentrais gentiment de la cantine, quand un collègue, resté au bureau bien à l’abri du croque-monsieur aux quatre huiles, m’alpague au vol.

Lui – Tiens, j’ai trouvé un défaut là, sur le portage winrt.

Moi – Ha ?

Je sais, j’ai l’air de manquer d’intérêt, mais d’une, je n’ai pas encore versé mon café, deux, ce n’est pas très étonnant que le portage winrt soit buggué, puisque c’est lui qui l’a fait et validé.

Lui – Oui, j’avais une trace « Underflow error », et puis les attentes de tâches de fonctionnaient pas. Enfin là, c’est corrigé. Je vais envoyer une nouvelle version de la lib compilée par mél.

Moi – Le code est en conf ?

Lui – Euh non, pas là, enfin pas encore.

Agile jusqu’au bout des ongles.

Moi – Je préfèrerai qu’on ouvre un Fait Technique, histoire qu’on trace le défaut, et qu’on corrige et livre proprement. Pourquoi, c’est urgent, le client est bloqué ?

Lui – Non, non, le client ne l’a pas encore vu.

Alors effectivement, il est urgent de se tirer une balle dans le pied.

Lui – Non mais t’as raison, je vais faire un FT, comme tu as dit.

Moi – Voilà, je livrerai lundi, au calme, ne t’inquiète pas. Par contre, je ne sais pas comment on compile le portage winrt, tu le feras ?

Lui – Euh, c’est que, je dois partir en vacances.

Comme quoi, répéter toutes les semaines qu’il faut partager la manière de compiler le portage winrt en point d’avancement hebdomadaire est aussi efficace qu’un physicien dans un harem.

Le Chef, qui doit également partir en vacances, et a initié le portage – Oui non mais regarde, c’est facile, tu prends la tablette là, tu branches un clavier, une souris, une alim’, tu te connectes là, tu vires le clavier ou la souris pour brancher une clef USB et monter les sources, tu redébranches, t’ouvres Visual, puis bon voilà hein, tu verras.

Moi – Les libs compilées, elles sortiront où ?

Le Chef – Ha ben je … Il faut regarder les attributs du projet.

Lui – Ou alors c’est dans C:

Moi – Bien. Je me débrouillerai je crois. Et c’était quoi ce défaut ?

Si j’échoue à compiler, je ne crois pas franchement qu’on pourra m’en vouloir après une formation aussi poussée.

Lui – En fait, les tâches en attente restaient bloquées. La tablette est trop lente. Quand on fait un appel du genre « réveille-moi dans 10 ms », le temps qu’on rentre dans la fonction, les 10 ms sont écoulées, du coup on a une échéance dans le passé, et on est bloqué.

Moi – Classique. C’est exactement le même problème qu’on a eu sur deux autres portages, mais OK. Et comment as-tu corrigé ?

Lui – En fait, avant, on ne vérifiait pas le code d’erreur de la soustraction entre l’échéance et la date courante. Comme la tablette est trop lente, la date courante est toujours plus grande que l’heure absolue de l’échéance, donc la soustraction échoue systématiquement, en produisant « Underflow error » et un code de retour FAILURE. Donc maintenant, on vérifie le code de retour.

Comment ? Dans un logiciel temps-réel embarqué, on devrait s’assurer en permanence que « tout marche bien navette » ? J’ai failli faire une descente d’organe.

Moi – Tu ferais mieux de faire une comparaison entre les deux dates, et en fonction tu fais l’appel système ou tu restes passant. Sinon tu vas produire des traces pour rien.

Lui – Ha ! mais non, pas de soucis : je me suis dit que ça ferait faire deux fois la comparaison, alors plutôt, j’ai testé le code de retour de la soustraction, et puis surtout, j’ai supprimé l’émission de la trace « Underflow error ». Tu vois, plus de problème.

Excellent ! Tu supprimes la trace qui t’a permis de pointer du doigt ton problème, et tu joues du tractopelle dans mon jardin à la française, mais continue ! Tu veux pas déféquer sur mon bureau, si ?

Moi – Certainement pas ! C’est un principe de conception ! Quand une fonction ne remplit pas son office, elle trace un problème, vu qu’en C, rien n’oblige à vérifier les codes de retour. ON NE SUPPRIME PAS LES TRACES DE DEBUG. Je pense l’avoir déjà expliqué un certain nombre de fois !

Lui – Oui mais j’avais plein de fois la trace alors qu’il n’y avait pas de problème.

Moi – Ha ben si, y’a un gros problème : comme tu peux vérifier avant d’appeler ta fonction si elle ira au tas ou pas, tu dois le faire, et pas attendre qu’elle échoue pour te dire qu’il y a un problème. Il est prédictible ton problème, c’est fini la programmation à base d’exception, hein, tu peux pas espérer en croisant les doigts que allez, on sait jamais, 2 – 5, cette fois, ça pourrait être positif, va savoir. Sur ce, bonnes vacances, moi, je pars en week-end.

Non mais.

Fedora – numériser soi-même ses livres

Pourquoi numériser ses livres ?

Avant d’acheter une liseuse numérique, j’ai dû opérer une réflexion de fond sur la nécessité de posséder physiquement un livre. Selon moi, ce n’est intéressant que si :

  • On aime lire et relire les même livres ; personnellement, je n’ai plus le temps de relire un livre qui m’a plu, même si ce n’est pas l’envie qui manque (1984, les racines du mal, etc.)
  • On a l’occasion de prêter ses livres à d’autres personnes.

Mais, au fond, les bibliothèques publiques ne servent-elles pas à ça ? J’aime l’objet « livre », mais les étagères viennent à manquer, et jeter un livre est un crève-cœur, d’autant plus s’il m’a plu. Enfin, le livre numérique peut être corrigé des inépuisables coquilles – certes, mais pour qui, puisqu’on ne le relira pas ?

Cependant, les éditeurs n’ont pas encore converti l’ensemble de leur catalogue, voire même, certains ne le font même pas pour leurs nouveautés. Luttons, et numérisons nous-même !

Première étape – la prise de vue

Un carton coupé en deux, une lampe de bureau, un cadre photo dont on aura récupéré le verre, et c’est parti ! Montant de l’investissement : moins de 40 euros, l’essentiel étant le prix du pied photo que je n’avais pas.

Au début, c’est un peu fastidieux ; il faut prendre le rythme. Vérifier toutes les quinze – vingt photos si le cadrage est toujours bon. Vérifier que les pages paires ne viennent pas gêner la prise de vue des pages impaires, et réciproquement. Personnellement, j’ai utilisé le fond du cadre photo pour écraser le livre.

Bref, au final, il faut deux heures pour photographier 500 pages. Puis revoir rapidement les clichés, et refaire ceux qui sont ratés.

Deuxième étape – le filtrage logiciel

Grâce à ScanTailor, cette étape est vraiment facile ! Il s’agit d’égaliser les clichés, pour n’en faire ressortir que le texte, en corrigeant les déformations de la prise de vue. C’est pour ça qu’il est importe que les clichés soient nets, quitte à ce qu’ils soient penchés ou déformés.

Petit bémol, ScanTailor ne prend pas en compte les informations EXIF des clichés. J’avais fait un script pour ajouter automatiquement l’orientation des pages par lot (toutes les pages paires sont orientées comme si, et les pages impaires dans l’autre sens), mais ça n’a pas eu d’effet sur ScanTailor. Il a donc fallu tourner DANS ScanTailor une page sur deux, ce qui est inutilement fastidieux.

ScanTailor fait tout à peu près automatiquement : identification de la zone de texte, réalignement avec l’horizontale, correction de la déformation (ne pas hésiter à l’activer, ça marche plutôt très bien !). Il faut passer un peu de temps sur :

  • les pages qui contiennent peu de texte, celles-ci mettant à mal les algorithmes ;
  • les pages à la mise en page inhabituelle
  • la suppression des numéros de page lors de la prise en compte de la zone d’intérêt.

Au final, tout sort au format TIFF, et on peut si on veut s’en contenter pour en faire un ePub. Mais pas moi.

Troisième étape – la reconnaissance de caractères

Pour avoir essayé gocr, je peux le dire : tesseract obtient de bien meilleurs résultats ! La difficulté que j’ai rencontrée : la prise en charge du texte en italique, ou des mots anglais pour un texte français. Apparemment, il y a moyen d’apprendre de nouvelles fontes (avec pyTesseract par exemple), tout ceci reste encore à creuser.

 

La suite au prochain numéro !

Tags:

Un nouvel hebdomadaire télé dans vos kiosques

Une petite révolution

Grâce à « Film amorti hebdo », plus de soucis pour planifier votre prochain visionnage de « Retour vers le futur II » ! En effet, ce nouveau magazine y consacre une double page vous indiquant les heures et les chaînes de la TNT qui le diffusent cette semaine. Pareil pour « Indiana Jones et le temple maudit », tout comme « Destination finale » et « Urban legends ». Il était temps ! Au vu des fréquences de rotation de ces œuvres et l’incapacité de la TNT à remplir ses grilles, il devient bien plus facile de programmer son épanouissement culturel.

Un magazine pour toute la famille

Les films de la saga « Star Wars » ont les honneurs d’un mini-livret de 8 pages à assembler soi-même, pour le bonheur de vos enfants. Ou alors, ça vous rappelera les suppléments été du « Journal de Mickey ».

De l’ambition à revendre

Mais les créateurs de « Film amorti hebdo » ne comptent pas s’arrêter là : l’acquisition d’un ensemble de canaux du bouquet Canal Satellite est en projet, qui bas de l’aile à s’entêter à diffuser des films récents. Les films les plus amortis passeront en boucle sur pas moins de deux canaux. Par exemple, vous pourrez choisir entre « Les bronzés font du ski TV » et sa version « + 1h ». Idéal pour ne plus jamais rater le début de votre film culte.

Tags:

Python – comparaison lexicographique efficace

Vu que j’ai mis un peu de temps à trouver comment faire, je me dis que ça mérite d’être inscrit au fronton du mausolée des bonnes idées.

Soit une classe Python Cost définie comme suit :

class Cost(object):
   def __init__(self):
      self.write = 0
      self.exec = 0

Comment y adjoindre efficacement les opérateurs classiques de comparaison, c’est-à-dire en écrivant le moins de code possible (très important !) et en faisant en sorte qu’elle coûte le moins cher possible à l’exécution ? On prendra l’exemple d’une comparaison lexicographique classique, celle-là même qui fait que :

(1, 2) < (1, 3)
"abc" >= "aac"

Aparté pour technologue : on pourra utiliser cette même classe Cost pour évaluer les différentes solutions, on est en pleine auto-référence, c’est trop génial !

Les comparaisons en Python

La documentation de python (« pydoc \< » dans votre interpréteur de commande favori) nous apprend qu’il y a deux stratégies pour les opérateurs de comparaison :

  • les surcharger un par un. Il faudra donc écrire le comportement de <, >, <=, >=, == et !=, en espérant que ces six implémentations soient homogènes. Bonne chance. Le plus simple dans ce cas est d’en implémenter un seul, et de définir les autres plus par rapport aux attributs, mais par rapport à cette implémentation de référence. Par exemple, on peut définir les cinq autres opérateurs à partir de <:
a <= b : return not (b < a)
a >  b : return a < b
a >= b : return not (a < b)
a != b : return a < b or b < a
a == b : return not (a < b or b<a)
  • surcharger un opérateur unique faisant ce qu’il est communément appelée une trichotomie : il renvoit une valeur numérique entière qui est négative quand a est plus petit que b (typiquement, -1), nulle quand ils sont égaux, et positive sinon (tout aussi typiquement, 1). Il n’y a alors qu’une méthode à écrire, et l’interpréteur Python se charge de câbler les opérateurs usuelle sur cette implémentation de référence.

Il est bien entendu évident qu’on va utiliser la stratégie de l’opérateur unique, si on trouve un moyen d’écrire ce même opérateur efficacement.

Rédaction efficace de la trichotomie

Une première implémentation naïve de la trichotomie serait la suivante :

class Cost(object):
   ...
   def __cmp__(self, other):
      if self.write < other.write: return -1
      elif self.write > other.write: return 1
      else:
         if self.exec < other.exec: return -1
         elif self.exec > other.exec: return 1
         return 0

C’est un peu verbeux, et on s’y prend mal : pourquoi n’utiliserait-on pas aussi ce même opérateur cmp pour nos propres attributs ? Recommençons.

class Cost(object):
   ...
   def __cmp__(self, other):
      test_write = cmp(self.write, other.write)
      if test_write != 0: return test_write
      test_exec = cmp(self.exec, other.exec)
      return test_exec

C’est déjà plus concis (ce qui est arbitrairement impératif) Mais peut-on faire mieux ? Se passer des variables temporaires tout en garantissant que la comparaison de l’attribut ‘exec’ des deux instances ne soit effectuée que si nécessaire, donc quand les attributs ‘write’ sont égaux ?

Les opérateurs logiques de Python (and, or, etc.) ont l’aimable particularité d’être semis-stricts : l’expression à leur droite n’est évaluée que si l’expression à leur gauche ne permet pas, seule, de déterminer le résultat de l’opération. Pour e1 and e2, ça signifie que e2 n’est évalué que si e1 est vrai. Ils sont donc des candidats de choix pour nous aider à avoir une écriture encore plus concise.

class Cost(object):
   ...
   def __cmp__(self, other):
      return cmp(self.write, other.write) \
          or cmp(self.exec, other.exec)

Et c’est là que la magie de Python opère (ne soyons pas chiens, le comportement serait identique en C) : X or Y renvoit X si X est jugé vrai, Y sinon. Et c’est bien ce que l’on veut ici : le résultat de la première comparaison si elle est déterminante, celui de la seconde sinon. Encore une fois, c’est génial !

Conclusion

La méthode est scalable et peut-être étendue à peu de frais à un troisième, un quatrième attribut, voire plus.

Tags: , ,