Journal TapTempo en Scala

Posté par (page perso) . Licence CC by-sa.
Tags :
21
23
juil.
2018

Le sujet TapTempo est très intéressant pour apprendre un nouveau langage de programmation. En effet, il est beaucoup plus complet qu'un simple Hello World ! Il nous impose de se plonger dans les arcanes du langage et de ses outils pour gérer le temps, l'affichage mais aussi les arguments de la ligne de commande ou les structures «complexe» comme les listes ou les buffers.

Bon évidemment, quand il s'agit d'un langage de description hardware comme présenté la dernière fois avec Chisel on évite la ligne de commande ! Par contre, Chisel étant basé sur le langage Scala, il est intéressant de se plonger dedans histoire de le maitriser un minimum.

Voici donc mon humble version de TapTempo en Scala. Langage étrange au paradigme bipolaire (;)) : Fonctionnel et Objet. Ce langage a déjà été décrit dans les colonnes de LinuxFR, il est basé sur une machine virtuelle Java et permet d'appeler les classes et fonctions de ce même langage. Et j'ai appris récemment que la réciproque est également vraie : On peut appeler les classes/fonctions Scala en Java.

    // TapTempo
    import jline.console.ConsoleReader /* to read keyboard */
    import scala.collection._
    import sys.process._ /* shell cmd execution with ! */

    val VERSION = "1.0"
    val PRECISION = 0
    val PRECISIONLIST = List("%.00f", "%.01f", "%.02f", "%.03f", "%.04f", "%.05f")
    val RESET_TIME = 5
    val SAMPLE_SIZE = 5
    val SECOND = 1e9
    val MIN = 60*SECOND

    object TapTempo {
      def usages() {
        println("-h, --help            affiche ce message d'aide")
        println("-p, --precision       changer le nombre de décimale du tempo à afficher")
        println("                      la valeur par défaut est 0 décimales, le max est 5 décimales")
        println("-r, --reset-time      changer le temps en seconde de remise à zéro du calcul")
        println("                      la valeur par défaut est 5 secondes")
        println("-s, --sample-size     changer le nombre d'échantillons nécessaires au calcul du tempo")
        println("                      la valeur par défaut est 5 échantillons")
        println("-v, --version         afficher la version")
      }

      def printversion() {
        println("TapTempo Scala version " + VERSION)
      }

      def tempo(tfifo: mutable.Buffer[Double]):Double = {
        var sum = 0.0
        tfifo.foreach(sum += _)
        sum/tfifo.length
      }

      def main(args: Array[String]) {
        val arglist = args.toList
        type OptionMap = Map[Symbol, Int]

        /****************/
        /* parsing args */
        /****************/
        def nextOption(map : OptionMap, list: List[String]) : OptionMap = {
          def isSwitch(s : String) = (s(0) == '-')

          list match {
            case Nil => map
            case ("-h" | "--help") :: tail => usages(); sys.exit(0)
            case ("-v" | "--version") :: tail => printversion; sys.exit(0)
            case ("-p" | "--precision") :: value :: tail =>
                                   nextOption(map ++ Map('precision -> value.toInt), tail)
            case ("-r" | "--reset-time") :: value :: tail =>
                                   nextOption(map ++ Map('rtime -> value.toInt), tail)
            case ("-s" | "--sample-size") :: value :: tail =>
                                   nextOption(map ++ Map('ssize -> value.toInt), tail)

            case option :: tail => println("Unknown option " + option)
                                   sys.exit(0)
          }
        }
        val options = nextOption(Map(), arglist)

        var precision = options.getOrElse('precision, -1)
        if(precision < 0 || precision > 5)
          precision = PRECISION
        var rtime = options.getOrElse('rtime, -1)
        if(rtime == -1)
          rtime = RESET_TIME
        var ssize = options.getOrElse('ssize, -1)
        if(ssize == -1)
          ssize = SAMPLE_SIZE

        /* Minimum caracters for completed read */
        (Seq("sh", "-c", "stty -icanon min 1 < /dev/tty") !)
        /* Do not print input caracters */
        (Seq("sh", "-c", "stty -echo < /dev/tty") !)

        var timefifo = mutable.Buffer.fill[Double](ssize)(0.0)
        var fifocount = 0

        println("Appuyer sur une touche en cadence (q pour quitter).")
        var c = 0
        var i = 0
        var current_time =  System.nanoTime()
        var old_time = System.nanoTime()
        do {
          c = Console.in.read
          current_time =  System.nanoTime()
          val tempotime = MIN/(current_time - old_time)
          if(tempotime < 60/rtime){
            fifocount = 0
          }
          if(fifocount < ssize){
            fifocount += 1
          }

          timefifo(i) = tempotime
          i = (i + 1) % ssize

          if(fifocount == ssize){
            printf("Tempo : " + PRECISIONLIST(precision) + "\n", tempo(timefifo))
          } else {
            printf("Tempo : %d/%d\n", fifocount, ssize)
          }

          old_time = current_time
        } while (c != 113) // While 'q' is pressed
        println("Bye Bye!")
      }

    }

    TapTempo.main(args)

Voila voila, le programme est également disponible sur mon github (oui oui je sais çémal). Ce petit exercice m'a permis de découvrir un peu plus de ce langage suisse très prisé du monde de la finance.

Mais il y a encore du chemin pour le maîtriser.

À oui j’oubliai, pour le lancer en tant que script faire :

$ scala -nc TapTempo.sc

Patienter un certain temps — hé oui on parle quand même d'une machine virtuel java et de compilation de bytecode, çélong ! — puis taper la touche de votre choix, 'q' pour quitter.

Appuyer sur une touche en cadence (q pour quitter).
Tempo : 1/5
Tempo : 2/5
Tempo : 3/5
Tempo : 4/5
Tempo : 152
Tempo : 186
Tempo : 195
Tempo : 164
Tempo : 159
Tempo : 162
Tempo : 157
Tempo : 156
Tempo : 159
Bye Bye!
  • # Objet et Fonctionnel

    Posté par . Évalué à 6.

    C'est marrant, parce que tu débutes le journal en disant que Scala est un langage Objet et Fonctionnel, mais le programme que tu nous partages est ni objet, ni fonctionnel.

    C'est du code procédural assez classique.

    Ce n'est pas forcément un mal, mais je suis curieux de savoir si la question s'était posée et si il n'y aurait pas d'autres designs plus objet et/ou fonctionnel :)

    • [^] # Re: Objet et Fonctionnel

      Posté par (page perso) . Évalué à 2.

      La réponse est oui, Victor, tu as raison :-) Pas le temps d’élaborer, peut-être l’auteur du journal le fera-t-il…

    • [^] # Re: Objet et Fonctionnel

      Posté par (page perso) . Évalué à 3. Dernière modification le 24/07/18 à 10:03.

      La réponse est oui certainement ;)

      Il serait intéressant de transformer le programme pour qu'il soit plus fonctionnel/objet en effet.

      • [^] # Re: Objet et Fonctionnel

        Posté par . Évalué à 4. Dernière modification le 24/07/18 à 20:25.

        Un truc facile pour commencer c'est de remplacer tous les var par des val. Il y a peut-etre plus simple pour le precision…

        val options = nextOption(Map.empty, arglist)
        
        val precision = options.get('precision)
          .filterNot(option => option < 0 || option > 5)
          .getOrElse(PRECISION)
        
        val rtime = options.getOrElse('rtime, RESET_TIME)
        
        var ssize = options.getOrElse('ssize, SAMPLE_SIZE)
        • [^] # Re: Objet et Fonctionnel

          Posté par . Évalué à 3.

          Voila un precision plus simple:

          val precision = options.get('precision) match {
            case Some(p) if p >= 0 && p <= 5 => p
            case _ => PRECISION
          }
  • # Faut se calmer, là !

    Posté par (page perso) . Évalué à 3. Dernière modification le 24/07/18 à 14:43.

    Une fonction de 76 lignes, ça va pas là !
    Allez, hop, tu nous refais ça avec des fonctions de 5 lignes maximum.

    • [^] # Re: Faut se calmer, là !

      Posté par (page perso) . Évalué à 2.

      Aucune chances, rien que l'affichage du help fait plus de 5 lignes. ;)

      • [^] # Re: Faut se calmer, là !

        Posté par . Évalué à 3. Dernière modification le 24/07/18 à 20:16.

        Ca compte les donnees? Sinon l'affichage du help se reduit a une ligne:

          val HelpMessages = Seq(
            "-h, --help            affiche ce message d'aide",
            "-p, --precision       changer le nombre de décimale du tempo à afficher",
            "                      la valeur par défaut est 0 décimales, le max est 5 décimales",
            "-r, --reset-time      changer le temps en seconde de remise à zéro du calcul",
            "                      la valeur par défaut est 5 secondes",
            "-s, --sample-size     changer le nombre d'échantillons nécessaires au calcul du tempo",
            "                      la valeur par défaut est 5 échantillons",
            "-v, --version         afficher la version")
        
          def usages(): Unit = {
            HelpMessages.foreach(line => println(line))
          }
        • [^] # Re: Faut se calmer, là !

          Posté par (page perso) . Évalué à 4.

          C'est mieux, oui !

          • [^] # Re: Faut se calmer, là !

            Posté par . Évalué à 3.

            pourquoi c'est mieux ?

            • [^] # Re: Faut se calmer, là !

              Posté par (page perso) . Évalué à 2.

              J'avoue que je me pose la même question ;)
              Ma fonction faisait 10 lignes, celle-ci en fait 12 !

              Après peut-être y a-t-il une «loi» de séparation du code fonctionnel et des données que je ne respecte pas en effet.

              • [^] # Re: Faut se calmer, là !

                Posté par . Évalué à 3. Dernière modification le 25/07/18 à 09:45.

                Tu t'en rends pas compte car c'est un petit programme.
                Mais admettons que tu dois afficher le messages d'aides à deux endroits différents dans le code :
                Dans un cas tu dois copier coller tout le texte, dans l'autre tu dois juste copier une ligne
                Admettons que tu t’aperçoives qu'il y a une faute d'orthographe :

                • Dans un cas tu dois corriger à chaque endroit où tu affiches l'aide
                • Dans l'autre il te suffit de corriger une fois le texte
                • [^] # Re: Faut se calmer, là !

                  Posté par . Évalué à 2.

                  J'ai dit des bêtises, mon argument ne tient pas puisque c'est une fonction.

                  Enfin bref c'est mieux de séparer code et message car ce ne sont pas forcément les mêmes personnes qui bossent dessus (par exemple pour une traduction on est obligé d'aller dans le code)

              • [^] # Re: Faut se calmer, là !

                Posté par (page perso) . Évalué à 2.

                J'appellerais pas ça une "loi", mais oui, l'idée de séparer les données du code est plutôt bonne. C'est peut-être un effet secondaire dû à des années de Spring/Guice couplée à un design générique pour gérer le rendu des options (pour générer des manfiles, de la doc audio, etc.).

  • # Régionalisation

    Posté par (page perso) . Évalué à 2. Dernière modification le 25/07/18 à 14:00.

    Chouette un nouveau portage, bravo :-) Il ne manque plus que la régionalisation pour qu'il soit conforme à l'original. Est-ce facile en Scala ?

    • [^] # Re: Régionalisation

      Posté par (page perso) . Évalué à 2.

      Je pense que c'est possible en effet. Déjà en intégrant les propositions de j_m on devrait pouvoir isoler les données (texte) du code.

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.