Programmation Avancée

TP9: un chat dans une monade

Dans ce TP en plusieurs parties, nous allons découvrir la bibliothèque de threads coopératifs Lwt, et finalement l'utiliser pour construire une petite application de messagerie instantanée pair-à-pair,

Pour commencer, il vous faut installer la librairie lwt, ainsi que la librairie lambda-term que nous utiliserons pour gérer les entrées-sorties dans le terminal. Pour cela nous allons utiliser opam, que vous commencerez donc par initialiser si ce n'est déja fait:

opam init
# Opam va vous proposer de modifier votre bashrc
# pour qu'il s'auto-initialise dans chaque shell
# nouvellement ouvert. Il est recommandé d'accepter.
# Pour en profiter, ouvrez ensuite un nouveau shell
# ou faites une initialisation manuelle de votre
# environnement:
eval `opam config env`

Ensuite on demande à opam d'installer lwt localement à votre compte utilisateur:

opam install lwt
opam install lambda-term

Récupérez alors le Makefile qui vous permettra de faire make foo pour construire un binaire à partir d'un unique fichier foo.ml utilisant les modules et l'extension de syntaxe de Lwt. Tester ce système en compilant et exécutant le fichier helloworld.ml.

Partie 1: se familiariser avec lwt

Ci-dessous, un minimum d'information est donné à propos de lwt. En complément, on consultera le manuel officiel.

Les threads coopératifs lwt forment une monade, dont les opérations de base sont Lwt.bind et Lwt.return. Dans le modèle lwt, un thread s'exécute potentiellement dès qu'il est créé. Cependant, il y a quand même une fonction Lwt_main.run qui sert à activer le scheduler lwt jusqu'à ce que le thread passé en argument se termine.

Sur ce noyau, on a des opérations comme Lwt.join pour exécuter deux threads en parallèle. La bibliothèque fournit aussi des tâches primitives, notamment Lwt_io.printf, version non-bloquante / coopérative de printf. (Toutes ces fonctions sont documentées dans le manuel, et je vous invite à ouvrir les pages correspondant à ces modules, par exemple celle pour Lwt_io.)

Dans le fichier hello.ml définir une fonction repete : string -> int -> unit Lwt.t telle que repete s n affiche n lignes numérotées et contenant le message s, avec un délai aléatoire de moins d'une seconde entre chaque ligne.

Exécuter en parallèle deux instances de repete pour dire bonjour dans deux langues de votre choix. Votre application ne devra pas terminer avant que les deux threads aient effectué tous leurs outputs.

Réécrire votre programme en utilisant l'extension syntaxique documentée brièvement sur la première page du manuel lwt. On utilisera notamment le notation lwt x = .. in .. pour le bind, >> pour la séquence, et éventuellement un petit for_lwt.

Partie 2: un jeu palpitant

Les threads lwt peuvent (en général) être "annulés" (interrompus / tués). Cela peut être fait explicitement via la fonction Lwt.cancel, mais aussi implicitement par exemple avec la fonction pick qui, comme join, exécute plusieurs tâches en parallèle, mais dès qu'une tâche termine annule les autres.

Ecrire une fonction timeout : delay:float -> kill:(unit -> 'a t) -> 'a t -> 'a t qui se comporte comme la tâche passée en argument si celle-ci termine avant de délai imposé, et sinon se comporte comme la tâche kill.

Tester avec le jeu suivant (sûrement une super idée de jeu pour téléphone mobile) où l'utilisateur doit répétitivement entrer une ligne avant un timeout d'une seconde:

let kill () =
  Lwt_io.printf "Game over!\n" >>
  Lwt.fail (Failure "over")

let rec f () =
  Lwt_io.printf "Vite, une ligne!\n> " >>
  lwt l = timeout 1. ~kill (Lwt_io.read_line Lwt_io.stdin) in
  Lwt_io.printf "Merci pour ces %d caractères...\n\n" (String.length l) >>
  f ()

let () = Lwt_main.run (f ())

A suivre...

Par ici!