Journal Un print(1 + "3a"), ça nous inspire comment ?

Posté par (page perso) . Licence CC by-sa
Tags : aucun
58
1
avr.
2017

Sommaire

Cher Nal,

Comme il faut bien un minimum de contenu sérieux aujourd’hui, histoire de faire diversion, je vais te parler de mes recherches sur la problématique print(1 + "3a"). Chaque langage/compilateur a sa façon de réagir sur le sujet, sa sensibilité, ses opinions ; des fois c’est juste une question de style, d’autres fois c’est des questions plus profondes qui émergent !

Voici donc les fruits de ces recherches.

Veux‐tu faire un print(1 + "3a"), s’il te plaît ?

Ceux qui te rejettent à l’exécution parce que Num + String, quoi

Ruby 1.8, c’est le point de vue coercition de types :

$ ruby18 -e 'puts 1 + "3a"'
-e:1:in `+': String can't be coerced into Fixnum (TypeError)
        from -e:1

Avec Python, on a droit à la perspective de la surcharge d’opérateur :

$ python -c 'print(1 + "3a")'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'

Pour Racket, c’est est une question bureaucratique de contrat non respecté !

$ racket -e '(printf "~s\n" (+ 1 "3a"))'
+: contract violation
  expected: number?
  given: "3a"
  argument position: 2nd
  other arguments...:
   1

Guile, avare en paroles, se plaint d’un point de vue typage, tout en pointant le vilain "3a" du doigt cependant :

$ guile -c '(display (+ 1 "3a"))'
ERROR: In procedure +:
ERROR: Wrong type: "3a"

Avec Common Lisp (sbcl), on a droit à des hurlements, on a fait une erreur simple de typage ! Et les remontrances n’en finissent pas…

$ sbcl --noinform --non-interactive --eval '(print (+ 1 "3a"))'
Unhandled SIMPLE-TYPE-ERROR: Argument Y is not a NUMBER: "3a"

Backtrace for: #<SB-THREAD:THREAD "main thread" RUNNING {1002C06753}>
0: ((LAMBDA NIL :IN SB-DEBUG::FUNCALL-WITH-DEBUG-IO-SYNTAX))
1: (SB-IMPL::CALL-WITH-SANE-IO-SYNTAX #<CLOSURE (LAMBDA NIL :IN SB-DEBUG::FUNCALL-WITH-DEBUG-IO-SYNTAX) {1002C193FB}>)
2: (SB-IMPL::%WITH-STANDARD-IO-SYNTAX #<CLOSURE (LAMBDA NIL :IN SB-DEBUG::FUNCALL-WITH-DEBUG-IO-SYNTAX) {1002C193CB}>)
3: (PRINT-BACKTRACE :STREAM #<SYNONYM-STREAM :SYMBOL SB-SYS:*STDERR* {10001528B3}> :START 0 :FROM :INTERRUPTED-FRAME :COUNT NIL :PRINT-THREAD T :PRINT-FRAME-SOURCE NIL :METHOD-FRAME-STYLE NIL)
4: (SB-DEBUG::DEBUGGER-DISABLED-HOOK #<SIMPLE-TYPE-ERROR expected-type: NUMBER datum: "3a"> #<unavailable argument>)
5: (SB-DEBUG::RUN-HOOK *INVOKE-DEBUGGER-HOOK* #<SIMPLE-TYPE-ERROR expected-type: NUMBER datum: "3a">)
6: (INVOKE-DEBUGGER #<SIMPLE-TYPE-ERROR expected-type: NUMBER datum: "3a">)
7: (ERROR SIMPLE-TYPE-ERROR :DATUM "3a" :EXPECTED-TYPE NUMBER :FORMAT-CONTROL "~@<Argument ~A is not a ~S: ~2I~_~S~:>" :FORMAT-ARGUMENTS (SB-KERNEL::Y NUMBER "3a"))
8: (SB-KERNEL:TWO-ARG-+ 1 "3a")
9: (+ 1 "3a")
10: (SB-INT:SIMPLE-EVAL-IN-LEXENV (+ 1 "3a") #<NULL-LEXENV>)
11: (SB-INT:SIMPLE-EVAL-IN-LEXENV (PRINT (+ 1 "3a")) #<NULL-LEXENV>)
12: (EVAL (PRINT (+ 1 "3a")))
13: (SB-IMPL::PROCESS-EVAL/LOAD-OPTIONS ((:EVAL . "(print (+ 1 \"3a\"))") (:QUIT)))
14: (SB-IMPL::TOPLEVEL-INIT)
15: ((FLET #:WITHOUT-INTERRUPTS-BODY-83 :IN SAVE-LISP-AND-DIE))
16: ((LABELS SB-IMPL::RESTART-LISP :IN SAVE-LISP-AND-DIE))

unhandled condition in --disable-debugger mode, quitting

jq nous dit que ce qu’on lui demande est tout simplement impossible :

$ echo {} | jq '1 + "3a"'
jq: error (at <stdin>:1): number (1) and string ("3a") cannot be added

Pour K (kona), c’est une erreur de type pour +, mais on n’en saura pas plus.

$ k
K Console - Enter \ for help

  1 + "3a"
type error
1 + "3a"
  ^

J voit là un problème de domaine et nous rajoute des espaces quelque part là où ça coince :

$ jc
   1 + '3a'
|domain error
|   1    +'3a'

Ceux qui te rejettent à l’exécution, mais parce que « a » dans "3a"

Perl 6 voudrait bien essayer de nous faire plaisir, mais avec le « a » après le « 3 », c’est au‐dessus de ces capacités et il nous l’explique avec Unicode à l’appui :

$ perl6 -e 'say 1 + "3a"'
Cannot convert string to number: trailing characters after number in '3⏏a' (indicated by ⏏)
  in block <unit> at -e line 1

Actually thrown at:
  in block <unit> at -e line 1

TCL ne peut pas non plus, mais fait preuve d’un style plus stacktrace, en nous montrant petit à petit le code de la commande englobante :

$ cat main.tcl
puts [expr 1+{3a}] 
$ tclsh8.5 main.tcl
can't use non-numeric string as operand of "+"
    while executing
"expr 1+{3a}"
    invoked from within
"puts [expr 1+{3a}]"
    (file "main.tcl" line 1)

Lua 5.2 essaie aussi, mais n’y arrive pas. Un peu plus sérieux dans le ton :

$ lua52 -e 'print(1 + "3a")'
lua52: (command line):1: attempt to perform arithmetic on a string value
stack traceback:
        (command line):1: in main chunk
        [C]: in ?

Avec le Shell, c’est expéditif :

$ expr 1 + "3a"
expr: non-numeric argument

Ceux qui essaient de te comprendre

Perl 5 sent bien que c’est louche et te le fait savoir, mais se débrouille pour trouver 4 à la fin.

$ perl -wE 'say 1 + "3a"'
Argument "3a" isn't numeric in addition (+) at -e line 1.
4

AWK (BWK awk), trop fort, arrive à faire le calcul sans problèmes :

$ awk 'END{print 1 + "3a"}' /dev/null
4

JavaScript (node), rebelle, fait son original :

$ node -e 'console.log(1 + "3a")'
13a

C n’essaye pas vraiment de comprendre ce que tu voulais faire, mais ne veut pas trop te mettre la pression non plus :

$ cat main.c
#include <stdio.h>

int main(void) {
        printf("%d", 1 + "3a");
        return 0;
}
$ gcc -Wall main.c
main.c: In function 'main':
main.c:4: warning: format '%d' expects type 'int', but argument 2 has type 'char *'
$ clang -Wall main.c
main.c:4:15: warning: format specifies type 'int' but the argument has type 'char *' [-Wformat]
        printf("%d", 1 + "3a");
                ~~   ^~~~~~~~
                %s
1 warning generated.
$ ./a.out
289409602

Ceux qui te rejettent à la compilation

Go, ça parle simplement typage, mais il pointe le vilain du doigt :

$ cat main.go
package main

import "fmt"

func main() {
        fmt.Println(1 + "3a")
}
$ go run main.go
# command-line-arguments
./main.go:6: cannot convert "3a" to type int
./main.go:6: invalid operation: 1 + "3a" (mismatched types int and string)

Avec OCaml, on parle typage, mais ça parle d’expressions, on sent l’esprit système de typage dans le style :

$ cat main.ml
let () = print_int (2 + "3a")
$ ocaml main.ml
File "main.ml", line 1, characters 24-28:
Error: This expression has type string but an expression was expected of type
         int

Pour Haskell (à tryhaskell.org), c’est une question de manque d’instance pour une typeclass, sa façon de parler surcharge d’opérateur :

λ print (1 + "3a")
No instance for (Num [Char]) arising from a use of ‘+’
In the first argument of ‘print’, namely ‘(1 + "3a")’
In the expression: print (1 + "3a")

Rust parle traits, sa façon de parler surcharge, avec au passage quelques histoires de macros pour faire peur aux enfants. Et il nous dit qu’on peut avoir plus de détails en lui posant des questions au sujet d’E0277 (qui vaut le détour, ceci dit : un peu la même idée que perldiag, mais dans un style pédagogique).

$ cat main.rs
fn main() {
    println!("{}", 1 + "3a");
}
$ rustc main.rs
main.rs:2:20: 2:28 error: the trait bound `_: std::ops::Add<&str>` is not satisfied [E0277]
main.rs:2     println!("{}", 1 + "3a");
                             ^~~~~~~~
<std macros>:2:25: 2:56 note: in this expansion of format_args!
<std macros>:3:1: 3:54 note: in this expansion of print! (defined in <std macros>)
main.rs:2:5: 2:30 note: in this expansion of println! (defined in <std macros>)
main.rs:2:20: 2:28 help: run `rustc --explain E0277` to see a detailed explanation
main.rs:2:20: 2:28 help: the following implementations were found:
main.rs:2:20: 2:28 help:   <f64 as std::ops::Add>
main.rs:2:20: 2:28 help:   <&'a f64 as std::ops::Add<f64>>
main.rs:2:20: 2:28 help:   <f64 as std::ops::Add<&'a f64>>
main.rs:2:20: 2:28 help:   <&'b f64 as std::ops::Add<&'a f64>>
main.rs:2:20: 2:28 help: and 58 others
error: aborting due to previous error

Voilà, c’est tout.

P.-S. : Pour ceux qui recherchent des choses à lire plus dans l’esprit du jour, il y a cet article de chez TuxFamily que j’ai trouvé pas mal.

  • # D'autres

    Posté par (page perso) . Évalué à 10. Dernière modification le 01/04/17 à 14:33.

    $ echo $((1 + "3a"))
    bash: 1 + 3a : valeur trop grande pour la base (le symbole erroné est « 3a »)
    zsh: bad math expression: operand expected at `"3a"'
    ksh: 1 + "3a": unexpected '"'
    dash: 1: arithmetic expression: expecting primary: "1 + "3a""
    
    $ echo "<?php print 1+\"3a\" ?>"|php5
    4
    
    $ units
    Currency exchange rates from www.timegenie.com on 2016-06-21 
    2926 units, 109 prefixes, 88 nonlinear units
    
    You have: 1+"3a"
    Unknown unit '"3a"'
    
    $ cat t.cpp 
    #include <iostream>
    int main() {
      std::cout << 1 + "3a" << std::endl;
    }
    $ g++ -Wall -pedantic t.cpp 
    $ ./a.out 
    a
    
    • [^] # Re: D'autres

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

      1 + 3a: valeur trop grande

      Haha, j'adore !

      • [^] # Re: D'autres

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

        J'avais pas bash chez moi, donc je pouvais pas tester, et sur le moment j'avais en fait cru à un poisson :) Et puis non, c'est juste que t'avais oublié les guillemets (c'est censé être une erreur de syntaxe ^^). Du coup, j'ai découvert qu'on pouvait spécifier la base avec $((1 + 2#11)) qui renvoit 4 ;)

    • [^] # Re: D'autres

      Posté par . Évalué à 5. Dernière modification le 01/04/17 à 19:25.

      php -r 'echo 1 + "3a";'
      

      -> 4

       php -r '<code>' 
      

      est bien plus simple

    • [^] # Re: D'autres

      Posté par . Évalué à 3.

      Ton implémentation en c++, c’est ce que j’imaginais en C :

      #include <stdio.h>
      
      int main()
      {
          printf(1+"3a");
          return 0;
      }
      $ gcc main.c
      $ ./a.out
      a$
      
  • # Elm rulez

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

    Voilà ce qui se passe quand on veut compiler une telle addition en Elm

    -- TYPE MISMATCH --------------------------------------------- repl-temp-000.elm
    
    The right side of (+) is causing a type mismatch.
    
    3|   1 + "3a"
             ^^^^
    (+) is expecting the right side to be a:
    
        number
    
    But the right side is:
    
        String
    
    Hint: To append strings in Elm, you need to use the (++) operator, not (+).
    <http://package.elm-lang.org/packages/elm-lang/core/latest/Basics#++>
    
    Hint: With operators like (+) I always check the left side first. If it seems
    fine, I assume it is correct and check the right side. So the problem may be in
    how the left and right arguments interact.
    

    Dieu que j'adore ce langage..

  • # En erlang

    Posté par . Évalué à 6.

    -module(essai).
    -export([essai/0]).
    
    essai()->
      1 + "3a".

    Ca commence par raler à la compilation :

    $ erlc /tmp/essai.erl
    /tmp/essai.erl:5: Warning: this expression will fail with a 'badarith' exception

    et à l'exécution:

    Eshell V8.2 (abort with G)
    1> essai:essai().
    ** exception error: an error occurred when evaluating an arithmetic expression
    in function essai:essai/0 (/tmp/essai.erl, line 5)

  • # Question

    Posté par . Évalué à 8. Dernière modification le 01/04/17 à 21:13.

    Est-ce qu’il existe un langage pour lequel la sortie serait : 59, de type entier donc ? (1 + 0x3A = 0x3B)

  • # Un peu déçu par Rust

    Posté par . Évalué à 5.

    Rust est un langage qui m'attire vraiment beaucoup, mais par manque de temps je ne m'y suis pas encore mis. Jusqu'ici je n'ai vu que des belles choses, et le seul reproche que je lui trouvais, c'était la difficulté à trouver un librairie mature pour faire une application graphique.

    Mais là, je trouve que le message d'erreur est… comment dire… Cryptique. Verbeux. Inutile. Décevant.

    Curieusement, ça m'a décidé à enfin l'installer et commencer à m'amuser avec.

    • [^] # Re: Un peu déçu par Rust

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

      Bonne chance avec les trois (!!) types de String dont je n'ai pas compris l'intérêt pour environ 1,5 d'entre eux.

      • [^] # Re: Un peu déçu par Rust

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

        Rust n'est pas un langage très simple, mais il me semble (je suis juste un peu de loin le langage) qu'ils sont en train de faire des efforts pour le rendre plus accessible (dans la limite du possible avec un langage aussi ambitieux, bien sûr). En cherchant rapidement, je suis tombé sur un article Shape of errors…, donc peut-être que les messages d'erreurs sont plus amicals maintenant (ma version de rust doit dater de mi-2016).

        • [^] # Re: Un peu déçu par Rust

          Posté par . Évalué à 5.

          Avec un rustc plus récent:

          fn main() {
              println!("{}", 1 + "3a");
          }
          $ rustc --version
          rustc 1.15.1 (021bd294c 2017-02-08)
          
          error[E0277]: the trait bound `{integer}: std::ops::Add<&str>` is not satisfied
           --> main.rs:2:20
            |
          2 |     println!("{}", 1 + "3a");
            |                    ^^^^^^^^ the trait `std::ops::Add<&str>` is not implemented for `{integer}`
            |
            = help: the following implementations were found:
            = help:   <u32 as std::ops::Add>
            = help:   <&'a u32 as std::ops::Add<u32>>
            = help:   <u32 as std::ops::Add<&'a u32>>
            = help:   <&'b u32 as std::ops::Add<&'a u32>>
            = help: and 90 others
          
          error: aborting due to previous error
          

          Et le message d"erreur vient avec des couleurs qui rendent la lecture très agréable.

    • [^] # Re: Un peu déçu par Rust

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

      Rust est un langage typé au même titre que Haskell et OCaml. Il faut bien comprendre que les erreurs de types sont difficile à rendre compréhensible. La surcharge possible de l'opérateur ( + ) en Rust complexifie la tâche et pour le coup, même si Haskell serait pour le coup un bonne exemple à suivre pour Rust, en général, c'est avec l'expérience qu'on comprends de manière évidente ces erreurs et on peut difficilement faire mieux.

      Tout ceux qui ont essayer de jouer avec les GADTs en OCaml savent de quoi je parle ('a#0 qui n'arrive pas à s'unifier avec 'a#1) et que cette critique (pertinente) est à re-contextualiser par rapport à ce qu'on est capable de faire aujourd'hui et que la problématique reste encore difficile.

      Mais on peut aisément imaginer qu'avec l'engouement de reste, on est plus d'investissement humain pour résoudre ce problème.

      • [^] # Re: Un peu déçu par Rust

        Posté par . Évalué à 2.

        Rust est un langage typé au même titre que Haskell et OCaml. Il faut bien comprendre que les erreurs de types sont difficile à rendre compréhensible. La surcharge possible de l'opérateur ( + ) en Rust complexifie la tâche et pour le coup,

        Python permet la surcharge de l’opérateur + (la fonction correspondante s’appelle __add__) et son message est on ne peut plus clair.

        même si Haskell serait pour le coup un bonne exemple à suivre pour Rust, en général, c'est avec l'expérience qu'on comprends de manière évidente ces erreurs et on peut difficilement faire mieux.

        En Haskell ça me semble un peu mieux mais c’est encore un peu « lourd », àmha.

        Le message de Guile au contraire me plaît par sa concision.

        C’est sûr que ça reste à mon avis une question de goût et comme tu le dis :

        c'est avec l'expérience qu'on comprends de manière évidente ces erreurs et on peut difficilement faire mieux.

        et une erreur de type c’est assez courant pour qu’on la reconnaisse du premier coup d’œil, même avec peu d’expérience. De manière générale on s’habitue à la tronche des messages. Du coup, même avec des messages d’erreur longs comme le bras, on sait tout de suite quelle ligne regarder.

        Ça reste pénible quand on débute dans un langage.

        • [^] # Re: Un peu déçu par Rust

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

          Python permet la surcharge de l’opérateur + (la fonction correspondante s’appelle add) et son message est on ne peut plus clair.

          Oui enfin Python n'a pas un algorithme d'unification derrière … Il faut comparer ce qui est comparable aussi (c'est pour ça que je compare pas cette problématique spécifique avec OCaml). Ce domaine demande encore pas mal de travail comme le prouve ce papier de recherche du OCaml Workshop 2014 notamment (si on prends la problématique dans sa globalité). Donc cette condescendance que Python y arrive mieux que Rust, non merci.

          • [^] # Re: Un peu déçu par Rust

            Posté par . Évalué à 2.

            Donc cette condescendance que Python y arrive mieux que Rust, non merci.

            Désolé d’avoir pu paraître condescendent ce n’est que de l’ignorance de ma part, merci pour ta réponse.

          • [^] # Re: Un peu déçu par Rust

            Posté par . Évalué à 2. Dernière modification le 03/04/17 à 09:33.

            Donc cette condescendance que Python y arrive mieux que Rust, non merci.

            Après… Il y a un autre facteur qui rentre en compte, rust et haskell sont des langages jeunes, et qui s'adressent à une communauté de développeurs assez particulière, et que je ne saurais pas caractériser. Alors qu'à l'inverse, python est plus tout jeune, et qu'il s'adresse à tout un chacun.
            À ce titre, un soin particulier à été apporté aux messages d'erreur de cpython. Alors que pour ghc (le compilateur haskell le plus utilisé) et l'implémentation de rust (dont j'ignore le nom), les messages d'erreurs sont très secondaires.

            Et oui, détecter les problèmes de types c'est beaucoup plus difficile en haskell et en rust qu'en python. La raison c'est que les deux premiers les détecte au plus tôt, et python, au dernier moment. Dit autrement, haskell et rust vont analyser les raisons qui feront que le programme merdera, alors que python se contentera de détecter le merdage (c'est pas mal déjà).

            • [^] # Re: Un peu déçu par Rust

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

              rust et haskell sont des langages jeunes

              Il me semble pourtant que Haskell et Python ont été créés à peu près la même année non ?

              • [^] # Re: Un peu déçu par Rust

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

                Hmm avec un première version datant de 1990 et une première implémentation du compilateur GHC datant de 1992, on peut certainement pas dire que Haskell est un langage aussi jeune que Rust.

                Quelques dates, que j'ai recopiées de Wikipedia:

                • 1957 Fortran
                • 1958 Lisp
                • 1959 COBOL
                • 1972 C
                • 1983 C++
                • 1984 Common Lisp, Objective C
                • 1985 Caml
                • 1987 Perl
                • 1991 Python
                • 1992 Haskell
                • 1995 Java, JavaScript, PHP, Ruby
                • 1996 OCaml
                • 2010 Rust
              • [^] # Re: Un peu déçu par Rust

                Posté par . Évalué à -3. Dernière modification le 03/04/17 à 13:50.

                rust et haskell sont des langages jeunes

                Il me semble pourtant que Haskell et Python ont été créés à peu près la même année non ?

                Oui, mais haskell c'était fils attardé de la fratrie, python permettait de faire des trucs de fous, alors qu'Haskell était toujours en mode « GNEEEEEEEEE "world!hello "… j'ai encore raté mon print :'( ».

                Mais en fait, il était pas attardé, juste lent. 20 ans plus tard, python à l'air échappé des années 80 (80 c'est pour l'objet, si on enlève ça, c'est échappé les années 50) mais il sait bien s'habiller, il présente relativement bien, il a l'air moderne et "user friendly". Et Haskell est devenu l'un des langage les plus beaux qui soient.

          • [^] # Re: Un peu déçu par Rust

            Posté par . Évalué à 3.

            Ce n'est pas pour autant impossible de faire un message clair: en pratique le message de Rust

            the trait bound _: std::ops::Add<&str> is not satisfied [E0277]

            contient exactement la même information que le message en Python :

            TypeError: unsupported operand type(s) for +: 'int' and 'str'

            À savoir qu'il n'y a pas d'opérateur défini pour l'opération entre un nombre et une string. On pourrait donc imaginer un message d'erreur dans ce style là :

            TypeError: unsupported operand type(s) for +: {integer} and '&str'
            the trait bound {integer}: std::ops::Add<&str> is not satisfied [E0277]

            • [^] # Re: Un peu déçu par Rust

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

              contient exactement la même information que le message en Python :

              Pas tout-à-fait quand-même, je vois deux différences importantes:

              • le message de Rust ne fait pas référence au type entier contrairement à celui de Python
              • le message de Rust parle de std::ops::Add alors que le programmeur a écrit +, alors que Python utilise le signe +.

              Le message de Rust est un peu plus difficile à comprendre il faut savoir (ou dans mon cas,deviner) que dans les message d'erreur:

              main.rs:2:20: 2:28 error: the trait bound `_: std::ops::Add<&str>` is not satisfied [E0277]
              main.rs:2     println!("{}", 1 + "3a");
                                           ^~~~~~~~
              

              le _ renvoie à la classe ou au type du bidule souligné, le 1 dans l'expression problématique et que le + est traduit en un appel à une méthode Add lorsque cela est possible.

              Le message de Python se lit tout seul.

              • [^] # Re: Un peu déçu par Rust

                Posté par . Évalué à 3.

                contient exactement la même information que le message en Python :

                Cette phrase était en réponse à Dinosaure qui disait :

                Oui enfin Python n'a pas un algorithme d'unification derrière … Il faut comparer ce qui est comparable aussi (c'est pour ça que je compare pas cette problématique spécifique avec OCaml).

                Mon argument était juste de dire que du point de vue de l'information disponible (pour le compilateur qui va générer le message) il n'y a rien qui manque en Rust qui lui permette de générer un message plus compréhensible.

                Parce que clairement on est d'accord, le message en Rust est abscons tandis que le message Python est très clair.

          • [^] # Re: Un peu déçu par Rust

            Posté par . Évalué à 5.

            Certes, mais même lorsque l'on fait du typage statique et que l'on utilise un algorithme d'unification, il est possible de renvoyer des messages plus compréhensibles. Le papier auquel tu renvoies donne des pistes, mais le message de Guillaume Denry sur elm est encore plus parlant. Elm a un système de type à la Haskell et son message d'erreur est on ne peut plus compréhensif et informatif.

            Comme ces derniers temps je joue un peu avec Coq, qui doit faire face à des problématiques d'unification à coté desquelles celles de Rust sont un jeu d'enfant, je vais donner des exemples dans ce langage.

            Pour commencer, il semblerait naturel de n'utiliser une notation infixe x + y que dans une structure de monoïde. Alors je vais le faire façon type classes à la Haskell :

            Class monoid {A : Type} (op : A -> A -> A) (e : A) := {
              _ : forall x y z, op x (op y z) = op (op x y) z ;
              _ : forall x, op e x = x ;
              _ : forall x, op x e = x }.
            
            Generalizable Variables A op e.
            
            Definition monoid_op `{M : monoid A op e} x y := op x y.
            
            Infix "+'" := monoid_op (at level 50).

            Là on fait un type checking sur la fonction monoid_op :

            Check monoid_op.
            (* ==>
            monoid_op
                 : ?A -> ?A -> ?A
            where
            ?A : [ |- Type] 
            ?op : [ |- ?A -> ?A -> ?A] 
            ?e : [ |- ?A] 
            ?M : [ |- monoid ?op ?e] *)

            Elle prend deux valeurs d'un certain type ?A et renvoie une valeur de ce type, et cela dans un contexte où ce type est connu avec un monoïde sur les valeurs de ce type (le ? signifie que le terme est indéterminé, la variable est existentielle : il existe une valeur A telle que …). Si on lui demande de typer l'expression 1 +' "3a", on obtient :

            Check 1 +' "3a".
            (* ==>
            Error:
            The term ""3a"" has type "string" while it is expected to have type "nat". *)

            Mais le plus amusant est qu'il peut typer le terme "1" +' "3a" avant même de savoir le calculer :

            Check "1" +' "3a".
            (* ==>
            "1" +' "3a"
                 : string
            where
            ?op : [ |- string -> string -> string] 
            ?e : [ |- string] 
            ?M : [ |- monoid ?op ?e] *)
            
            Eval compute in "1" +' "3a".
            (* ==>
                 = ?op "1" "3a"
                 : string *)

            Maintenant pour lui permettre de calculer, on lui fournit une instance de la classe :

            (* on prouve que le type string muni de l'opération de concaténation
               est un monoïde dont l'élément neutre est la chaîne vide. *)
            Instance Str_append : monoid append "".
            Proof.
            split.
            - induction x; [auto | intros; simpl; rewrite IHx; reflexivity].
            - reflexivity.
            - induction x; [auto | simpl; rewrite IHx; reflexivity].
            Qed.
            
            (* maintenant on vérifie le type et on évalue : *)
            Check "1" +' "3a".
            (* ==>
            "1" +' "3a"
                 : string *)
            
            Eval compute in "1" +' "3a".
            (* ==>
                 = "13a"
                 : string *)

            Pour le cas plus général où les deux opérandes ne sont pas du même type (et l'on se demande bien alors pourquoi utiliser la notation additive ?), et revenir à la situation de python, je vais l'illustrer ainsi.

            Class bin_op (A B C : Type) := op : A -> B -> C.
            
            Infix "+'" := op (at level 50).
            
            Check 1 +' "3a".
            (* ==>
            1 +' "3a"
                 : ?C
            where
            ?C : [ |- Type] 
            ?bin_op : [ |- bin_op nat string ?C] *)

            Là on est dans la situation de python : comme il fait du typage dynamique, lors de l'appel au calcul de l'expression il vérifie si l'environnement permet de bien typer le terme (s'il peut instancier ces variables existentielles). Comme ce langage permet de modifier dynamiquement l'environnement, il est impossible de refuser statiquement ce code. Il pourrait, par exemple, être défini dans une bibliothèque où le code ne peut être évalué, puis importer dans un code qui rend l'évaluation possible. Comme dans le dialogue suivant avec la REPL Coq :

            Eval compute in 1 +' "3a".
            (* ==>
                 = ?bin_op 1 "3a"
                 : ?C *)
            
            (* ici l'évaluation totale génère une erreur : le typage étant statique,
               il exige d'être dans un environnement où les variables existentielles
               sont instanciées. *)
            Eval vm_compute in 1 +' "3a".
            (* ==>
            Error: vm_compute does not support existential variables. *)
            
            Instance nat_add : bin_op nat nat nat := {op := plus}.
            Instance str_app : bin_op string string string := {op := append }.
            Instance nat_str : bin_op nat string string := {
              op n s:=
                match get n s with 
                | None => ""
                | Some c => String c ""
                end }.
            
            Eval vm_compute in 1 +' 3. (* ==> 4 : nat *)
            Eval vm_compute in "1" +' "3a". (* ==> "13a" : string *)
            Eval vm_compute in 1 +' "3a". (* ==> "a" : string, à la manière du C *)

            Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

            • [^] # Re: Un peu déçu par Rust

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

              Une bonne occasion de me mettre au Coq (même si je continue à dire qu'il n'y a que les fous pour utiliser ce langage :p ).

              • [^] # Re: Un peu déçu par Rust

                Posté par . Évalué à 4.

                Sinon, à mi-chemin entre Coq et des langages de programmation généralistes, tu as Idris. En plus il vient juste de sortir en version 1.0, ce qui stabilise pas mal de trucs.

                • [^] # Re: Un peu déçu par Rust

                  Posté par . Évalué à 3.

                  J'ai regardé un peu la documentation de Idris et je ne suis pas convaincu que cela simplifie les choses par rapport à Coq.

                  Une des fonctionnalité centrale des langages avec types dépendants (les types sont des citoyens de première classe, comme les fonctions le sont dans les langages fonctionnels) est de pouvoir prouver des théorèmes de spécification et de correction sur les programmes. Or quand je lis les deux exemples de la documentation : l'addition est associative sur les entiers et l'addition est commutative, je ne trouve pas cela plus simple. Il faut écrire un module dans un fichier avec une preuve laissée indéterminée, chargé le module dans la REPL, utiliser les tactiques pour effectuer la preuve puis recopier le script de preuve dans le fichier.

                  Avec CoqIde, pour la preuve d'associativité j'écris cela dans un fichier :

                  (* je remets la définition du type nat et de la fonction plus *)
                  Inductive nat :=
                    | O : nat
                    | S : nat -> nat.
                  
                  Fixpoint plus n m :=
                    match n with
                    | O => m
                    | S p => S (plus p m)
                    end.
                  
                  Infix "+" := plus.
                  
                  (* pour ce qui est des tactiques utilisées dans la preuve,
                     j'utilise les même que dans la documentation de Idris *)
                  Theorem plus_assoc : 
                    forall n m p, n + m + p = n + (m + p).
                  Proof.
                    induction n.
                    - simpl. trivial.
                    - simpl. intros. rewrite IHn. trivial.
                  Qed.

                  Les preuves en Coq ou Idris peuvent se paraphraser ainsi :

                  On prouve le théorème par récurrence sur l'entier n.

                  • Si n = 0 alors après simplification il faut prouver que m + p = m + p ce qui est trivial.
                  • Ensuite si n = S n' alors il faut montrer S n' + m + p = S n' + (m + p), ce qui revient, par définition de la fonction plus, à montrer que S (n' + m + p) = S (n' + (m + p)) (c'est là l'appel à la tactique simpl qui effectue des réductions de calcul à partir de la définition de plus). Or, d'après l'hypothèse de récurrence on a n' + m + p = n' + (m + p), il suffit donc de montrer S (n' + (m + p)) = S (n' + (m + p)) (là c'est la série intros. rewrite IHn) ce qui est trivial.

                  Avec un peu de pratique, on peut factoriser les parties communes entres les deux cas de la preuve par récurrence est aboutir à cette preuve :

                  Theorem plus_assoc : 
                    forall n m p, n + m + p = n + (m + p).
                  Proof.
                    induction n; simpl; [|intros; rewrite IHn]; trivial.
                  Qed.

                  Les preuves se construisent pas à pas comme décrit dans la documentation de Idris et elles ne semblent pas plus compliquées à effectuer. Je dirais même que vu l'éventail de tactiques disponibles de base avec Coq, elle sont même sans doute plus simple. Exemple :

                  Require Import Omega.
                  Theorem thm1 :
                    forall n m p, n <= m -> p + n <= p + m.
                  Proof.
                    intros; omega.
                  Qed.

                  Ensuite, il y a un point qui me chagrine dans l'article de présentation de Edwin Brady. Au paragraphe 3.5 page 14, il écrit :

                  3.5 Metatheory
                  We conjecture that TT respects the usual metatheoretic properties, as shown in Figure 5.

                  Ici c'est moi qui graisse, parce que pour le Calcul des Constructions Inductives (qui est à Coq ce que TT est à Idris) on n'en est pas au stade de la conjecture mais d'un résultat établi. Est-ce bien une conjecture pour TT ou le résultat a été prouvé ?

                  De plus, même si je peux comprendre le choix fait pour contrôler si les fonctions définies sont totales ou partielles (paragraphe 3.6), il est plus strict que celui de Coq qui permet de définir des fonctions totales qui seront considérées (à tort) comme partielles par Idris (d'un autre côté en Coq il n'y a pas le choix : les fonctions sont toutes totales).

                  Pour apprendre en douceur et progressivement la programmation en Coq lorsque l'on vient de la programmation fonctionnelle sans type dépendant (Haskell ou OCaml, par exemple), à mon avis le plus simple est de lire le livre (en anglais) Software Foundations de Benjamin C. Pierce disponible en ligne avec le code associé. D'ailleurs le livre est écrit en Coq et la version htlm est générée via l'utilitaire coqdoc.

                  Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

        • [^] # Re: Un peu déçu par Rust

          Posté par (page perso) . Évalué à 5. Dernière modification le 03/04/17 à 12:18.

          et une erreur de type c’est assez courant pour qu’on la reconnaisse du premier coup d’œil, même avec peu d’expérience.

          Si on sort des exemples bétas 1 + "two" et bien non les erreurs de type ne sont pas faciles à repérer à la lecture. Ces temps-cis je travaille sur un code NodeJS qui lit depuis une base de données. La BDD a des capacités de tri limitées et l'API que j'implémente doit aussi proposer du tri et de la réduction de données (un peu comme le group by de SQL pour faire vite). Dans le code existant, la lecture de la BDD produit un flot d'items, mais pour pouvoir profiter des capacités de tri de celle-ci, j'ai décidé de changer en un “flot de pages d'items peut-être triées” ce qui simplifie beaucoup la communication entre les modules de l'application. Un langage comme OCaml ou Haskell permet au compilateur de détecter automatiquement toutes les zones de code affectées par ce changement de type. JavaScript ou les langages typés dynamiquement en général n'apportent simplement aucune aide pour résoudre ce problème – qui est pourtant le pain quotidien du programmeur non?

          Autre exemple plus béta, on ne reconnaît pas forcément du premier coup d'œil qu'il fallait écrire if("names" in params) plutôt que if(names in params) et le compilateur d'un langage typé statiquement détecte l'erreur instantanément. Dans mon application NodeJS avec du code asynchrone, je me retrouve avec une exception et une pile d'erreur inutilisable car toutes les fonctions référencées sont des callbacks d'une bibliothèque pour la programmation asynchrone et donc pas du code utilisateur.

          Je n'ai donc aucun mal a percevoir l'intérêt du typage statique.

          • [^] # Re: Un peu déçu par Rust

            Posté par . Évalué à 2.

            Si tu veux du typage statique en Node (ou JavaScript en général), tu peux utiliser flow: tu rajoutes juste des annotations de type à ton JavaScript et un compilateur (écrit en OCaml) va vérifier que ton typage est correcte. Il est même capable de faire un peu d'inférence de type (mais pas dans tous les cas je crois). Le système de type est assez complet, avec des génériques, des types sommes et des objets non-nullables par défaut. Ce qui est bien c'est que ton code reste du JavaScript, contrairement a TypeScript, qui t'oblige à changer ton utilisation du langage.

            • [^] # Re: Un peu déçu par Rust

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

              J'ai entendu parler de flow mais je ne l'ai jamais encore essayé. Je n'en attends pas grand chose, en Javascript il y a beaucoup d'obstacles qui empêchent le système d'être très pertinent, et j'avais discuté avec un des développeurs du projet qui avait présenté flow à React en 2016 (à Paris) qui avait plus ou moins confirmé mes craintes. L'idée d'annoter les types me semble vider un peu le concept de son intérêt puisque si justement les types étaient notés explicitement on pourrait retrouver relativement facilement les endroits affectés par un changement de type – même si un système automatique peut sûrement apporter son aide. Mais j'essaierai quand-même de voir ce que ça donne.

              Je rappelle pour ceux qui lisent que OCaml ou Haskell déduisent les types et vérifient leur cohérence sans aucune annotation de l'utilisateur. En pratique on écrit souvent ces types déduits dans un fichier, mais le système est capable de générer un canvas qu'il suffit de compléter.

              • [^] # Re: Un peu déçu par Rust

                Posté par . Évalué à 1.

                Je rappelle pour ceux qui lisent que OCaml ou Haskell déduisent les types et vérifient leur cohérence sans aucune annotation de l'utilisateur.

                Ils peuvent le faire, mais (dans la communauté Haskell du moins) il est généralement recommandé de mettre des annotations de type au niveau des fonctions, dans un but de documentation du code et pour expliciter les invariants, même si le compilateur serait capable de s'en passer.

                Par exemple :

                When writing our own functions, we can choose to give them an explicit type declaration. This is generally considered to be good practice except when writing very short functions.

                http://learnyouahaskell.com/types-and-typeclasses
                Qu'on peut traduire par : En écrivant nos propres fonctions, on peut choisir de déclarer explicitement leur type. C'est généralement considéré comme une bonne pratique, sauf dans le cas de fonctions très courtes.

                • [^] # Re: Un peu déçu par Rust

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

                  (dans la communauté Haskell du moins) il est généralement recommandé de mettre des annotations de type au niveau des fonctions

                  C'était la phrase “mais en pratique…” du commentaire auquel tu réponds ;)

              • [^] # Re: Un peu déçu par Rust

                Posté par . Évalué à 1.

                Je rappelle pour ceux qui lisent que OCaml ou Haskell déduisent les types et vérifient leur cohérence sans aucune annotation de l'utilisateur. En pratique on écrit souvent ces types déduits dans un fichier, mais le système est capable de générer un canvas qu'il suffit de compléter.

                OCaml sait faire ça, pas Haskell :

                data Foo
                data Bar
                class Claaaasse a where
                  claaaasse :: a -> IO ()
                instance Claaaasse Foo where
                  claaaasse _ = putStrLn "Je suis Foo"
                instance Claaaasse Bar where
                  claaaasse _= putStrLn "Je suis Bar"
                
                main = claaaasse undefined

                Le compilo répond :

                 Ambiguous type variable ‘a0’ arising from a use of ‘claaaasse’
                      prevents the constraint ‘(Claaaasse a0)’ from being solved.
                      Probable fix: use a type annotation to specify what ‘a0’ should be.
                      These potential instances exist:
                        instance Claaaasse Bar -- Defined at foo.hs:7:10
                        instance Claaaasse Foo -- Defined at foo.hs:5:10
                    • In the expression: claaaasse undefined
                      In an equation for ‘main’: main = claaaasse undefined
                
                

                En précisant le type de undefined ça marche.

                • [^] # Re: Un peu déçu par Rust

                  Posté par . Évalué à 2.

                  En même temps, undefined pouvant se faire passer pour n’importe lequel, quel critère proposes-tu pour que le compilateur détermine le bon type à la compilation ?

                  • [^] # Re: Un peu déçu par Rust

                    Posté par . Évalué à 3.

                    En même temps, undefined pouvant se faire passer pour n’importe lequel, quel critère proposes-tu pour que le compilateur détermine le bon type à la compilation ?

                    C'était pas pour critiquer Haskell, c'était juste pour pour souligner cette différence entre Haskell et OCaml.
                    Le fait est que le système de type de Haskell est beaucoup plus puissant que celui d'OCaml, et ce n'est pas gratuit…

                    Le problème en l’occurrence ne vient pas de l'existence du "undefined" d'haskell, je m'en suis servi ici pour simplifier l'exemple. J'aurais pu écrire :

                    class Num a => Claaaasse a where
                      claaaasse :: a -> IO ()
                    instance Claaaasse Int where
                      claaaasse _ = putStrLn "Je suis un Int"
                    instance Claaaasse Integer where
                      claaaasse _= putStrLn "Je suis Integer"
                    main = claaaasse 1

                    En fait, un programme aussi simple que :

                    main = print 1

                    Soulève le problème. "1" a un type polymorphe : Num t => t, autrement dit, est que c'est un Int, un Integer ? un Float, un Rational ? Un entier de peano défini par l'utilisateur ? Bon, le compilo n'est pas si chiant, il compile quand même, mais avec les warning il précise quand même qu'il "Defaulting the following constraints to type ‘Integer’"

                    Il y a un mécanisme de résolution par défaut dont je ne connais pas le fonctionnement.

                    • [^] # Re: Un peu déçu par Rust

                      Posté par . Évalué à 2. Dernière modification le 04/04/17 à 20:13.

                      C'était pas pour critiquer Haskell, c'était juste pour pour souligner cette différence entre Haskell et OCaml.

                      Ce n'est pas vraiment une différence entre OCaml et Haskell, en OCaml aussi les usages avancés du système de type requièrent des annotations de type explicite: que ce soit au niveau des modules et foncteurs, des types algébriques généralisés, du polymorphisme de second ordre, ou de la récursion polymorphique.

                      Il est peut être plus courant de se heurter à ces problèmes en Haskell, mais le problème sous-jacent reste le même: au-delà d'un certain niveau d'expressivité du système de type, l'inférence devient indécidable, et le compilateur a besoin que le programmeur explicite son intention.

                      • [^] # Re: Un peu déçu par Rust

                        Posté par . Évalué à 1.

                        Ce n'est pas vraiment une différence entre OCaml et Haskell, en OCaml aussi les usages avancés du système de type requièrent des annotations de type explicite: que ce soit au niveau des modules et foncteurs, des types algébriques généralisés, du polymorphisme de second ordre, ou de la récursion polymorphique.

                        Tu as un exemple de programme qui compile pas sans annotation ?

                        Je connais beaucoup mieux Haskell que OCaml.

                        • [^] # Re: Un peu déçu par Rust

                          Posté par . Évalué à 3.

                          Au niveau du système de module, des annotations de types sont nécessaires pour les arguments des foncteurs:

                          module F(X:sig type t end (* ← ceci est une annotation de type *)) = 
                          struct include X end (* mais le type (i.e. la signature) de retour est inferré *)

                          Les modules récursifs eux ont besoin d'être totalement annoté, par exemple:

                          module rec M : (* ↓ annotation du type de M *)
                          sig
                            type t = A of S.t  
                            val compare: t -> t -> int
                          end = struct
                            let compare = compare
                            type t = A of S.t
                          end
                          and S : (Set.S with type elt = M.t) (* ← annotation du type de S *) 
                          = Set.Make(M);;

                          Ce besoin d'annotation pour les modules peut se propager dans les fonctions à cause
                          des modules locaux (ou de première classe).

                          let dedup (type a (* ←soit un type a abstrait*) ) l = 
                            (* ici on a besoin d'introduire un type a pour le module S définit ci-dessous *)
                            let module S = Set.Make(struct type t = a let compare = compare end) in
                            S.elements @@ S.of_list l

                          Un autre cas classique d'annotation explicite apparaît pour la récursion polymorphique
                          sur les types récursifs non homogènes:

                          type 'a arbre_complet = Feuille of 'a | Noeud of ('a * 'a) arbre_complet
                          let rec hauteur: 'a.(*← ∀a *) 'a arbre_complet -> int = function
                            | Feuille _ -> 0
                            | Noeud a ->
                            (* dans cette branche, hauteur est appliqué à ('a * 'a) arbre_complet *) 
                              1 + hauteur a

                          Dans ce cas, l'annotation 'a.'a arbre_complet -> int qui équivaut à forall 'a.…
                          est nécessaire car le type du paramètre 'a à l'intérieur de la définition de hauteur et à l'extérieur diffère.

                          Pour le polymorphisme de second ordre, OCaml ne supporte pas directement les annotations forall à l'intérieur des arguments de fonctions, il faut donc passer soit par des types enregistrements(records):

                          type id = { f:'a. 'a -> 'a }

                          ce qui est plus contraignant qu'une annotation directe, soit par le système objet

                          let pair (id: <f:'a.'a -> 'a>) = id#f 1, id#f []

                          Sans cette annotation le typeur va essayer d'unifier le type de 1 et [] et renvoyer une erreur.

                          Les GADTs de leur côté sont toujours lourds en annotations en Haskell ou OCaml

                             type 'a monoid =
                            | R: float monoid
                            | N: int monoid  
                            | Pair: 'a monoid * 'b monoid -> ('a * 'b) monoid
                          
                            let rec zero: type a. (*← ∀a, a abstrait *) a monoid -> a = function
                              | R -> 0. (* ← dans cette branche, a = float *)
                              | N -> 0  (* ← a = int *)
                              | Pair (a,b) -> zero a, zero b (* a = b * c *)

                          Enfin, une particularité d'OCaml du aux effets de bords, ce sont les types faiblement polymorphiques, qui ne peuvent pas être exporté par une unité de compilation, ce qui peut nécessiter d'ajouter des annotations de type pour les éliminer. Par exemple, un fichier a.ml

                            let x: int list ref = ref []

                          ne compile pas sans son annotation de type.

  • # Re : Journal Un print(1 + "3a"), ça nous inspire comment ?

    Posté par . Évalué à 5.

    En résumé : il existe beaucoup trop de langages de programmation…

  • # Merci

    Posté par . Évalué à 2.

    Merci anaseto pour cette grosse tranche de rigolade au réveil !
    Ça me confirme dans mon idée que j'aime vraiment, vraiment beaucoup les messages d'erreur de Clang.

  • # Let's talk about JavaScript

    Posté par . Évalué à 8.

  • # R

    Posté par . Évalué à 4.

    1+"3a"

    Error in 1 + "3a" : argument non numérique pour un opérateur binaire

  • # Java

    Posté par . Évalué à 8.

    Personne pour du Java ?

    public class BadPrint
    {
        public static void main(String[] args)
        {
            System.out.println(1 + "3a");
        }
    }
    $ java BadPrint
    13a
    • [^] # Re: Java

      Posté par . Évalué à 2.

      On parle de langage, pas de bloatware.

    • [^] # Re: Java

      Posté par . Évalué à 2.

      C’est marrant je viens de faire le test juste avant de lire ton message parce que j’avais en tête des messages d’erreur à l’exécution valant le détour en Java… mais non, ça marche.

      Comme Javascript ça "cast" silencieusement vers string, c’est sûr que si on (int,str) alors qu’on attend soit (int,int) soit (str,str), choisir le deuxième, donc faire un int → str c’est être sûr que ça va fonctionner :)

    • [^] # Re: Java

      Posté par . Évalué à 3.

      le grand cousin C# avec la console "C# interactive" de Visual Studio 2015 :

      Chargement du contexte de 'CSharpInteractive.rsp'.
      Tapez "#help" pour plus d'informations.
      > System.Console.WriteLine(1 + "3a");
      13a

      Pas drôle.. :(

      "Quand certains râlent contre systemd, d'autres s'attaquent aux vrais problèmes." (merci Sinma !)

      • [^] # Re: Java

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

        C'est assez largement utilisé en C#, le + est l'opérateur de concaténation de chaîne et la classe de base Object a un opérateur ToString : ca fonctionnera donc toujours.

  • # SBCL

    Posté par . Évalué à 6.

    SBCL a été assez clair : il indique que "3a" n'est pas un nombre.

    Après il affiche la pile d'appels, c'est souvent utile quand un programme plante. Par contre la pile n'est pas des plus claires, surtout que sur un petit exemple comme ça elle contient plus de machinerie de SBCL que de fonctions du développeur. On voit quand même qu'elle contient les valeurs des paramètres passés aux fonctions (trame 9 notamment), ce qui est parfois utile.

    Sinon j'avais lu des éloges sur Elm et ses messages d'erreur, joli…

  • # Eiffel

    Posté par . Évalué à 2.

    make
        do
            --| Add your code here
            print (1 + "3a")
        end

    Error code: VWOE

    Error: invalid Operator_expression.
    What to do: make sure that equivalent dot form is valid Call.

    Class: APPLICATION
    Feature: make
    Formal type: INTEGER_32
    Actual type: attached STRING_8
    Line: 21
    --| Add your code here
    -> print (1 + "3a")
    end

  • # En Scala

    Posté par . Évalué à 2.

    println(1 + "3a")
    13a

    Le Scala élève le type du Integer vers Any et peux les concaténer via :

    /** A wrapper class that adds string concatenation `+` to any value */
    final class StringAdd(val self: Any) extends AnyVal {
      def +(other: String) = String.valueOf(self) + other
    }
    • [^] # Re: En Scala

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

      Le Scala élève le type du Integer vers Any et peux les concaténer via :

      C'est pas tout à fait ça, toute classe hérite de Any en Scala, et ce code est l'ancienne version et il manque la méthode implicite de conversion qui allait avec :

      implicit def any2stringadd(x: Any): StringAdd

      Maintenant ça utilise les implicit class (qui datent de 2.10 je crois ?) pour tout faire en une fois dans Predef :

      implicit final class any2stringadd[A](private val self: A) extends AnyVal {
          def +(other: String): String = String.valueOf(self) + other
      }

      Y a rien de magique, ce qu'il se passe c'est que le compilateur scala cherche la méthode +(other: String) sur Int, ne la trouve évidemment pas, alors il cherche (entre autres ; pour l'ancienne version il cherchait une méthode implicite prenant en unique paramètre un truc permettant de passer des Int, et ayant pour type de retour un truc ayant une méthode +(other: String)) une classe qui soit implicite, à un seul paramètre de constructeur (de type permettant de passer un Int, ce qu'est Any puisque tout objet est un Any), et disposant d'une méthode +(other: String), ce que any2stringadd est.

      Donc il remplace ton 1 + "3a" par :

      new any2stringadd(1).+("3a")

      Et (rien à voir avec ce qui permet de concaténer des carottes et des String, c'est orthogonal) le extends AnyVal fait de any2stringadd une value class, c'est à dire une classe wrapper que le compilo est encouragé à virer dans le code final. Donc là en l'occurence ce qui arrive à la fin c'est que + est "inlinée" et ça donne au final :

      String.valueOf(1) + "3a"
  • # Le but ?

    Posté par . Évalué à 6.

    Salut,

    J'ai du mal à comprendre ce que tu essaies de montrer/calculer.

    Il y a d'abord pour moi les langages dans le "+" n'est que l'opérateur d'addition des nombres. Ceux là, doivent renvoyer une erreur de type : Shell, Php, Perl, Elm…

    Il y a certains langages dont le "+" est la fois l'opérateur d'addition des nombres et l'opérateur de concaténation des chaînes de caractères.

    Parmi ceux là, il y a ceux qui se basent seulement sur le type de l'opérande de gauche pour savoir s'il faut réaliser une addition ou une concaténation.
    Ceux-là doivent normalement lever une erreur : Ruby, Python…

    Ceux qui sont regardent le type des deux opérandes pour savoir s'il faut réaliser une addition ou une concaténation et qui sont capable de faire du trans-typage doivent renvoyer la chaîne "13a" : Java, Javascript, Scala…

    Et puis, il y a le C, qui utilise le "+" comme opérateur d'addition de pointeur avec un nombre, et il sort une réponse presque logique : "a".

    Donc en gros, si j'ai tout bien compris, tu montres qu'il faut connaître la syntaxe et la grammaire d'un langage pour l'utiliser,et qu'il y a des mauvais élèves pour expliquer ou montrer une erreur, c'est bien cela ?

    Si tu voulais comparer les messages d'erreur, n'aurait-il pas mieux utiliser un opérateur possédant moins de signification ? Par exemple "/" ou "*" ?

    • [^] # Re: Le but ?

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

      Eh bien, faut pas chercher un truc très profond. Ce qui est intéressant, c'est qu'observer la réaction d'un langage sur cette simple opération en dit pas mal sur ses particularités : typage statique ou non, typage fort ou faible (tous vs C/C++), style objet, système de type à la fonctionnelle ou typage plus simple (par exemple Ruby vs Haskell vs Go), surcharche d'opérateur ou non, opérateur qui coercitionne les arguments en fonction de la valeur de retour ou choisit la valeur de retour en fonction de ceux-ci (Perl vs Javascript), qui essaye de deviner ou non ("3a" vs "3") (awk vs lua), stacktrace par défaut ou non (sbcl vs guile), messages d'erreurs pensés pour débutants ou connaisseurs, etc.

    • [^] # Re: Le but ?

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

      Et puis, il y a le C, qui utilise le "+" comme opérateur d'addition de pointeur avec un nombre, et il sort une réponse presque logique : "a".

      Euh, non, dans l’exemple donné le programme C sort une valeur qui, sauf erreur de ma part, devrait être l’adresse de la chaîne "3a" incrémentée de 1.

      Ce qui correspond à ce qui est attendu d’après le standard C (section 6.5.6, §4 : When an expression that has integer type is added to or subtracted from a pointer, the result has the type of the pointer operand).

      D’ailleurs, l’avertissement du compilateur (aussi bien gcc que clang) ne concerne pas le fait d’additionner un entier et un pointeur (il n’y a pas lieu d’avertir de quoi que ce soit puisque c’est quelque chose de prévu par le standard), mais la discordance entre le type attendu d’après la chaîne de format ("%d", soit un entier) et le type de l’expression correspondante (pointeur, conformément à la règle sus-citée). Un appel de la forme printf("%p", 1 + "3a") ne provoque aucun avertissement.

      • [^] # Re: Le but ?

        Posté par . Évalué à 4. Dernière modification le 03/04/17 à 15:42.

        Et puis, il y a le C, qui utilise le "+" comme opérateur d'addition de pointeur avec un nombre, et il sort une réponse presque logique : "a".

        Euh, non, dans l’exemple donné le programme C sort une valeur qui, sauf erreur de ma part, devrait être l’adresse de la chaîne "3a" incrémentée de 1.

        Ben si, c’est exactement ce qu’il dit :

        Et puis, il y a le C, qui utilise le "+" comme opérateur d'addition de pointeur ("3a") avec un nombre (1), et il sort une réponse presque logique : "a" (quand on met printf(1+"3a") et non la version avec "%d").

        Je ne l’avais pas vu ambigüe.

    • [^] # Re: Le but ?

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

      Le but ? Pour moi c'est très intéressant: la prochaine fois que je dois choisir un langage, ça donne des informations très importantes.
      Personnellement, le langage qui ne râle pas et qui "essaye de deviner" pour toi, je l'élimine de suite. Mais ça, c'est mes critères… Enfin, j'ai mes raisons:

      Imaginons:
      FONCTION (a)
      SI a > 100 ALORS
      DECLENCHER GUERRE NUCLEAIRE…
      SINON
      CA VA…
      FIN SI
      FIN

      Et maintenant, imaginons que 'a' vienne de l'addition de cet article: j'aimerais bien être sûr que le résultat de l'addition soit prévisible et pas interprétée avec une foultitude de règles genre javascript: si le deuxième argument peut-être casté vers une string, je caste le premier sauf si il ressemble à un int (c'est quoi, "ressembler à un int" ?). Enfin bref, revoir le lien déjà posté:
      https://www.destroyallsoftware.com/talks/wat

    • [^] # Re: Le but ?

      Posté par . Évalué à 3.

      J'ai lu quelque part que l'on peut juger du niveau d'un logiciel en fonction des erreurs qu'il est capable de gérer …

      il est bien loin le temps des "syntax error" génériques qui pouvait rendre fou :)

      pour ma part je préfère de loin des langages qui "t'aide à la correction" comme python plutôt que des langages laxistes comme php.

      D'un autre coté si vous aimez la rigueur pure et dure … il y a ada … mais pour avoir testé c'est un peu too much …

      • [^] # Re: Le but ?

        Posté par . Évalué à 4.

        D'ailleurs en Ada cela ne compile pas …

        with Ada.Text_IO; 
        procedure bad_add is
        begin
          Ada.Text_IO.Put_Line( 1 + "3a" );
        end bad_add;
        `̀̀``
        
        `gcc-4.6 -c bad_add.adb
        bad_add.adb:5:29: expected type universal integer
        bad_add.adb:5:29: found a string type
        gnatmake: "bad_add.adb" compilation error
        `
        • [^] # Re: Le but ?

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

          En même temps, tu fais pas d'effort ;)

          with Ada.Text_IO; 
          procedure bad_add is
          
             function "+" (Left : Integer;
                           Right : String ) return String is
               (Integer'Image (Left) & Right);       
          
          begin
             Ada.Text_IO.Put_Line( 1 + "3a" );
          end bad_add;

          Il faut juste lui dire comment faire :)
          Par contre, c'est pas commutatif, il faudrait aussi écrire ce que l'on fait dans l'autre sens.

          • [^] # Re: Le but ?

            Posté par . Évalué à 4.

            OK pris en flag …
            cela m'apprendra à la ramener sur un langage que j'ai survolé (mais que j'ai beaucoup apprécié et déplore le manque de temps pour réellement le pratiquer)

            il faut juste lui dire comment faire :)

            Ada en demande beaucoup, mais en retour si la compilation passe c'est que déjà beaucoup de choses ont correcte

            Est ce vrai ou je me trompe lourdement ?

            • [^] # Re: Le but ?

              Posté par (page perso) . Évalué à 2. Dernière modification le 04/04/17 à 12:10.

              Est ce vrai ou je me trompe lourdement ?

              Globalement, c'est vrai mais ça ne corrige pas les erreurs d'algo qui déclenchent des exceptions ;)

              Ceci dit, il y a aussi des cas où ça peut se planter alors que ça compilait très bien.
              Typiquement, je pense à l'utilisation des bindings.
              La majeure partie du temps, ce sont des bibliothèques C/C++ qui sont liées et du coup, elles introduisent l'utilisation de pointeurs venant directement du code C et là, c'est le drame ;)

              Ada n'est pas la panacée mais c'est vrai qu'une fois que j'ai corrigé toutes les erreurs de compilation, je suis déjà plus sûr de mon coup :D

              Enfin, cela ne représente que mon avis…

              • [^] # Re: Le but ?

                Posté par . Évalué à 2.

                Ada n'est pas la panacée mais c'est vrai qu'une fois que j'ai corrigé toutes les erreurs de compilation, je suis déjà plus sûr de mon coup :D

                Non bien sur, mais j'avais été surpris par les 'missing case values:' et avais trouvé cela très bien, cette obligation de mettre un cas par défaut quand on utilise un case par exemple.

                En revanche la courbe d'apprentissage est plutôt rude … dommage

                • [^] # Re: Le but ?

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

                  En revanche la courbe d'apprentissage est plutôt rude … dommage

                  Il faut y aller petit à petit et ne pas chercher à manipuler le langage dans son ensemble d'un seul coup.
                  Personnellement, il y a encore un tétra chiée de trucs dans la norme que je n'ai pas encore utilisés ;)

          • [^] # Re: Le but ?

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

            Par contre, c'est pas commutatif, il faudrait aussi écrire ce que l'on fait dans l'autre sens.

            Mouais ça n'a pas grand chose à voir avec (pour le meilleur et pour le pire) les langages où on peut ajouter des choux et des carottes. C'est sûr que si tu écris une fonction permettant explicitement et seulement d'additionner des Int et des String, le programme va pouvoir additionner des Int et des String, encore heureux.

            • [^] # Re: Le but ?

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

              Ce que je voulais dire c'est que comme rien n'est implicite, il faut écrire la fonction pour l'autre addition et ainsi assurer la commutativité de l'addition, ce qui n'a rien d'obligatoire…

              La version exhaustive serait, du coup, celle-là

              with Ada.Text_IO;
              with Ada.Strings; use Ada.Strings;
              with Ada.Strings.Fixed; use Ada.Strings.Fixed;
              
              procedure bad_add is
              
                 function "+" (Left : Integer;
                               Right : String ) return String is
                   (Integer'Image (Left) & Right);       
              
                 function "+" (Left : String;
                               Right : Integer) return String is
                   (Left & Trim (Integer'Image(Right), Both));
              
              begin
                 Ada.Text_IO.Put_Line( 1 + "3a" );
                 Ada.Text_IO.Put_Line("3a" + 1);
              end bad_add;

              C'est sûr que si tu écris une fonction permettant explicitement et seulement d'additionner des Int et des String, le programme va pouvoir additionner des Int et des String, encore heureux.

              Certes mais je préfère dire ce que je veux faire que d'avoir un comportement inféré qui n'est pas forcément vrai… Bien qu'à partir du moment où on écrit '+', il semble, par convention, obligatoire que l'opération soit commutative.

      • [^] # Re: Le but ?

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

        pour ma part je préfère de loin des langages qui "t'aide à la correction" comme python plutôt que des langages laxistes comme php.

        Je n'ai encore jamais essayé mais il semblerait que PHP 7.0 implémente un typage statique (optionnel). Quelqu'un a une expérience avec ça?

  • # Et en brainfuck ?

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

    Je suis déçu, le brainfuck ne permet pas de faire ce genre d'opération complexe. J'aurai été curieux de voir le message d'erreur…

    • [^] # Re: Et en brainfuck ?

      Posté par . Évalué à 0.

      Il aurait peut-être répondre "4a" ! Comme aucun autre langage apparemment, pas si compliqué pourtant.

  • # Le langage ou le compilateur/interpréteur ?

    Posté par . Évalué à 1.

    Le comportement (i.e. choix effectué face à la situation) et l'affichage du comportement (i.e. le message d'erreur ou le warning généré) sont-ils intrinsèques au langage (i.e. décrit dans sa définition) ou bien juste au compilateur/interpréteur ?

    Y a t-il des langages ayant plusieurs compilateurs/interpréteurs ? Si oui, il faudrait distinguer les cas où le comportement obtenu est décrit par le langage ou bien résulte du "complément de définition" apporté par le compilateur…

    Un langage dont le comportement* est défini par sa définition ET son compilateur est-il un bon langage ?

    * J'admets que l'affichage du comportement puisse relever du seul compilateur.

    • [^] # Re: Le langage ou le compilateur/interpréteur ?

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

      En l'occurrence, je pense qu'ici, dans tous les cas, le comportement est bien défini par tous les langages du fil (par exemple si awk renvoit 4 c'est parce qu'il utilise la fonction de la libc standard strtod pour produire un nombre à partir d'une chaîne, c'est documenté dans le manuel). Le message d'erreur et son style par contre peuvent dépendre dans une plus ou moins grande mesure du compilateur (par exemple gcc vs clang pour le C/C++).

Suivre le flux des commentaires

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