Homero 2019

Posted on 2019-01-02

En écoutant France inter ce matin j’ai eu immédiatement envie de partager cette lecture collective sur twitter de l’Iliade en français. J’ai donc immédiatement tweeté les premiers vers du premier chant de l’Iiliade, avant de me rendre compte que la contrainte exigeait de twitter un chant par semaine. Il est donc devenu urgent d’écrire un programme pour faire en sorte de pouvoir générer ces tweets automatiquement au lieu de devoir laborieusement les taper un par un. Voici le fruit, brut de décoffrage de cette micro-aventure…

Tout d’abord un peu de cérémonies pour que le fichier contenant le code soit interprétable comme un script:

#!/usr/bin/env stack
-- stack runhaskell --resolver lts-12.5 --

Puis les inévitables extensions de langage et imports…

{-# LANGUAGE LambdaCase #-}
import           Control.Applicative
import           Data.Char
import qualified Data.List           as List
import           Data.Monoid
import           System.Directory
import           System.Environment
import           System.IO

On va avoir envie de générer plus d’un tweet à la fois, et on va donc lire le nombre de tweets à générer en argument de la ligne de commande:

getNumTweets :: IO Int
getNumTweets = getArgs >>= \case
  [] -> pure 1
  (n:_) -> pure $ read n

Le principe de ce programme, concentré dans la fonction makeTweets est extrêmement simple, on pourrait même dire brutal:

makeTweets :: Int -> String -> IO ()
makeTweets 0 _ = pure ()
makeTweets numTweets content = do
  start <- readBreak <|> pure 0
  let ws = List.inits . drop start . words . filter (not . isDigit) $ content
      sentence = lastWithPunctuation $ takeWhile ((< 260) . length . unwords) ws
      end = start + length sentence
  putStrLn $ unwords sentence <> " #homero2019"
  writeBreak end
  makeTweets (numTweets - 1) content

La fonction auxiliaire lastWithPunctuation est particulièrement brutale et dangereuse, mais néanmoins amusante:

lastWithPunctuation :: [[ String ]] -> [String]
lastWithPunctuation = head . dropWhile (\ s -> not $ isPunctuation $ head $ head $ reverse <$> reverse s) . reverse

Les deux fonctions readBreak et writeBreak servent simplement à mettre à jour le fichier .break contenant l’état du flux de tweets.

writeBreak :: Int -> IO ()
writeBreak = writeFile ".break" . show

readBreak :: IO Int
readBreak = do
  exist <- doesFileExist ".break"
  if exist
    then read <$> readFile ".break"
    else pure 0

Il ne reste plus qu’à écrire le main dont le principal travail est d’appeler makeTweets avec le contenu lu depuis l’entrée standard:

main :: IO ()
main = do
  numTweets <- getNumTweets
  getContents >>= makeTweets numTweets

En téléchargeant le Chant I depuis le site officiel de l’éditeur et en le copiant/collant dans un fichier chant1, on peut lancer le programme tweets.hs:

$ cat chant1 | ./tweets.hs 3
Chante, déesse, la colère d’Achille, le fils de Pélée; détestable colère, qui aux Achéens valut des souffrances sans nombre et jeta en pâture à Hadès tant d’âmes fières de héros, #homero2019
tandis que de ces héros mêmes elle faisait la proie des chiens et de tous les oiseaux du ciel – pour l’achèvement du dessein de Zeus. Pars du jour où une querelle tout d’abord divisa le fils d’Atrée, protecteur de son peuple, et le divin Achille. #homero2019
Qui des dieux les mit donc aux prises en telle querelle et bataille ? Le fils de Létô et de Zeus. C’est lui qui, courroucé contre le roi, fit par toute l’armée grandir un mal cruel, dont les hommes allaient mourant; cela, #homero2019

j’aurais aimé pouvoir utiliser tweet-hs pour pouvoir effectivement tweeté ces textes mais malheureusement, le processus pour obtenir des clés OAuth sur twitter est long et pénible et je ne sais pas si j’aurais l’énergie nécessaire pour ce faire…