Journal Haskell et le tri

Posté par  (site web personnel) . Licence CC By‑SA.
Étiquettes :
30
18
fév.
2016

Sommaire

Bonjour nal.

Introduction

Cela fait longtemps que je n'ai pas posté ici, cela me manquait. Je trouve que LinuxFR ne parle pas assez de Haskell, alors je vais en parler un peu aujourd'hui sur une digression totalement inintéressante et proche de la masturbation intellectuelle. On va partir de rien et discuter de la fonction de tri fournie dans la librairie standard (base) de GHC, le compilateur le plus connu / utilisé.

L'idée c'est de vous présenter un peu Haskell et sa syntaxe à partir d'un exemple simple à partir d'une fonction simple, le tri. Attention, on ne va pas implémenter de tri, mais on va voir comment utiliser les fonctions déjà existante pour construire une fonction de tri de plus haut niveau.

Sort

Cette fonction s'appelle sort et a comme signature :

sort :: Ord a => [a] -> [a]

Dit autrement, c'est une fonction qui prend [a], une liste de n'importe quel type a et renvoie une liste de n'importe quel type a. Seule contrainte sur a, ce type doit être Ordonable, c'est à dire que on doit pouvoir comparer les éléments de a entre eux. La liste est une liste simplement chaînées et elle est non modifiable, ainsi le tri ne se fait pas en place, mais crée une nouvelle liste.

Cela ressemblerait un peu à cette fonction en c++ :

template<typename A>
std::list<A> sort(const std::list<A> &l);

Exemple d'usage dans l'invite de commande ghci :

> import Data.List
> sort [1, 10, 3, 2, 0, -1]
[-1,0,1,2,3,10]

Sort By

Super ! Bon, il existe un autre variantes de cette fonction, sortBy.

sortBy :: (a -> a -> Ordering) -> [a] -> [a]

sortBy accepte une fonction de comparaison en plus de type (a -> a -> Ordering), en gros, elle prend deux éléments et renvoi leur ordre. Exemple d'utilisation :

> let compareName nameA nameB
|       | nameA == "Chuck Norris" = GT
|       | nameB == "Chuck Norris" = LT
|       | otherwise = compare nameA nameB
| 

> sortBy compareName ["Chuck Norris", "Bruce Wayne", "Bruce Banner", "Zorro", "Denver"]
["Bruce Banner","Bruce Wayne","Denver","Zorro","Chuck Norris"]

Notez comment j'écris une fonction de comparaison spécifique qui rend toute sa gloire à Chuck Norris.

Cette example de code est équivalent au code python suivant, qui ne marche plus qu'en python 2, l'attribut cmp de sorted ayant disparu en python 3, ce que je regrette. :

>>> sorted([1, 10, 3, 2, 0, -1])
[-1, 0, 1, 2, 3, 10]

>>> def compareName(nameA, nameB):
...     if nameA == "Chuck Norris": return 1
...     if nameB == "Chuck Norris": return -1
...     return cmp(nameA, nameB)
... 
>>> sorted(["Chuck Norris", "Bruce Wayne", "Bruce Banner", "Zorro", "Denver"], cmp=compareName)
['Bruce Banner', 'Bruce Wayne', 'Denver', 'Zorro', 'Chuck Norris']

Digression paresseuse

Petite digression sur Haskell, connaissez vous la complexité de cette opération :

> head (sort [1..100])
1

Naïvement on pourrait se dire, un tri, O(n log n). Mais non, Haskell est paresseux et rend le résultat en O(n) car il n'a pas besoin de calculer le tri entier avant d'obtenir le résultat. Super non ? En pratique cela aide rarement et souvent cela complexifie la réflexion.

Tri inversé

Revenons à nos fonctions de tri. Je veux pouvoir trier ma liste à l'envers. En python, rien de plus simple, il suffit d'utiliser le paramètre optionnel reverse :

>>> sorted(["Chuck Norris", "Bruce Wayne", "Bruce Banner", "Zorro", "Denver"], cmp=compareName, reverse=True)
['Chuck Norris', 'Zorro', 'Denver', 'Bruce Wayne', 'Bruce Banner']

En Haskell, ce n'est pas si simple… On peut utiliser reverse, mais cela va trier la liste dans un sens puis l'inverser, quelle perte de temps :

reverse (sortBy compareName ["Chuck Norris", "Bruce Wayne", "Bruce Banner", "Zorro", "Denver"])
["Chuck Norris","Zorro","Denver","Bruce Wayne","Bruce Banner"]

Mais on peut aussi se dire que si on inversait les arguments de la fonction compareName avec flip alors cela fonctionnerait sans soucis :

> (sortBy (flip compareName) ["Chuck Norris", "Bruce Wayne", "Bruce Banner", "Zorro", "Denver"])
["Chuck Norris","Zorro","Denver","Bruce Wayne","Bruce Banner"]

On peut donc facilement crée une fonction de plus haut niveau :

> let reverseSortBy f = sortBy (flip f)
| 
> reverseSortBy compareName ["Chuck Norris", "Bruce Wayne", "Bruce Banner", "Zorro", "Denver"]
["Chuck Norris","Zorro","Denver","Bruce Wayne","Bruce Banner"]

Règle de compilation

On peut même crée une règle de compilation qui va remplacer automatiquement reverse (sortBy f l) par sortBy (flip f) l :

   {-# RULES
"reverse/sortBy"    forall f l.  reverse (sortBy f l) = sortBy (flip f) l
#-}

C'est une chose que j’apprécie vraiment en Haskell, c'est que on peut écrire du code assez clair et plus tard le rendre efficace en transformant notre code automatiquement.

Tri multiple

Pour continuer, nous allons nous concentrer sur sortBy en Haskell ou l'attribut cmp de la fonction sorted de python.

Je suis maintenant l'heureux utilisateur d'une liste de noms et de poids que je nommerais l :

> let l = [("Bruce Lee", 50), ("Batman", 100), ("Hulk", 200), ("La montagne", 100)]

J'aimerais trier ces gens par poids décroissant et par nom croissant en cas d'égalité. Nous allons faire cela en python, puis en Haskell, puis en Haskell for WarLdOrZOrgCoderZZZ.

En python tout d'abord :

>>> l = [("Bruce Lee", 50), ("Batman", 100), ("Hulk", 200), ("La montagne", 100)]

>>> def compare(t0, t1):
...     c = cmp(t1[1], t0[1])
...     if c == 0:
...         return cmp(t0[0], t1[0])
...     else:
...         return c
... 
>>> sorted(l, cmp=compare)
[('Hulk', 200), ('Batman', 100), ('La montagne', 100), ('Bruce Lee', 50)]

Rien de bien dramatique. C'est pas forcement beau, mais cela fait le boulot. Ma fonction de comparaison commence par comparer les indices [1] (donc les poids), observez l'inversion des arguments de cmp où je passe t1 puis t0 pour l'ordre décroissant. Si les deux sont égaux (car cmp renvoie 0) alors je regarde le nom, sinon je renvois le résultat de la comparaison.

En Haskell maintenant :

> let compare' t0 t1 = case (compare (snd t1) (snd t0)) of
|                      EQ -> compare (fst t0) (fst t1)
|                      ltOrGt -> ltOrGt
| 
> sortBy compare' l
[("Hulk",200),("Batman",100),("La montagne",100),("Bruce Lee",50)]

J'ai appelé ma fonction de comparaison compare' pour ne pas la confondre avec celle du système qui s'appelle compare. snd et fst sont sensiblement équivalents aux [0] et [1] de python. Ce que j'aime en Haskell c'est que bien que je n'ai mis aucune annotation de type, ils existent et sont vérifiés. J'aime aussi le fait que compare me renvoi EQ ou GT ou LT et non pas -0 ou -1 ou 1 comme le fait python. Bref, les avantages du typage statique sans les inconvénients.

Un des défaut de cette méthode, c'est qu'elle va vite devenir complexe si j'ai plus de critères de tri. Nous allons régler ce problème.

Première factorisation

En premier lieu je n'aime pas la duplication de code qu'il y a sur compare (snd t1) (snd t0) et compare (fst t0) (fst t1). Je propose de factoriser cela dans une fonction que nous nommerons de façon malicieuse comparing, je ne me suis pas foulé, elle existe déjà avec ce nom la ;)

> let comparing f a b = compare (f a) (f b)
| 
> let compare' t0 t1 = case (comparing snd t1 t0) of
|                      EQ -> comparing fst t0 t1
|                      ltOrGt -> ltOrGt
| 
> sortBy compare' l
[("Hulk",200),("Batman",100),("La montagne",100),("Bruce Lee",50)]

C'est plus simple ;) Comme comparing existe déjà dans la librairie standard, je vais me contenter de l'importer avec import Data.Ord (comparing).

On va continuer à factoriser un peu. Je n'aime vraiment pas cette inversion des arguments de comparing snd t1 t0. Cela ressemble à un bug, il manque un commentaire pour dire que je veux trier en ordre descendant. Bref, c'est peu agréables. Je vais écrire deux variantes de comparing :

ascending f a b = comparing f a b
descending f a b = flip (comparing f) a b

Application partielle

Petite digression sur l'application partielle. En Haskell, tout est fonction. Il y a les fonctions constantes de type t et les fonctions unaires de type de type t -> t'. Par exemple, abs qui donne la valeur absolue, est une fonction unaire de Int -> Int. Mais le type de retour d'une fonction unaire peut être aussi une fonction. On peut donc avoir une fonction du type a -> (b -> r) qui peut s'écrire a -> b -> c. En appliquant cette fonction une unique fois sur une valeur de type a on obtient une fonction de type b -> c. Bref. On se sert souvent de cette propriété pour créer des fonctions avancée à partir de fonction plus simples. Exemple, la fonction d'addition s'écrit (+), notez dans l'exemple suivant comment on peut l'utiliser en forme infix ou postfix, et comment on peut créer une fonction partiellement appliquée inc :

> (+) 1 2
3
> 1 + 2
3
> let inc = (+) 1
> inc 5
6

De façon similaire, toutes ces notations sont équivalentes, seul changent le fait que les arguments sont explicitement présents ou pas :

let add x y = (+) x y
let add x y = x + y
let add x = (+) x
let add = (+)

Seconde factorisation

Revenons à nos fonctions ascending et descending. Voici le code prenant en compte ces nouvelles fonctions. Notez que j'ai un peu simplifié l'écriture de ascending et descending afin d'utiliser l'application partielle :

> let ascending = comparing

> let descending = flip . comparing

> let compare' t0 t1 = case (descending snd t0 t1) of
|                         EQ -> ascending fst t0 t1
|                         ltOrGt -> ltOrGt
|
> sortBy compare' l
[("Hulk",200),("Batman",100),("La montagne",100),("Bruce Lee",50)]

Bon, les choses commencent à devenir intéressantes. Ma fonction compare' commence à être bien claire, si ce n'est le case pour enchaîner sur le critère suivant si le premier critère est égale à EQ. On pourrait définir un opérateur binaire permettant de réaliser ce traitement, appelons cet opérateur blork juste parce que c'est marrant de lui donner ce nom et qu'aucun autre ne me vient à l'esprit là maintenant…

> let blork a b
|        | a == EQ = b
|        | otherwise = a

Notez la syntaxe de guard, qui permet de tester différentes équations afin de trouver la valeur de retour.

Réécrivons encore une fois notre code, notez que j'utilise blork comme opérateur infix.

> let compare' t0 t1 = descending snd t0 t1 `blork` ascending fst t0 t1

> sortBy compare' l
[("Hulk",200),("Batman",100),("La montagne",100),("Bruce Lee",50)]

On factorise encore plus

Cela va commencer à devenir plus simple de chaîner les prédicats puisqu'au lieu d'une cascade de case je peux simplement ajouter une composition avec blork. On pourrait même commencer à virer compare' et à mettre en place une fonction lambda anonyme :

> sortBy (\t0 t1 -> descending snd t0 t1 `blork` ascending fst t0 t1) l
[("Hulk",200),("Batman",100),("La montagne",100),("Bruce Lee",50)]

Bon. Mais je ne suis encore pas satisfait par ça. Je n'aime pas cette fonction, la presence de t0 et t1 de partout. Ce que j'aimerais c'est écrire une fonction blirk qui permet de combiner deux fonctions de comparaison ensemble. On va l'écrire vite :

> let blirk f0 f1 a b = f0 a b `blork` f1 a b

> sortBy (descending snd `blirk` ascending fst) l
[("Hulk",200),("Batman",100),("La montagne",100),("Bruce Lee",5)]

Et voila, là je suis super content… J'ai une belle api, cela permet d'expliquer vraiment simplement ce que je veux. Mais il y a toujours ces deux fonctions blirk et blork pour lesquels j'ai du mal à donner un nom.

Monoids

Je rappel, blork retourne le premier terme qui n'est pas EQ, tel que :

EQ `blork` GT `blork` LT == GT
LT `blork` EQ `blork` GT = LT

Cela ne vous rappel rien ? :

True and True and True == True
True and False and True == False

C'est très proche de l’opérateur and ou de l’opérateur or logique. En généralisant un peu, il s'agit d'un [Monoid]. Non, ce n'est pas une pub pour du shampoing, un monoid c'est un ensemble qui admet un élément neutre et une operation de réduction associative. Comme (+) et 0 ou (*) et 1 pour les entiers.

Nos résultats d’égalité forment un monoid. L’élément neutre étant EQ et l'operation entre les éléments étant blork.

On et bien avancé, oui ! Car ce concept est tellement courant qu'il y a une librairie standard dans Haskell qui gère les Monoids, Data.Monoid. Et coup de chance, Ordering, le type de EQ, LT et GT est géré.

En haskell, les Monoids ont deux fonctions utiles, mappend et mempty. Regardons un peu celles-ci en action sur différents monoids.

Tout d'abord les elements neutres, avec mempty. Ici, c'est un peu tordu, car mempty est une fonction constante polymorphique, c'est à dire que sa valeur dépend du type utilisé, d'où les annotation de type faites avec :: que j'ai rajoutées.

> mempty :: Sum Int
Sum {getSum = 0}
> mempty :: Product Int
Product {getProduct = 1}
> mempty :: Ordering
EQ
> mempty :: String
""
> mempty :: [Int]
[]

Puis la fonction de réduction, mappend :

> Sum 5 `mappend` Sum 12
Sum {getSum = 17}
> Product 5 `mappend` Product 12
Product {getProduct = 60}
> [1,2,3] `mappend` [4,5,6]
[1,2,3,4,5,6]
> "Hello" `mappend` "you"
"Helloyou"
> EQ `mappend` GT
GT
> EQ `mappend` LT
LT
> GT `mappend` LT
GT
> GT `mappend` LT

Application au critères de tri

Donc en fait, ici, mappend c'est la fonction blork. Et comme mappend est super utilisée, il a un synonyme, l'operateur (<>). Nous pouvons donc dégager blork et utiliser (<>) à la place.

Qu'en est il de blirk ?. Rappel, cette fonction permettait de combiner ensemble plusieurs fonctions binaire retournant des monoids afin de crée une unique fonction binaire.

Il se trouve que les fonctions sont aussi des monoids, voyons ce que cela implique.

Je reprend donc ma définition de blirk:

> let blirk f0 f1 a b = f0 a b `blork` f1 a b

Je remplace blork par <> comme vu dernièrement :

> let blirk f0 f1 a b = f0 a b <> f1 a b

Au lieu d'appliquer l’opérateur <> entre les résultats de f0 et f1 appliqués à a et b, je peux appliquer l’opérateur <> entre f0 et f1 :

> let blirk f0 f1 a b = (f0 <> f1) a b

Bon… Super… Je vais nettoyer un peu en virant a et b grâce à l'application partielle :

> let blirk f0 f1 = f0 <> f1

C'est pas mal, blirk est bien plus simple maintenant, mais est toujours là… Juste pour la forme, je pourrais écrire <> sous forme prefix :

> let blirk f0 f1 = (<>) f0 f1

Tient, mais je peux utiliser l'application partielle ici ! :

> let blirk = (<>)

Et voila blirk n'est autre que l’opérateur de réduction des monoids… Je n'ai donc plus besoin de mes fonctions blirk et blork. Voici maintenant une session de shell complète me permettant d'arriver à mon résultat :

> import Data.List (sortBy)
> import Data.Ord (comparing)
> import Data.Monoid ((<>))

> let l = [("Bruce Lee", 50), ("Batman", 100), ("Hulk", 200), ("La montagne", 100)]

> let ascending = comparing
> let descending = flip . comparing

> sortBy (descending snd <> ascending fst) l
[("Hulk",200),("Batman",100),("La montagne",100),("Bruce Lee",50)]

Voila. Grâce à trois fonctions de la librairie standard et deux définitions triviales pour ascending et descending, j'ai réalisé un mini langage me permettant de paramétrer ma fonction sortBy pour réaliser n'importe quel type de tri.

Définir ses propres types

Notez que dans la vraie vie, je n'aime pas les tuples anonymes, donc je ferais mon propre type super hero que j'exploiterais de cette manière :

> data Hero = Hero {name :: String, weight :: Int} deriving (Show)
> let l = [Hero "Bruce Lee" 50, Hero "Batman" 100, Hero "Hulk" 200, Hero "La montagne" 100]
> sortBy (descending weight <> ascending name) l
[Hero {name = "Hulk", weight = 200},Hero {name = "Batman", weight = 100},Hero {name = "La montagne", weight = 100},Hero {name = "Bruce Lee", weight = 50}]

Ceci afin de vous montrer la souplesse d'Haskell à ce niveau là. Je crois que sortBy (descending weight <> ascending name) ne peut pas être plus clair et de façon intéressante, cela n'a demandé aucun boilerplate. L'annotation deriving (Show) permet de munir mon type "Hero" d'un affichage par défaut que nous voyons dans le résultat. C'est l'équivalent de la surcharge de __str__ en python ou de ostream &operator<<(ostream&) en C++, mais avec un comportement par défaut pas trop mal.

Sort On

Je disais que je regrettais la disparition du paramètre cmp de la fonction sorted de python. Il est maintenant remplacé par le paramètre key qui s'utilise un peu comme notre fonction comparing. En python :

 >>> sorted(["Hello", "My", "Friend"], key=len)
 ['My', 'Hello', 'Friend']
 >>> sorted(["Hello", "My", "Friend"], key=len, reverse=True)
 ['Friend', 'Hello', 'My']

La difference étant que cette approche n'appelle la fonction key qu'une fois par élément de la liste contrairement à la version avec cmp qui appelle cette fonction en moyenne O(n log n) fois. Mais en pratique elle nécessite de stocker une liste temporaire en plus. C'est donc un compromis entre mémoire utilisée et efficacité de la fonction de comparaison.

La fonction sortOn en Haskell fonctionne de façon similaire :

> sortOn length ["Hello", "My", "Friend"]
["My","Hello","Friend"]

Mais qu'en est il du tri inversé. Encore une fois il n'y a pas de fonction de tri avec reverse. L'idée étant que la fonction attendue par sortOn, ici length transforme nos éléments en une autre valeur qui est triable. Ici, Haskell propose un type, Down qui inverse les propriétés de tri du type interne. Exemple :

> sortOn (Down . length) ["Hello", "My", "Friend"]
["Friend","Hello","My"]

La différence en terme d'API avec python est faible, mais revenons à un example plus complexe comme celui du tri de notre liste de héros. Nous allons réaliser une fonction de tri qui transforme un hero en tuple (poids du hero descendant, nom du héro), les tuples étant triés dans l'ordre lexicographique, cela résous notre problème :

> sortOn (\hero -> (Down (weight hero), name hero)) l
[Hero {name = "Hulk", weight = 200},Hero {name = "Batman", weight = 100},Hero {name = "La montagne", weight = 100},Hero {name = "Bruce Lee", weight = 50}]

A ma connaissance il n'y a pas d'équivalent direct de Down en python, alors nous allons l'écrire. J'en profite pour aussi crée le type Hero pour pouvoir facilement comparer :

>>> class Hero:
...     def __init__(self, name, weight):
...        self.name = name
...        self.weight = weight
...     
...     def __repr__(self):
...         return "Hero(%r, %r)" % (self.name, self.weight)
... 
>>> l = [Hero("Bruce Lee", 50), Hero("Batman", 100), Hero("Hulk", 200), Hero("La montagne", 100)]
>>> class Down:
...     def __init__(self, obj):
...          self._obj = obj
...     def __lt__(self, other):
...          return self._obj.__gt__(other._obj)
...     def __gt__(self, other):
...          return self._obj.__lt__(other._obj)
...     def __eq__(self, other):
...          return self._obj.__eq__(other._obj)
... 
>>> sorted(l, key=lambda hero: (Down(hero.weight), hero.name))
   [Hero('Hulk', 200), Hero('Batman', 100), Hero('La montagne', 100), Hero('Bruce Lee', 50)]

Voila, on peut faire la même chose, mais la lib standard Haskell propose ici des solutions déjà implémentées.

Conclusion

Voila, j'espère que cette introduction à Haskell vous aura fait plaisir, j'ai essayé d'aborder un problème simple, le tri, et en tirant un peu dessus, montrer quelques subtilités du langage.

A bientôt.

  • # merci

    Posté par  (site web personnel) . Évalué à 7.

    Merci, c'est très sympa comme journal.

    Moi qui n'ai pas (encore) appris Haskell (mais qui joue en clojure) ça permet de se plonger dedans petit à petit à partir d'un exemple "simple" et c'est plutôt cool.
    Bon par contre je vais le re-lire à tête reposée par ce que je ne suis pas sur d'avoir tout capté :-)

    • [^] # Re: merci

      Posté par  (site web personnel) . Évalué à 2.

      Bon par contre je vais le re-lire à tête reposée par ce que je ne suis pas sur d'avoir tout capté :-)

      En même temps c'est dense ;) N'hésites pas à discuter les points pas clairs. Je suis parti de ma révélation de hier soir de "Mon dieu, les fonctions sont aussi des Monoids" et en fait je suis arrivé à ce truc immonde ;)

      • [^] # Re: merci

        Posté par  (site web personnel) . Évalué à 1.

        "Mon dieu, les fonctions sont aussi des Monoids"

        Et les monades sont aussi des monoïdes. Mouahahaha !

        • [^] # Re: merci

          Posté par  (site web personnel) . Évalué à 3.

          Et les fonctions sont aussi des Monads… Et là j'ai souvent le cerveau qui fond. Je crois que je me suis servi une fois de l'instance d'Applicative pour les fonctions, pour un code golf, mais plus jamais ;) Alors l'instance de Monad ;)

          • [^] # Re: merci

            Posté par  (site web personnel) . Évalué à 1.

            Et les fonctions sont aussi des Monads…

            Ça ne veut pas dire grand chose à priori que les fonctions sont des monades*, tu veux probablement parler du foncteur (A → _) pour un A fixé. Mais dans ce cas, c'est juste la bonne vieille monade reader, pas de quoi casser trois pattes à un lambda !

            [*] La théorie des catégories est le genre de contexte dans lequel n'importe quel énoncé peut avoir du sens, donc je fais gaffe quand même.

            • [^] # Re: merci

              Posté par  (site web personnel) . Évalué à 2.

              (->) r est une instance de Monad, donc "Les fonctions sont des monades" ;) Mais je t'avoue que au niveau théorie des catégories, je suis largué et comme tu le dis, on peut vraiment dire n'importe quoi et que cela ai du sens.

              Mais en effet, tu as raison, c'est simplement Reader en mode anonyme, merci de m'avoir fait observer cela.

      • [^] # Re: merci

        Posté par  . Évalué à 5.

        Je suis parti de ma révélation de hier soir de "Mon dieu, les fonctions sont aussi des Monoids" et en fait je suis arrivé à ce truc immonde ;)

        Hum, donc, même toi tu admets que c'est immonde? Puis-je étendre à "illisible", "super chiant à maintenir"? Je suppose que non.

        Ok, je trolle. Enfin, je révèle surtout mon incompétence, au fond, mais… je verrais bien un sondage pour savoir quelle famille de langages de programmation est la plus lisible! Sauf qu'un sondage serait trop superficiel: un mathématicien aimera sûrement plus des trucs genre lisp, qui lui rappellent ses formules idéales, tandis qu'un physicien saura peut-être mieux gérer du C, qui laisse la place à l'incertitude: 0,5V, c'est 0 ou c'est 1?

        • [^] # Re: merci

          Posté par  (site web personnel) . Évalué à 3.

          Ce qui est imonde c'est cet article, mais par contre je trouve que ce code au contraire est "lisible" et "simple à maintenir" (Et je rentre dans ton troll ;)

          Pour trier une liste d'étudiant par note descendentes et par nom ascendant :

          sortBy (descending note <> ascending nom) listeEtudiants

          Je trouve cela très lisible et simple à maintenir. Après c'est le problème de tous les DSL, c'est qu'il y a une abstraction (ici <> et ce qui se cache sous ascending et descending), mais celle-ci n'est pas nécessaire à comprendre pour comprendre ce que fait réellement le code ni le modifier. Au pire on peut renommer <> en andThenSortBy pour rendre cela encore plus lisible.

          Je ne suis pas certain que je pourrais écrire plus clair en haskell et je sais que écrire aussi clair en python ou c++ me demandera d'ajouter des fonctions non triviales qui ne sont pas dans la librairie standard.

          • [^] # Re: merci

            Posté par  . Évalué à 4.

            par contre je trouve […]

            Chacun son truc :)

            ou c++ me demandera d'ajouter des fonctions non triviales qui ne sont pas dans la librairie standard.

            Je confirme qu'en C++ cette ligne-ci sera moins claire:

            std::sort( listeEtudiants.begin(), listeEtudiants.end(), [](TypeEtudiant const& e1, TypeEtudiant const& e2){return e1.note == e2.note ? e1.nom < e2.nom : e1.note < e2.note; } );

            Ceci en considérant que l'on à pas surchargé un opérateur spécifique à TypeEtudiant, bien sûr (ça ne serait pas très pertinent à mon avis).

            Mais je trouve ça trivial, par contre. L'écriture du lambda… on peut considérer ça comme une fonction je suppose, je suis mitigé à ce sujet.
            Je pense que si on pouvais passer un conteneur d'un coup plutôt que de spécifier les bornes, ça allègerait déjà pas mal de code (perso, j'utilise les conteneurs entiers la plupart du temps…).
            Ensuite, oui, il n'existe pas de fonction de comparaison d'éléments multiples, ce qui dans le cas cité est intéressant, il faut bien l'admettre.

            • [^] # Re: merci

              Posté par  (site web personnel) . Évalué à 3.

              Mais je trouve ça trivial, par contre. L'écriture du lambda… on peut considérer ça comme une fonction je suppose, je suis mitigé à ce sujet.

              Je n'ai jamais dis que on ne pouvais pas le faire en C++ attention. Mais quand tu regardes le lambda, avec l’opérateur ternaire, la subtilité sur l'ordre du test pour l'ascendant / descendant (que tu as mal pris en compte, de là à conclure que c'est source d'erreur ;) et la logique à écrire pour chaîner les éléments.

              Tient, j'ai une autre proposition marrante, en C++ :

              std::sort( std::begin(listeEtudiants), std::end(listeEtudiants.end), [](TypeEtudiant const& e1, TypeEtudiant const& e2){return std::make_tuple(e2.note, e1.nom) < std::make_tuple(e1.note, e2.nom)});

              Je pense que si on pouvais passer un conteneur d'un coup plutôt que de spécifier les bornes, ça allègerait déjà pas mal de code (perso, j'utilise les conteneurs entiers la plupart du temps…).

              C'est en discussion pour c++17 et j’espère que cela aboutira.

              • [^] # Re: merci

                Posté par  . Évalué à 2.

                que tu as mal pris en compte, de là à conclure que c'est source d'erreur ;)

                Mais non, c'est juste que c'est la meilleure façon pour que je sois en haut du classement en maths :D

                Tient, j'ai une autre proposition marrante, en C++ :

                Du coup, le tri se fait dans le même sens pour les deux membres, c'est la même erreur que j'ai faite moi-même, non?
                Aussi, je ne suis pas sûr que les tuples implémentent l'opérateur< ? Si c'est le cas, peut-être que je finirai par m'en servir, jusqu'ici à chaque fois que j'ai essayé, ça c'est transformé en classe dans les heures qui suivent (contrôle du comportement plus fin, et code vachement plus lisible, parce que les tuples c'est supra lourd à lire. Un peu comme les lambda, mais sans la raison de rendre utilisable , en fait.)

                C'est en discussion pour c++17 et j’espère que cela aboutira.

                Ça serait vraiment pas mal oui. J'avoue ne pas avoir trop suivi ce qui est prévu pour C++17, il faudrait peut-être que je commence à m'en inquiéter.

                • [^] # Re: merci

                  Posté par  (site web personnel) . Évalué à 2.

                  Du coup, le tri se fait dans le même sens pour les deux membres, c'est la même erreur que j'ai faite moi-même, non?

                  Non, j'ai inversé l'ordre des champs ;)

                  • [^] # Re: merci

                    Posté par  . Évalué à 2.

                    Erf, exact, j'avais pas vu que tu as mixé les champs de e1 et de e2… c'est fourbe ça :)

                    En tout cas je note le coup de la comparaison des tuples, ça pourrait me servir un de ces 4.

              • [^] # Re: merci

                Posté par  . Évalué à 3.

                Avec l'indentation qui va bien pour les autres lecteurs :

                std::sort(
                    std::begin(listeEtudiants),
                    std::end(listeEtudiants.end),
                    [](TypeEtudiant const& e1, TypeEtudiant const& e2) {
                        return std::make_tuple(e2.note, e1.nom) < std::make_tuple(e1.note, e2.nom)
                    }
                );

                Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

            • [^] # Re: merci

              Posté par  . Évalué à 3.

              Je pense que si on pouvais passer un conteneur d'un coup plutôt que de spécifier les bornes, ça allègerait déjà pas mal de code (perso, j'utilise les conteneurs entiers la plupart du temps…).

              Moi je trouve que ça n'irait pas assez loin. Comme toi, je n'ai jamais vraiment apprécié ces paires d'itérateurs. Non seulement c'est source d'erreur pour les débutants, mais c'est aussi limitant par la suite. Le fait de passer des conteneurs (tels que ce que l'on trouve dans la STL) me semble encore bien limité. Il serait génial de pouvoir reproduire ce que l'on a avec RxCpp. Ça permet de traiter autre chose que ce que l'on imagine habituellement dans les conteneurs.

              Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

              • [^] # Re: merci

                Posté par  . Évalué à 2.

                Je ne connais pas RxCpp, mais de ce que j'ai pu lire vite fait dans le README, ça semble reproduire des fonctionnalités du .NET, LINQ à priori.
                Je n'ai jamais utilisé LINQ, mais un collègue m'en a dis beaucoup de bien.
                Il me semble que c'était lié aux bases de données, et wikipedia le confirme:

                LINQ définit un ensemble d’opérateurs de requêtes qui peuvent être utilisés pour effectuer des requêtes, filtrer et projeter des données dans des collections, dans des classes énumérables, dans des structures XML, dans des bases de données relationnelles, et dans des sources de données tierces.

                Enfin, à ceci près que ce n'est pas juste les bases de données, du coup :)
                C'est vrai que ça peut avoir son intérêt, mais je ne suis pas convaincu par le bout de code du README.
                Peut-être à cause du style d'indentation (le K&R, c'est bien sur du C pas trop indenté, mais avec l'enchaînement de lambda de ce code, c'est juste illisible sans éditeur, amhà)… ce qui est dommage.

                • [^] # Re: merci

                  Posté par  . Évalué à 3.

                  Peut-être à cause du style d'indentation (le K&R, c'est bien sur du C pas trop indenté, mais avec l'enchaînement de lambda de ce code, c'est juste illisible sans éditeur, amhà)… ce qui est dommage.

                  Il y a aussi le fait que c'est un bout de code pas triviale et que les lambdas en C++ ont une syntaxe particulièrement pas agréable (le me suis toujours demandé si le commité de standardisation n'avait pas volontairement rendu ça aussi atroce parce qu'ils n'aiment pas les lambdas).

                  Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

        • [^] # Re: merci

          Posté par  (site web personnel) . Évalué à 6. Dernière modification le 19 février 2016 à 10:38.

          je verrais bien un sondage pour savoir quelle famille de langages de programmation est la plus lisible

          C'est surtout qu'une majorité d'informaticiens est formatée aux langages à la C et qu'ils ont ensuite du mal à lire autre chose. Comme les bons utilisateurs Windows qui disent que Linux est trop compliqué :-)

          Franchement, pour avoir enseigné la programmation à des débutants, le C, le Java et le JavaScript sont loin d'être faciles à comprendre.

          Professionnellement, je fais du Java, mais j'apprécie beaucoup l'Ocaml (et le fonctionnel en général). Comme je n'ai pas souvent l'occasion d'en faire, il faut que je me remette un peu les idées en place pour que ça redevienne naturel. Les concepts sont différents et ma façon de programmer aussi, mais c'est juste de l'adaptation.

          En revanche, j'ai essayé un tuto de DropWizard : du Java 8 écrit dans le style fonctionnel. Résultat, je trouve ce mélange incompréhensible.

          • [^] # Re: merci

            Posté par  . Évalué à 2.

            Franchement, pour avoir enseigné la programmation à des débutants, le C, le Java et le JavaScript sont loin d'être faciles à comprendre.

            Forcément. Le Java impose du sucre syntaxique débile dans tous les sens (tout est objet… y compris la fonction d'entrée, que l'on doit du coller en static… ahem…), le C impose de gérer la mémoire à la main, donc une connaissance au moins de base de ce qu'est un pointeur… et JavaScript, je sais pas, je connais pas.
            Personnellement, j'ai débuté en QBasic, c'était vraiment simple. Pas de gestion mémoire, pas d'objet, du typage optionnel (si je ne me trompe pas) et des chaînes de caractère digne de ce nom (contrairement au C, qui à été mon 2nd langage).

            Aussi surprenant que ça puisse paraître, je pense que C++ est plus facile à utiliser que le C ou le Java, si on évite d'enseigner la création de bibliothèques et qu'on se limite à la console: de l'objet optionnel, pas de gestion mémoire manuelle. Choses qui deviennent nécessaires par la suite, mais au début on peut s'en passer.
            Le seul truc gênant, c'est le typage fort, je dirait, mais un physicien ne serait pas surpris: les unités c'est important, 5°C ce n'est pas 5V (et un résultat sans unité était considéré comme faux, de mémoire).
            Ah, et bien sûr, les messages d'erreur liés aux templates aussi, c'est moche, mais ça s'est bien amélioré.
            M'enfin, de toute façon, aucun des langages que l'on à cité n'est fait pour l'enseignement. Pascal/Delphi eux ont été conçus pour il me semble, mais je n'ai pas assez vu de code Pascal pour juger de la lisibilité du langage.

            • [^] # Re: merci

              Posté par  (site web personnel) . Évalué à 4.

              Je ne parle même pas des spécificités des langages, mais du paradigme de la programmation impérative. L'affectation n'est pas du tout naturelle (alors ne parlons pas d'utiliser = pour), et les variables n'ont pas du tout le même sens que celui appris en maths. Cela peut nous permettre évident maintenant, mais ce n'est pas franchement naturel. Le pire étant de pouvoir modifier le contenu des variables passées en paramètres des fonctions (mais pas pour tous les types de variables pour compliquer le truc) qui complique le raisonnement.

              • [^] # Re: merci

                Posté par  . Évalué à 1.

                L'affectation n'est pas du tout naturelle

                Pourquoi?
                Quand j'étais enfant et que je lisais (je lisais beaucoup) "untel est affecté par [évènement|maladie]" je comprenais ce que ça voulait dire. Si on traduit en programmation, en C++ histoire de troller peu mais bien: "untel = event();" ou "untel = MALADIE::GRIPPE;" ne me semble pas si choquant (à part que la maladie fait gueuler du coup ;)).
                Je pense qu'on pourrait aussi parler de l'affectation à un poste. En bref, en français, affectation => changement d'état. Exactement comme en programmation, non?

                les variables n'ont pas du tout le même sens que celui appris en maths

                Hum… je ne sais pas, sur ce coup, je n'arrive pas à comprendre l'intro wikipedia sur les définitions d'une variable mathématique, dans les différents branches (mathématiques, proba, stats… que des trucs avec lesquels j'étais très, très fâché à l'école). Mais, ça ne me choque pas, je n'ai jamais compris en quel honneur l'informatique devrait être considérée comme une branche des maths.
                Pour moi, c'est l'inverse: les maths sont un outil utilisé par les autres sciences, et sans les autres sciences elles n'ont aucun intérêt pratique.
                Par contre, j'arrive à comprendre les définitions pour les autres domaines:

                • En physique, en biologie, en mécanique et en chimie, la variable représente un paramètre mesurable comme la température, le temps, la vitesse ou l'intensité.
                • En informatique, une variable est un symbole (habituellement un nom) qui renvoie à une position de mémoire dont le contenu peut prendre successivement différentes valeurs pendant l'exécution d'un programme
                • En sociolinguistique, une variable est un mot dont la forme varie selon le genre, le nombre ou la fonction

                La notion commune, c'est le changement, ou pour être plus exact, le nommage d'une valeur qui peut potentiellement changer. Je trouve que ça se tiens, mais c'est peut-être une question de biais personnel. Il faudra que je teste un truc la prochaine fois que je discuterais avec des techniciens qui ne connaissent rien à l'informatique: placer le mot variable pour voir comment ils réagissent.

                Le pire étant de pouvoir modifier le contenu des variables passées en paramètres des fonctions qui complique le raisonnement.

                Hum, je vois ce que tu veux dire, mais à part les langages qui passent des références non constantes par défaut (ça, c'est vraiment dégueu si veux mon avis), le programme appelant ne verra pas la modification.
                Ce qui est regrettable, et je suis d'accord, c'est que par défaut ça ne soit pas constant, et du coup le compilateur ne gueule pas quand on fait void foo( int bar ){bar=2;}.

                (mais pas pour tous les types de variables pour compliquer le truc)

                Pas d'accord.
                En C et en C++, pour tous les types de variables on peut, tant que ce n'est pas constant.
                Ce qui complique le truc, comme tu dis, c'est que certaines variables indiquent la position d'éléments qui ne sont pas elles-même. Et la modification n'affecte qu'une copie, l'appellant ne la verra donc pas.
                Dans d'autres langages, ça peut changer, mais ça serait de très mauvais goût de passer by-ref si les ref ne sont pas constantes…

                J'admets que ça dois pas être simple à expliquer, et typiquement je me souviens que la notion de pointeurs et de mémoire à été difficile à comprendre pour mes camarades de BTS (pourtant, la plupart avait un bac STI électronique… l'adressage mémoire c'était au programme de 1ère me semble). Moi, je ne me souviens pas si j'ai eu du mal: j'ai appris seul donc, pas pu me comparer aux autres.

                • [^] # Re: merci

                  Posté par  (site web personnel) . Évalué à 3.

                  Pourquoi?
                  Quand j'étais enfant et que je lisais (je lisais beaucoup) "untel est affecté par [évènement|maladie]" je comprenais ce que ça voulait dire. Si on traduit en programmation, en C++ histoire de troller peu mais bien: "untel = event();" ou "untel = MALADIE::GRIPPE;" ne me semble pas si choquant (à part que la maladie fait gueuler du coup ;)).
                  Je pense qu'on pourrait aussi parler de l'affectation à un poste. En bref, en français, affectation => changement d'état. Exactement comme en programmation, non?

                  On peut en effet l'expliquer relativement simplement, mais il n'empêche que ça reste moins simple que le fonctionnel où tu n'as pas à te représenter les états de la variable car sa valeur ne change jamais.

                  Mais, ça ne me choque pas, je n'ai jamais compris en quel honneur l'informatique devrait être considérée comme une branche des maths.
                  Pour moi, c'est l'inverse: les maths sont un outil utilisé par les autres sciences, et sans les autres sciences elles n'ont aucun intérêt pratique.

                  Je ne parle même pas de l'importance des maths en info, simplement l'ordre dans lequel les concepts sont appris. Tout le monde apprend ce qu'est une variable en maths avant de l'apprendre en informatique, c'est tout.

                  Hum, je vois ce que tu veux dire, mais à part les langages qui passent des références non constantes par défaut (ça, c'est vraiment dégueu si veux mon avis), le programme appelant ne verra pas la modification.

                  Pas d'accord.
                  En C et en C++, pour tous les types de variables on peut, tant que ce n'est pas constant.

                  Ce que je veux dire, c'est qu'il faut expliquer aux étudiants que la valeur de l'entier qu'il passent en paramètre de la fonction ne pourra pas être modifié par la fonction, mais que ce n'est pas le cas des tableaux (par exemple).

                  Tout ça pour dire qu'on est certes habitué à la programmation impérative et à voir les programmes comme des modifications d'état, mais que ce n'est pas nécessairement la plus naturelle.

                  • [^] # Re: merci

                    Posté par  . Évalué à 1.

                    simplement l'ordre dans lequel les concepts sont appris. Tout le monde apprend ce qu'est une variable en maths avant de l'apprendre en informatique, c'est tout.

                    Dans ce cas, tout le monde apprend ce qu'est une variable en français (un truc qui varie en fonction de l'environnement) avant d'apprendre ce que c'est en maths.

                    Tout ça pour dire qu'on est certes habitué à la programmation impérative et à voir les programmes comme des modifications d'état, mais que ce n'est pas nécessairement la plus naturelle.

                    Je suppose que ça dépend des gens, en fait. J'étais bon en électronique et physique mais mauvais en maths. Pour d'autres c'est l'inverse (pourtant, ces domaines sont très liés!). Le raisonnement n'est pas le même, et de là à dire que l'aisance avec certains paradigmes de programmation en dépends, il n'y a qu'un pas qui ne me semble pas si dur que ça à franchir.
                    Bref, je suis convaincu par tes arguments :)

  • # encore !

    Posté par  . Évalué à 2.

    Je suis en train d'apprendre haskell, donc n'hésite surtout pas.

    J'ai déjà les bases, mais je retiens par exemple les règles de compilation que je ne connaissais pas.

    Merci !

  • # Très (trop ?) dense

    Posté par  (site web personnel) . Évalué à 6.

    Merci pour ce journal assez marrant je trouve. (c'est positif)

    Par contre, Haskell n'est pas le plus simple pour un public non connaisseur. Par exemple, il n'est pas évident de se dire que la signature des fonctions est logique sans comprendre qu'une fonction de plusieurs paramètres peut-être écrite comme la composition de plusieurs fonctions partielles dont la dernière est à 1 paramètre, ceci expliquant la notation des types en Haskell (Curryfication) qui ne ressemble pas au C++/Python.

    Par contre, je pense qu'il y a une question à laquelle il serait bon de répondre par un journal / dépêche : celui de l'organisation du code, ou de sa différence entre l'approche objet, l'approche déclarative, l'approche fonctionnelle pure sans DataType, avec DataType, impérative sans Objet, etc.

    Je trouve que c'est un point difficile à capter quand on change de paradigme, mais qui change drastiquement la manière de penser finalement, et qui remet en place les avantages et inconvénients de chacun, surtout quand on s'habitue à penser dans un paradigme en particulier (genre Objet avec Python et aussi souvent avec C++).

    • [^] # Re: Très (trop ?) dense

      Posté par  (site web personnel) . Évalué à 6.

      Pour le coté dense, oui je l'admet. Comme je le dis dans un commentaire plus haut c'est partie d'une moment "Whaou" simple mais qui demande du contexte, et puis j'ai toujours eu du mal à être concis ;( Saint Exupéry ne serait pas fier de moi.

      Merci pour la remarque "marrant", je prend cela comme un compliment ;)

      Par exemple, il n'est pas évident de se dire que la signature des fonctions est logique sans comprendre qu'une fonction de plusieurs paramètres peut-être écrite comme la composition de plusieurs fonctions partielles dont la dernière est à 1 paramètre, ceci expliquant la notation des types en Haskell (Curryfication) qui ne ressemble pas au C++/Python.

      J'ai essayé de faire un paragraphe la dessus mais finalement je me rend compte qu'il arrive au milieu du texte alors qu'il mériterait d'être au début.

      Par contre, je pense qu'il y a une question à laquelle il serait bon de répondre par un journal / dépêche : celui de l'organisation du code, ou de sa différence entre l'approche objet, l'approche déclarative, l'approche fonctionnelle pure sans DataType, avec DataType, impérative sans Objet, etc.

      Tu aurais un exemple de la problématique que tu sous entend ? J'ai l'impression que je suis biaisé parce que depuis que je fais du Haskell mon style en C++ et en Python a évolué vers quelque chose qui ressemble à Haskell et forcement je suis convaincu que c'est la bonne solution ;)

      J'ai tendance à penser que l'approche fonctionnelle motive la séparation donnée / comportement alors que l'approche objet couple souvent les deux, mais à mon avis ce n'est pas imposé par la technologie, c'est plus un choix de développeur. D'ailleurs la STL C++ a vraiment une approche découplée depuis quelque temps, std::begin() ou lieu de obj.begin() ou l'ensemble <algorythms>, où le discours dernier sur le fait que f.method() pourrait devenir équivalent à method(f) pour unifier les styles d'appel.

      La seule grosse difference que je trouve vraiment dure entre les deux approches c'est pour le polymorphisme runtime, (i.e: les appels virtuels en nomenclature POO). En fonctionnel, du moins en Haskell, j'ai l'impression que 99% des cas de figures sont remplacés par du curying ou des types sommes.

      Par contre je ferais bien un article sur l'organisation de code et le typage. Je trouve que beaucoup de monde dans la communauté POO/mutable confonds généricité et absence de type. Un example que je donne souvent c'est pour la réalisation d'une librairie de vecteur pour un moteur 3D, un développeur c++/python vas faire une classe Point avec pleins d'operations : soustraction, addition, produit scalaire. Finalement il sera heureux, sous prétexte de généricité, d'utiliser la même classe pour generer les points, directions, normales, couleurs. De mon coté, j'aurais plus tendance à définir plusieurs classes / types, au moins Point, Direction, Normal / DirectionNormalisée, Couleur avec un sous ensemble de méthodes limitées aux besoins du domaine. (Par exemple, pas d'addition entre Point, pas de produit scalaire entre Couleur…). Mais encore une fois, c'est Haskell qui m'a biaisé l'esprit de cette manière, mais ce n'est pas inapplicable aux C++…

    • [^] # Re: Très (trop ?) dense

      Posté par  . Évalué à 3.

      Oui enfin le problème de la currification, ça n'est pas vraiment la compréhension (à part peut être pour un débutant) mais si tu appelle une fonction avec n argument mais que tu en oublie un? Dans un langage normal tu as une erreur a la compilation, tu la corrige en 30s et c'est fini, avec Haskell ton résultat n'est pas un résultat mais une fonction donc c'est a lors des utilisations de ce résultat que le compilateur va détecter une erreur.
      Bof, je préfère les fonctions normales, en plus f(a, b) -> (c,d) sépare clairement les entrées des sorties.

      • [^] # Re: Très (trop ?) dense

        Posté par  . Évalué à 2.

        Je me suis mal exprime avec "clairement" je voulais dire: de maniere plus symetrique que a->b->(c,d)

  • # En ce qui concerne Python

    Posté par  . Évalué à 3.

    Très intéressant comme journal, mais, pointilleux comme je le suis, je ne peux m'empêcher de faire remarquer que :

    Cette example de code est équivalent au code python suivant, qui ne marche plus qu'en python 2, l'attribut cmp de sorted ayant disparu en python 3, ce que je regrette.

    N'est pas tout à fait exact. Effectivement, l'argument cmp a été retiré en Python 3, mais il y a plusieurs raisons derrière ce choix :
    1. Fonctionnellement, tout ce qui pouvait être réalisé avec cmp le peut encore avec key. Il y a d'ailleurs une fonction dans functools qui remplit exactement cet office automatiquement : cmp_to_key
    2. L'utilisation d'une telle fonction de comparaison qui doit choisir entre "plus grand", "plus petit" ou "égal" est loin d'être aisé dans le cas général où on peut être "aucun des trois".
    3. Ce modèle de comparaison faisait double emploi avec les fonctions lt, gt, eq, etc.
    4. Finalement, comme plusieurs l'ont fait remarquer, utiliser key est généralement plus efficace que cmp, puisque key est appelé une seule fois par élément (O(n) donc), puis la comparaison (O(n log n) pour ordonner) est effectuée en C si les clés sont des type de base. À l'opposé, utiliser cmp fait en sorte que ces O(n log n) comparaisons impliqueront toutes un appel d'une fonction Python.

    Bref, pour toutes ces raisons, je ne pense pas que ce soit dommage que cet argument ait été retiré, au contraire, cela apporte une meilleure cohérence au langage.

    • [^] # Re: En ce qui concerne Python

      Posté par  (site web personnel) . Évalué à 3.

      1.

      Merci, je ne connaissais pas cmp_to_key.

      1. L'utilisation d'une telle fonction de comparaison qui doit choisir entre "plus grand", "plus petit" ou "égal" est loin d'être aisé dans le cas général où on peut être "aucun des trois".

      Tu as un exemple de cela ? Si tu veux trier, tu dois forcement pouvoir définir un ordre, au moins arbitraire. Tu peux te contenter de <= pour établir une relation d'ordre. Il y a un mapping bijectif entre key et cmp, du moins je pense.

      1. Finalement, comme plusieurs l'ont fait remarquer, utiliser key est généralement plus efficace que cmp, puisque key est appelé une seule fois par élément (O(n) donc), puis la comparaison (O(n log n) pour ordonner) est effectuée en C si les clés sont des type de base. À l'opposé, utiliser cmp fait en sorte que ces O(n log n) comparaisons impliqueront toutes un appel d'une fonction Python.

      En effet, mais l'overhead mémoire peut ne pas être négligeable. Je n'ai pas regardé l'implementation, mais j'imagine que le tri en place avec cmp peut se faire en O(1) en mémoire, si on ignore la mémoire prise par la liste initiale. Alors que le tri avec le DSU ajoute forcement un overhead en O(n) pour stocker les résultats de l'appel à key. En fonction du problème cela peut être une limitation, même si j'avoue que dans le cas d'un tableau énorme on passera surement par une autre structure, type numpy.

      • [^] # Re: En ce qui concerne Python

        Posté par  . Évalué à 1.

        Tu as un exemple de cela ? Si tu veux trier, tu dois forcement pouvoir définir un ordre, au moins arbitraire. Tu peux te contenter de <= pour établir une relation d'ordre. Il y a un mapping bijectif entre key et cmp, du moins je pense.

        Je te l'accorde, je suis allé un peu vite pour résumer ceci. L'idée est qu'il possible de ne pas pouvoir comparer certains objets, tout en supportant de manière générale la comparaison avec ce type, ce qui rend l'utilisation de cmp (et de son pendant, l'attribut _ _ cmp _ _) beaucoup moins logique que les opérateurs de comparaison tels que lt, gt, eq, etc. Ces derniers faisant double emploi avec cmp, il a été décidé de retirer la "vieille" manière de procéder.

        Pour ce qui est de la mémoire, formellement je pense que tu as raison, mais j'émets l'hypothèse qu'en pratique ce n'est pas très problématique, pour deux raisons. Premièrement, parce que la plupart des clés de comparaison sont des types simples et consommant donc peu de mémoire, si bien qu'à moins d'avoir d'immenses listes, l'overhead mémoire risque d'être faible. Par ailleurs, il est toujours possible de revenir au comportement de cmp, à savoir d'appeler la fonction passée en argument key à chaque fois sans conserver le résultat. En bref, en utilisant key, on peut se comporter similairement à cmp, mais l'inverse n'est pas vrai.

        Je t'accorde cependant que je m'obstine sur des broutilles. In fine, je crois que la raison principale qui a motivé ce retrait est le principe suivi par Python selon lequel il ne doit y avoir qu'une et une seule façon de faire une tâche. En l'occurrence, cmp et key remplissaient grosso modo le même office, donc l'utilisation de l'un d'entre eux a été retiré.

  • # Moi == pas doué, je suppose....

    Posté par  . Évalué à 3.

    Je vais commencer par le commencement… Haskell, un langage dont je n'ai lu que du bien. ouai, promis, je n'en ai que lu, et c'était uniquement du bien.
    Bon.

    Maintenant, tu as réussi à me perdre sur un simple tri…

    Sort

    Cette fonction s'appelle sort et a comme signature :

    sort :: Ord a => [a] -> [a]
    Dit autrement, c'est une fonction qui prend [a], une liste de n'importe quel type a et renvoie une liste de n'importe quel type a. Seule contrainte sur a, ce type doit être Ordonable, c'est à dire que on doit pouvoir comparer les éléments de a entre eux. La liste est une liste simplement chaînées et elle est non modifiable, ainsi le tri ne se fait pas en place, mais crée une nouvelle liste.

    Cela ressemblerait un peu à cette fonction en c++ :

    template
    std::list sort(const std::list &l);

    Bon, que je sois mauvais ou non n'est pas vraiment important, j'espère… mais, pour le coup, toi, tu es mauvais pédagogue. En tout bien tout honneur bien sûr, et chacun son job. Prof, c'est manifestement ni mon métier (sachant que, pour le moment, j'ai pas de taf, si ça veut dire quelque chose), ni le tien. Je ne dis pas ça pour t'empêcher de faire de nouveaux journaux, au contraire… et puis merde, je vais balancer le reste sans politiquement correct, et faire confiance à ton sens critique, puisqu'après tout, tu postes sur linuxfr.

    La première chose qu'il me semble importante de noter, c'est l'aspect très… matheux, du langage. C'est con à dire, mais, moi, j'ai jamais été un matheux. Ouai, ok, j'ai un bac sti "génie" électronique, mais je me tapais des 8 en maths, quand, en physique je tapais du 13+ et du 15+ en élec.

    Le rapport? C'est simple, très simple, mais peut-être aussi lié à la résistance au changement (oui, j'ai aussi étudié la gestion de projet, même si… peu importe): la syntaxe de ton exemple est absolument imbuvable.
    Soyons sérieux, un instant. Ta fonction, elle prend [a] en argument. Soit. dans ton exemple de déclaration, je vois le [a] 2 fois. Quelle occurrence est donc le paramètre, histoire que l'on puisse au moins deviner quelle autre occurrence est le retour?
    Le coup du ordonnable (2n, au passage, me dit mon correcteur), on le remarque comment? Encore une fois, tu me rejette dans mon ignorance crasse, et ça me semble mauvais en terme de pédagogie.
    Ensuite, une "liste simplement chaînées" (j'ai reproduit le 's' après quelques secondes d'hésitations, parce que ça me fait chier de jouer les nazi du french) et non modifiable. Le haskell semble séduire quelques utilisateur du C++, mais je n'ai jamais compris (ni cherché) pourquoi? Ok, une liste simple, ça veut dire 1 pointeur de moins par élément, soit 8 bytes (ou octets en français, mais le terme est moins précis, niveau sémantique, un byte n'étant pas forcément constitués de 8 bits) et dans le cas de haskell c'est du const. Bien. Pourquoi pas. Mais dans ce cas, quel est l'impact en terme de perfs lorsqu'une fonction à pour simple but de construire une liste chaînée? Doit-elle être copiée-collée? J'espère bien que non.

    Bref, ton journal est bien trop… hum… c'est cru, mais mon sentiment, alors, voilà: imbu de ta connaissance.
    Tu sembles oublier que l'on ne maîtrise pas tous le Haskell, ni sa syntaxe.
    Le C, à imposé sa syntaxe comme un quasi-standard. Le pascal aussi. Les paradigmes de ces langages sont peut-être moins efficaces (ref. souhaitée) mais plus adaptés à la façon de penser des gens normaux (ref souhaitée aussi), c'est à dire pas théoriciens des langages ni matheux (ref souhaitée encore, notez comme je m'auto-troll).
    Une question simple, pour clore ce déjà trop long post: pourquoi ne pas nous montrer le traditionnel hello world, en C, puis en C++ (2 langages différents pour moi, bien que très proches), puis en JS (à la mode), python (idem), et enfin haskell?
    Même si le hello world ne montre quasiment rien du langage, il permets au moins aux devs de savoir comment écrire un programme qui ne fait rien. C'est, l'ai de rien, très important de savoir faire un truc qui ne fait rien avec le minimum de code. En fait, c'est la base pour savoir écrire un programme qui fait uniquement ce qu'on lui demande.

    PS: je n'ai lu que le court passage que j'ai cité. Ça m'a déjà semblé trop inaccessible, trop chiant. Cool, on est dredi.

    • [^] # Re: Moi == pas doué, je suppose....

      Posté par  (site web personnel) . Évalué à 4.

      Le coup du ordonnable (2n, au passage, me dit mon correcteur), on le remarque comment?

      J'ai mis un commentaire sur le problème de la lecture de la signature qui n'est pas naturelle (lire pas enseignée), mais donc oui c'est pas simple si on ne connait pas. Dans la signature Ord a => indique a doit être un type implémentant Ord (d'ordonnable).

      Les paradigmes de ces langages sont peut-être moins efficaces (ref. souhaitée) mais plus adaptés à la façon de penser des gens normaux (ref souhaitée aussi), c'est à dire pas théoriciens des langages ni matheux (ref souhaitée encore, notez comme je m'auto-troll).

      Alors moi je me rappelle très clairement de quand j'ai appris le « i = i + 1 » qui n'était absolument pas naturel (ben on casse juste la signification du « = » que l'on apprend à l'école, et en plus l'assignation de la nouvelle valeur se fait à postériori !). Mais c'est assez naturelle après de la pratique. C'est donc plus une question d'habitude que de « naturalitée » à mon humble avis.

      Bref, ton journal est bien trop… hum… c'est cru, mais mon sentiment, alors, voilà: imbu de ta connaissance.
      Tu sembles oublier que l'on ne maîtrise pas tous le Haskell, ni sa syntaxe.

      Il faudrait peut-être rajouter des pré-requis à certains journaux / dépêches.

      pourquoi ne pas nous montrer le traditionnel hello world, (…) et enfin haskell?

      Parce que c'est pas si simple. Il faut comprendre que dans les langages que tu as cités, le postulat de base est que « tout est mutable » par défaut, c'est-à-dire que les variables et fonctions peuvent renvoyer des résultats qui changent avec les mêmes paramètres.
      En fonctionnel pur (ce qu'est Haskell), c'est le postulat inverse. Du coup, écrire un hello world en haskell va forcer à utiliser un mécanisme autorisant à casser cette immutabilité, et ça implique d'utiliser une monade IO, mais là il faut donc expliquer ce qu'est une monade. Bref, on a pas fini. hello world est ici le mauvais exemple de base simple pour ce type de langage.

      • [^] # Re: Moi == pas doué, je suppose....

        Posté par  . Évalué à 2.

        Alors moi je me rappelle très clairement de quand j'ai appris le « i = i + 1 » qui n'était absolument pas naturel

        Le C (et ses fils) sont en effet moches sur ce point, mais ce n'est pas une fatalité, certains langages utilisent d'autres signes (: en pascal je crois?).
        Je parle de paradigme, et tu réponds avec un exemple de syntaxe :)

        En fonctionnel pur (ce qu'est Haskell), c'est le postulat inverse.

        Oui, je sais. J'avais essayé de me mettre à je sais plus quel langage fonctionnel à un moment donné. Mais le fait est que je n'ai jamais réussi à faire une I/O, très gênant, un programme qui ne modifie rien, je trouve (y compris l'écran…).

        Ça reviens à ce que je disais, les langages fonctionnels sont complexes, peu naturels. Dire à quelqu'un qu'il va apprendre la programmation pour écrire des programmes qui n'interagissent avec rien, ça me semble difficilement motivant.

        • [^] # Re: Moi == pas doué, je suppose....

          Posté par  (site web personnel) . Évalué à 6. Dernière modification le 20 février 2016 à 15:14.

          Ça reviens à ce que je disais, les langages fonctionnels sont complexes, peu naturels. Dire à quelqu'un qu'il va apprendre la programmation pour écrire des programmes qui n'interagissent avec rien, ça me semble difficilement motivant.

          D'un autre côté, Haskell n'est pas le langage fonctionnel le plus simple pour commencer. OCaml est par exemple bien plus accessible (afficher un "Hello world" ne fait pas apparaître des concepts compliqués, contrairement à Haskell avec la monade IO). Haskell ne vise pas à être un langage facile à apprendre, c'est un peu comme C++ (ou Rust), mais dans un genre plus mathématique, ce qui fait que les interactions avec le monde extérieur ne sont pas la première chose mise en avant.

          Le fait qu'un langage soit fonctionnel ne le rend pas complexe ou peu naturel en soi. En fait, je trouve par exemple que l'utilisation de fonctions courantes d'ordre supérieur sur les listes est plutôt sain, et je ne m'en prive pas non plus lorsque j'écris dans un langage non particulièrement fonctionnel qui les propose (même si c'est toujours une question de limite : par exemple, autant map et filter me semblent vraiment apporter quelque chose, je n'ai pas toujours l'impression que les fold_left apportent beaucoup par rapport à une boucle). Dans le cas de Haskell, c'est aussi un peu un terrain de prototypage pour plein d'idées sur les système de types, du coup il y a plein d'extensions, c'est complexe et on se cogne à un moment ou à un autre avec des concepts théoriques. D'autres langages comme OCaml sont un peu plus conservateurs (ça veut pas dire que tout code OCaml va être compréhensible pour un néophyte non plus, et ces derniers temps il y a pas mal de nouveautés).

          Ceci dit, ce qui en pratique freine le plus sont des choses pas vraiment liées aux langages en soi : le fait que le langage offre des possibilités complexes ne veut pas dire que faire des choses simples va devenir compliqué. Mais en prenant un livre un peu au hasard sur Lisp ou Haskell, il y a de bonnes chances qu'une bonne partie du livre, surtout l'introduction, soit dédiée à expliquer pourquoi le langage est si génial (super macros ou super système de types), ce qui a de bonnes chances d'arrêter quelqu'un qui n'a pas assez de patience ou de temps pour arriver au stade où il peut écrire des choses utiles de base avant de faire des choses géniales. Et puis même une fois les bases du langage maîtrisées, c'est facile de tomber sur un package documenté à l'aide seul de la signature de ses fonctions et une ligne de description pour chacune (ou zéro), et c'est pas souvent que l'on trouve des exemples tout prêts qui permettent d'emblée de se faire une idée de comment on va utiliser le truc : on attend souvent de l'utilisateur qu'il étudie l'API et réfléchisse lui-même à la bonne façon d'agencer les types, et ça fait perdre du temps.

          Il y a aussi le fait que ce n'est pas parce qu'on commence à comprendre Haskell et faire des choses utiles facilement que l'on va être capable de lire le code des autres (si ça se trouve, ils utilisent des Lens ou des Arrows ou autre nouveauté, et il faut potasser un peu de théorie avant de pouvoir faire quelque chose :) ). Et ça peut parfois être un inconvénient. On reproche souvent quand quelqu'un fait des choses « trop intelligentes ». Eh bien avec Haskell ça arrive assez souvent (moins avec OCaml, je pense), ce qui a du bon comme du mauvais, mais peut être déroutant et faire perdre du temps en pratique suivant l'utilisation que l'on en fait (c'est pour ça qu'après avoir utilisé un serveur web Haskell faisant usage des Lens un moment, j'étais passé ensuite à du Perl pour reposer les neurones et être sûr qu'au bout d'un an j'ai pas oublié la théorie).

          • [^] # Re: Moi == pas doué, je suppose....

            Posté par  (site web personnel) . Évalué à 5.

            Tu soulèves des points importants que je veux discuter ;) J'aime discuter, il est Dimanche, mon fils dors, j'ai rien à faire, alors discutons ;)

            Juste une chose, je ne défend pas Haskell bec et ongles, je gagne ma vie en faisant du C++, j'ai contribué et je contribue encore a Python où à des projets en Python et j'apprécie Haskell. La suite de ce message peut donner l'impression que j'essaye de montrer qu'Haskell est bien meilleurs que ces langages, je ne le pense pas, je pense juste que c'est different, mais en répondant point à point à ton message, on est tenté de faire des comparaisons, j’espère donc ne pas être jugé comme un extrémiste Haskell ;)

            D'un autre côté, Haskell n'est pas le langage fonctionnel le plus simple pour commencer. OCaml est par exemple bien plus accessible (afficher un "Hello world" ne fait pas apparaître des concepts compliqués, contrairement à Haskell avec la monade IO).

            (Je ne connais pas du tout OCaml, mais bon…, je viens de jeter un œil, mais le hello world Ocaml print_string "Hello World!\n";; me fait peur avec ses deux ;; alors que le hello world Haskell main = putStrLn "Hello World" me fait moins peur et n'introduit aucun concept compliqué… Je troll, mais bon, j'essaye de montrer à quel point tous les arguments de cette discussion sont subjectifs.)

            J'ai vraiment l'impression que Haskell fait peur alors qu'il ne devrait pas. Sans doute à cause de sa réputation de langage pour Matheux et une communauté qui n'a pas vraiment la motivation pour rendre cela accessible. J'ai aussi l'impression que beaucoup de gens oublient les concepts avancés qu'ils ont du comprendre pour écrire leur première ligne de code dans n'importe quel langage. Blague à part, je n'ai jamais fais de OCaml car en voyant que tous les étudiants de prépas "prestigieuses" en faisait, j'ai eu l'impression que c'était un langage théorique pour matheux, surtout sachant que c'était poussé par l'INRIA. J'ai donc choisis Haskell qui me semblait moins élitiste du fait de mon historique ;)

            On peut écrire beaucoup de code sans rien comprendre aux monades, sans rien comprendre à IO, à partir du moment où tu acceptes sans te poser de question que pour récupérer la valeur de retour d'une fonction à effet de bord, c'est <- et pas = et que les fonctions qui font des IO retournent leur résultat avec return.

            Dans un hello world C++, avant de pouvoir faire quoi que ce soit, tu dois faire au moins un include de iostream. Tu vas devoir mettre explicitement des types alors que tu ne sais pas forcement ce que cela signifie. Tu vas devoir utiliser des namespace (std::cout ou std::cin) ou le trop classique using namespace std. Tu vas devoir utiliser les opérateurs << et >>, et j'en passe. Tout autant de concepts compliqués que tu vas accepter en tant que débutant.

            je n'ai pas toujours l'impression que les fold_left apportent beaucoup par rapport à une boucle.

            J'étais initialement d'accord avec toi. Finalement, je trouve agréable le fait qu'un fold représente vraiment une opération de plus haut niveau. Puis une boucle, c'est ENORMEMENT de concepts d'un coup. Aux heures sombres de ma vie, j'ai enseigné à l'IUT, la fac et en école d'ingénieur à des groupes de débutant et je peux t'assurer que les boucles ce n'est pas une mince affaire, les étudiants oublient de l'initialiser, d’incrémenter l'iterateur, ne comprennent pas ce que cela fait vraiment.

            Dans le cas de Haskell, c'est aussi un peu un terrain de prototypage pour plein d'idées sur les système de types, du coup il y a plein d'extensions, c'est complexe et on se cogne à un moment ou à un autre avec des concepts théoriques.

            À l'inverse, beaucoup de dev C++ se cassent les dents sur certains concepts "théoriques" spécifiques au langage comme l'abondance de constructeurs / destructeurs spécifiques et les subtilités de leur implémentation. Je cite l'exemple de python sur la mutabilité des objets :

            >>> a = 5
            >>> b = a
            >>> b += 2
            >>> a
            5
            >>> b
            7
            >>> a = [1,2,3]
            >>> b = a
            >>> a += [4, 5, 6]
            >>> a
            [1, 2, 3, 4, 5, 6]
            >>> b
            [1, 2, 3, 4, 5, 6]

            Ça c'est deux bonnes heures d'explication à des étudiants en stress énorme… Même après plus de 12 ans de python, je me plante encore des fois sur ce genre de bêtise…

            Bref, là où je veux en venir c'est que je suis d'accord Haskell est un language complexe, comme beaucoup, mais pas forcement plus compliqué à comprendre que d'autre, c'est juste que les problèmes sont autre part.

            Ceci dit, ce qui en pratique freine le plus sont des choses pas vraiment liées aux langages en soi : le fait que le langage offre des possibilités complexes ne veut pas dire que faire des choses simples va devenir compliqué. Mais en prenant un livre un peu au hasard sur Lisp ou Haskell, il y a de bonnes chances qu'une bonne partie du livre, surtout l'introduction, soit dédiée à expliquer pourquoi le langage est si génial (super macros ou super système de types), ce qui a de bonnes chances d'arrêter quelqu'un qui n'a pas assez de patience ou de temps pour arriver au stade où il peut écrire des choses utiles de base avant de faire des choses géniales. Et puis même une fois les bases du langage maîtrisées, c'est facile de tomber sur un package documenté à l'aide seul de la signature de ses fonctions et une ligne de description pour chacune (ou zéro), et c'est pas souvent que l'on trouve des exemples tout prêts qui permettent d'emblée de se faire une idée de comment on va utiliser le truc : on attend souvent de l'utilisateur qu'il étudie l'API et réfléchisse lui-même à la bonne façon d'agencer les types, et ça fait perdre du temps.

            On en reviens au problème d’évangélisme et je suis d'accord avec toi. Mais ce n'est pas particulièrement une pathologie Haskell, comme partout, certaines libs sont bien documentées, d'autres non. Mais aux moins il y a des types expressifs qui aident et un compilateur qui gueule. Parce que les projets non documentés en python avec des API qui acceptent des chaînes de caractères à la place d'un Enum et pour lesquelles tu dois chercher dans le code source pour savoir quels sont les valeurs possibles et qui ne pètent pas le jour où la valeur choisie disparaît de la lib. Ou les API qui utilisent des types "fourre-tout", comme un vecteur à trois dimension, pour représenter des objets plus contraints, comme des directions ou des couleurs, il y en a partout et j'ai l'impression qu'en Haskell il y en a moins car les auteurs de librairies ont tendance aux types.

            Il y a aussi le fait que ce n'est pas parce qu'on commence à comprendre Haskell et faire des choses utiles facilement que l'on va être capable de lire le code des autres (si ça se trouve, ils utilisent des Lens ou des Arrows ou autre nouveauté, et il faut potasser un peu de théorie avant de pouvoir faire quelque chose :)

            Oui… Et non… Si l'abstraction est bien faite, tu peux comprendre ce que le code représente sans pour autant comprendre comment cela fonctionne. On fait cela tous les jours avec de nombreuses fonctions "simples" (Qui sait vraiment ce qui se passe lors d'un printf ?). Mais encore une fois, c'est un argument qui est valable dans n'importe quel langage. Une librairie C++ utilisant un peu de meta programmation template. Il faut énormément potasser de théorie pour avoir la moindre idée de ce qui est fait. Aller lire un peu de python "intelligent" où le code s'introspecte et se modifie tout seul en fonction du sens du vent. Aller lire un peu de javascript qui basé sur le nom de ta méthode injecte du html autour d'un tag dont la classe css correspond à ta méthode via un mapping auto généré selon des expression régulière… Il y a le même problème partout, seulement j'ai l'impression que quand quelqu'un n'est pas fan d'une technologie, il reproche ces problèmes a la technologie en question en ignorant que les autres souffrent de la même pathologie.

            • [^] # Re: Moi == pas doué, je suppose....

              Posté par  (site web personnel) . Évalué à 3. Dernière modification le 21 février 2016 à 22:25.

              Juste une chose, je ne défend pas Haskell bec et ongles, je gagne ma vie en faisant du C++, j'ai contribué et je contribue encore a Python où à des projets en Python et j'apprécie Haskell.

              Je précise aussi que je suis loin de détester Haskell, c'est un des premiers langages que j'ai appris parce que le côté mathématique m'attirait (je suis mathématicien à la base), et il y a bien des choses que j'apprécie dans ce langage (en fait, je suis capable sans problème d'aimer des langages aux philosophies totalement opposées :) ).

              me fait peur avec ses deux ;;

              Les ;; c'est que pour la REPL uniquement, normalement on n'en écrit jamais. Ceci dit, pour Hello World j'exagère un peu en ce qui concerne Haskell, mais pour des choses un poil plus compliquées, si on comprend pas ce qu'est la monade IO dont on ne peut pas sortir, il y a de quoi s'étonner, quand même (quelqu'un qui veut débugger une fonction avec un print aura une mauvaise surprise, par exemple).

              Blague à part, je n'ai jamais fais de OCaml car en voyant que tous les étudiants de prépas "prestigieuses" en faisait, j'ai eu l'impression que c'était un langage théorique pour matheux, surtout sachant que c'était poussé par l'INRIA.

              Je crois que tu te fais des idées : OCaml c'est à peu près le même public que Haskell, j'ai l'impression, avec un bon pourcentage de public académique qui aime les systèmes de types dans les deux cas. Ceci dit, Haskell a un système de types plus compliqué qu'OCaml, et a un modèle pour l'IO moins simple. Par exemple, utiliser des tableaux mutables (parfois c'est nécessaire) est une plaie en Haskell (soit IO soit ST, dans les deux cas ça ne s'intègre pas très naturellement avec le reste), alors que c'est simple en OCaml et dans la plupart des langages. Tout est question de compromis, et là où Haskell perd il gagne ailleurs, mais comme c'est sur des choses assez atypiques, ça surprend.

              J'étais initialement d'accord avec toi. Finalement, je trouve agréable le fait qu'un fold représente vraiment une opération de plus haut niveau. Puis une boucle, c'est ENORMEMENT de concepts d'un coup. Aux heures sombres de ma vie, j'ai enseigné à l'IUT, la fac et en école d'ingénieur à des groupes de débutant et je peux t'assurer que les boucles ce n'est pas une mince affaire, les étudiants oublient de l'initialiser, d’incrémenter l'iterateur, ne comprennent pas ce que cela fait vraiment.

              Par boucle je pensais à des boucles de plus haut niveau permettant d'itérer sur une liste, par exemple, pas une boucle for C. Et je dis pas que le fold c'est mal, je dis juste que dans ces cas là, par rapport à une boucle de haut niveau, c'est à peu près pareil à l'usage, avec des chances similaires de faire des erreurs, juste qu'au lieu d'avoir une syntaxe spécifique on doit se rappeler de la signature de fold (fold_left, bien sûr). Après, personnellement, mon problème avec le fold, c'est que je ne me souviens jamais de l'ordre des arguments entre l'élément initial et la liste (je me souviens juste qu'en Coq c'est l'inverse d'OCaml, et comme j'utilise les deux assez souvent ça n'aide pas :) ).

              Ça c'est deux bonnes heures d'explication à des étudiants en stress énorme… Même après plus de 12 ans de python, je me plante encore des fois sur ce genre de bêtise…

              J'aime pas spécialement la sémantique de copie de référence non plus (je suis plus habitué au Perl qui passe par valeur et fait des vraies copies à moins de créer explicitement des références).

              Bref, là où je veux en venir c'est que je suis d'accord Haskell est un language complexe, comme beaucoup, mais pas forcement plus compliqué à comprendre que d'autre, c'est juste que les problèmes sont autre part.

              Je suis bien d'accord. D'ailleurs, du C++ je n'en ai fait qu'une seule fois, et ça m'a semblé le genre de langage que si on n'en fait pas pendant un an, il faut réapprendre (comme Haskell). Ceci dit, comme tu dis, les points difficiles ne sont pas au mêmes endroits.

              Oui… Et non… Si l'abstraction est bien faite, tu peux comprendre ce que le code représente sans pour autant comprendre comment cela fonctionne.

              Oui, mais des fois c'est pas évident de comprendre une abstraction : de temps en temps si l'abstraction ou la doc ne sont pas suffisamment claires et qu'il te faut regarder le code (ou l'abstraction n'est pas totale et te fait manipuler des Lens explicitement ou autre, ou t'expose une API pour faire du xml avec des Arrows), là tu dois vraiment potasser un peu avant de comprendre de quoi il retourne.

              Parce que les projets non documentés en python avec des API qui acceptent des chaînes de caractères à la place d'un Enum et pour lesquelles tu dois chercher dans le code source pour savoir quels sont les valeurs possibles et qui ne pètent pas le jour où la valeur choisie disparaît de la lib.

              Peut-être, je n'ai jamais utilisé beaucoup plus que la librairie standard en Python. Peut-être que je suis trop habitué au Perl, où l'habitude de mettre un Synopsis avec des exemples est très encrée dans la culture. Il faut dire que contrairement à Haskell ou OCaml, ou un langage avec au moins des signatures plus formelles, il n'y a pas trop le choix, car pas de documentation automatique des signatures et pas moyen du coup d'imaginer comment agencer quoi que ce soit, du coup ça incite peut-être à mettre des exemples et écrire une documentation moins automatique et plus pragmatique.

              • [^] # Re: Moi == pas doué, je suppose....

                Posté par  . Évalué à 2. Dernière modification le 22 février 2016 à 00:11.

                J'aime pas spécialement la sémantique de copie de référence non plus (je suis plus habitué au Perl qui passe par valeur et fait des vraies copies à moins de créer explicitement des références).

                Juste pour savoir, quelle langage passe par défaut des références?

                C++ […] m'a semblé le genre de langage que si on n'en fait pas pendant un an, il faut réapprendre

                Honnêtement, le C++ à une syntaxe horrible, je ne pourrai jamais affirmer le contraire, particulièrement quand on utilise les templates, et de manière générale la STL.
                Depuis C++11, on peut enfin utiliser sans trop se prendre la tête, grâce aux lambdas et std::bind, mais je me souviens d'avoir implémenté une lib de fenêtrage (pour la SDL même si le code était plutôt générique… faudrait que je remette la main dessus et que je le nettoie) et dans la gestion des évènements, j'avais une ligne en particulier (qui en faisais 2 dans vim, d'ailleurs) pour laquelle j'avais écrit un paragraphe complet de commentaire, moi qui n'en écrivait que très très peu à l'époque (ce qui n'à pas changé, d'ailleurs, mais bon je préfère un code qui se commente tout seul).

                Autre chose que je trouve imbuvable, les streams. J'ai un mal fou avec les opérateurs '<<' et '>>'. Ça viens peut-être du fait que j'ai commencé par le C, mais je trouve printf et puts plus clairs. Pour scanf, non, mais c'est que scanf est un truc vraiment complexe (je veux dire, ça manipule du pointeur dans tous les sens ainsi que des conversions pas forcément évidentes à comprendre).

                Par contre, mais ce n'est que mon opinion, si on reste à un niveau faible je trouve qu'il est moins délicat à manier que C ou Java (moins de sucre syntaxique que ces deux-là). Pour le C, il faut vraiment gérer toutes les ressources manuellement, tandis que pour Java, on se retrouve avec une tonne de sucre syntaxique pour le moindre pet (tout est obligatoirement objet donc il faut des classes pour tout, il faut obligatoirement ajouter des try/catch, les "include" font vite la taille du bras…) et si on joue avec des ressources qui sont vraiment demandées, il faut la même rigueur qu'en C, parce que le GC ne passe pas forcément quand il faut (et on ne peut pas l'y forcer, à ce que je sais).
                La complexité du C++ est plus dans l'écriture de libs avec de bonnes APIs. Est-ce pareil pour les autres langages, je suppose que oui mais ça dois être moins pire (encore une fois mention spéciale aux templates C++, très puissants mais illisibles et qui génèrent des messages d'erreur horribles).

                si l'abstraction ou la doc ne sont pas suffisamment claires

                Je pense que c'est justement là le point le plus difficile en programmation: exposer une interface assez simple et claire pour être compréhensible rapidement. Je doute que changer de langage change ça, c'est plus du niveau de l'architecture logicielle?

                • [^] # Re: Moi == pas doué, je suppose....

                  Posté par  (site web personnel) . Évalué à 3.

                  Juste pour savoir, quelle langage passe par défaut des références?

                  Par défaut c'était beaucoup dire, je faisais référence par exemple au fait que les tableaux sont souvent passés par référence, et par exemple en Python dans l'exemple donné par Guillaum une copie b = a d'un tableau ne crée pas une copie du tableau, juste une copie de la référence au tableau (ce qui peut être surprenant, car la même syntaxe crée une vraie copie pour les nombres). Mais bon, c'est pas non plus vraiment un défaut, ce genre d'irrégularités a ses raisons d'être (par exemple, en Perl le fait de rendre plus explicite les références devient parfois verbeux), c'est juste que ça peut être surprenant.

                  Je pense que c'est justement là le point le plus difficile en programmation: exposer une interface assez simple et claire pour être compréhensible rapidement. Je doute que changer de langage change ça, c'est plus du niveau de l'architecture logicielle?

                  Ce que je veux dire c'est que si l'interface est pas claire, tu risque de passer un pire moment en Haskell (pas forcément non plus), mais surtout que même si elle est bien faite, il y a plus de chances qu'elle fasse appel à des notions théoriques nouvelles pour être comprise (par exemple la notion d'Arrow permet de voire certaines choses comme faire un filtre sur du xml de façon assez élégante, mais on comprend pas immédiatement pourquoi ça peut être bien, d'ailleurs je ne me souviens plus très bien). Et puis après, d'expérience, j'ai l'impression que plus un langage propose des moyens de documenter automatiquement et formellement (signatures des fonctions, etc.), moins il y a d'exemples dans la doc en moyenne (c'est loin d'être un théorème ce que je dis là, juste une impression et qui a sans doute plus à voir avec les habitudes et traditions du public du langage en question, il y a des exceptions).

                  • [^] # Re: Moi == pas doué, je suppose....

                    Posté par  (site web personnel) . Évalué à 3.

                    Par défaut c'était beaucoup dire, je faisais référence par exemple au fait que les tableaux sont souvent passés par référence, et par exemple en Python dans l'exemple donné par Guillaum une copie b = a d'un tableau ne crée pas une copie du tableau, juste une copie de la référence au tableau (ce qui peut être surprenant, car la même syntaxe crée une vraie copie pour les nombres).

                    Techniquement ce n'est pas = qui est en faute ici, mais la sémantique de += ;)

                    >>> a = [1,2,3]
                    >>> b = a
                    >>> a = a + [4,5,6]
                    >>> a
                    [1, 2, 3, 4, 5, 6]
                    >>> b
                    [1, 2, 3]

                    Pour la suite je répond au deux ;)

                    Je pense que c'est justement là le point le plus difficile en programmation: exposer une interface assez simple et claire pour être compréhensible rapidement. Je doute que changer de langage change ça, c'est plus du niveau de l'architecture logicielle?

                    Le langage peut aider avec certaines construction syntaxiques. Par example, l'absence de type algébriques en C++, (i.e. des unions "safe") ne permet pas de facilement retourner un "optional". Boost et c++17 proposent des optionals, mais ils sont limités car il n'y a pas de pattern matching et donc tu peux te tromper facilement.

                    En Haskell ou Rust ou OCaml, tu peux retourner un Maybe, un type qui contient soit ta valeur de retour Just v soit Nothing et pour t'en servir tu es forcé de pattern matché dessus. C'est simple, clair, sécurisé et sympatique, et il faut un support du langage pour. Après tu ajoutes la syntaxe do en Haskell qui, de ce que je sais, est plutôt unique dans son genre, et tu peut t'exprimer encore plus clairement. (Je fournis des examples si vous voulez).

                    Ce que je veux dire c'est que si l'interface est pas claire, tu risque de passer un pire moment en Haskell (pas forcément non plus), mais surtout que même si elle est bien faite, il y a plus de chances qu'elle fasse appel à des notions théoriques nouvelles pour être comprise (par exemple la notion d'Arrow permet de voire certaines choses comme faire un filtre sur du xml de façon assez élégante, mais on comprend pas immédiatement pourquoi ça peut être bien, d'ailleurs je ne me souviens plus très bien).

                    Oui, c'est vrai, mais il n'est pas forcement nécessaire de comprendre une abstraction pour s'en servir et l'utiliser. Prend l'exemple des parseurs type Parsec, je trouve la syntaxe plutôt simple… Pourtant derrière cela utilise des Monades plutôt trapues.

                    Et puis après, d'expérience, j'ai l'impression que plus un langage propose des moyens de documenter automatiquement et formellement (signatures des fonctions, etc.), moins il y a d'exemples dans la doc en moyenne (c'est loin d'être un théorème ce que je dis là, juste une impression et qui a sans doute plus à voir avec les habitudes et traditions du public du langage en question, il y a des exceptions).

                    Je suis malheureusement d'accord avec toi, et surtout, en tout cas dans le cas d'Haskell, les auteurs pensent que cracher une phrase genre "Cette libraire gère des endo functors polymorphiques de rang 4 isomorphiques à des bifunctors tribales, cf. types." va aider ;)

                    Mais clairement le même problème existe dans de nombreuses librairies de la vraie vie en python ou en c++. Dernièrement avec une librairie standard de l'industrie, une fonction qui prend en paramètre une chaîne de caractère pouvant avoir 10 valeurs différentes. Pas documentée. Je dois regarder le code source du binding python pour voir que la chaîne est utilisée tel qu'elle pour construire une constante, genre constante = CONSTANTES["MODE_" + laChaine] et passée à la lib C++ associée qui n'est pas documentée pour cette fonction si ce n'est qu'elle prend une constante int en paramètre. Je regarde le code source de la lib C++ et après être descendu de 3 appels de fonction, je trouve enfin la comparaison avec des constantes et grâce à la lecture du code j'en deduis celle qui fait ce que je veux… J'aurais eu une fonction non documenté mais avec un type sérieux, j'aurais été plus heureux ;)

                    • [^] # Re: Moi == pas doué, je suppose....

                      Posté par  (site web personnel) . Évalué à 2.

                      Techniquement ce n'est pas = qui est en faute ici, mais la sémantique de += ;)

                      Aussi :)

                      Oui, c'est vrai, mais il n'est pas forcement nécessaire de comprendre une abstraction pour s'en servir et l'utiliser. Prend l'exemple des parseurs type Parsec, je trouve la syntaxe plutôt simple… Pourtant derrière cela utilise des Monades plutôt trapues.

                      Parsec fait partie de mes librairies préférées en Haskell, et l'interface est simple, je suis d'accord. Je pensais plus à des choses comme ceci : si on veut faire un petit truc simple avec du xml, c'est vraiment une mauvaise idée (je crois que j'avais trouvé un petit tuto dans le haskell wiki et ça s'est sans doute étoffé depuis, mais la documentation officielle est juste impossible à appréhender rapidement, même en connaissant les Arrows probablement).

                      J'aurais eu une fonction non documenté mais avec un type sérieux, j'aurais été plus heureux ;)

                      Je suis d'accord qu'en l'absence de documentation, les types c'est mieux que rien :)

                    • [^] # Re: Moi == pas doué, je suppose....

                      Posté par  . Évalué à 2.

                      Par example, l'absence de type algébriques en C++, (i.e. des unions "safe")

                      Qu'entends-tu par union sécurisée? Une union avec de la "RTTI"?

                      Rust

                      Le prochain langage que j'apprendrais, ce sera sûrement Rust, il a pas mal de points intéressants à ce qu'il semble. (pas de GC, se base sur des techniques éprouvées, multi-paradigmes, pas d'héritage direct du C)

                      (Je fournis des examples si vous voulez).

                      Je suis preneur, à titre de curiosité.

                      Mais clairement le même problème existe dans de nombreuses librairies de la vraie vie en python ou en c++.

                      C'est clair. Malheureusement le C++ autorise les auteurs à faire de la merde, et j'ai lu assez de codes sources pour savoir que l'on ne s'en prive pas, avec un volontarisme variable.
                      je n'ouvrirai pas le débat sur les origines (en langage de prog) des gens faisant le plus de merdes en C++, mais mon opinion est… disons, contraire à celle Linus Torvalds.
                      Perso, je pense que quand on fait une fonction qui prend en paramètre un jeu de constantes, le plus simple est de définir une enum qui les définit, et si il s'avère qu'un utilisateur normal (du logiciel et non de la lib) peut impacter dessus via de la config, alors on file une fonction de conversion string<->enum.

                      • [^] # Re: Moi == pas doué, je suppose....

                        Posté par  . Évalué à 2.

                        Qu'entends-tu par union sécurisée? Une union avec de la "RTTI"?

                        Je pense qu'il veut dire une union taggée où tu n'as pas la possibilité de mal interpréter le tag en restant dans le subset safe du langage. Par exemple, en rust, le type option permet de représenter un résultat ou son absence, et le langage t'oblige à faire du pattern matching pour savoir quel cas tu as eu. C'est impossible à utiliser de manière incorrecte sans faire appel à unsafe.

                        Au passage ce type a une optimisation assez puissante lorsqu'il est instancié sur des pointeurs: dans ce cas, il ne rajoute pas de champ à l'objet pour tagger, il va tester directement si le pointeur est nul ou non. C'est aussi performant que de gérer le pointeur à la main, mais tout aussi sûr.

                      • [^] # Re: Moi == pas doué, je suppose....

                        Posté par  (site web personnel) . Évalué à 5.

                        Qu'entends-tu par union sécurisée? Une union avec de la "RTTI"?

                        Exemple :

                        data Object = Square Float | Rectangle Float Float

                        Ici, le type Object peut-être soit un carré Square avec un argument Float (le coté), soit un Rectangle avec deux arguments Float (les deux cotés). Pour faire la difference entre les différents objets, tu es forcé de "pattern match", examples :

                        area :: Object -> Float
                        area (Square c) = c * c
                        area (Rectangle c0 c1) = c0 * c1
                        
                        -- ou
                        
                        area' :: Object -> Float
                        area' object = case object of
                            (Square c) -> c * c
                            (Rectangle c0 c1) = c0 * c1

                        Ici tu ne peux pas te tromper, tu ne peux pas utiliser un carré à la place d'un rectangle.

                        Example plus avancé, si tu veux faire une fonction qui peut ne pas renvoyer la valeur, tu fais sois appel à une valeur "sentinelle" soit tu passe par un booléen en retour. Exemple avec une fonction find qui te renvoie la position dans un tableau d'un élément.

                        // Valeur sentinelle, c'est l'approche stl qui renvoie std::end()
                        // Si l’élément est trouvé, cela renvoi son index dans le vecteur
                        // Sinon, cela renvoie "-1"
                        template<typename T>
                        int find(const std::vector<T> &v, const T value);
                        
                        
                        // Approche booléenne
                        // Si l’élément est trouvé, cela renvoie true et met l'index dans `result`
                        // Sinon, cela renvoi false et la valeur de result est indéterminé
                        template<typename T>
                        bool find(const std::vector<T> &v, const T value, int &result);

                        Chaque approche a ses limitation. L'approche "sentinelle" fait que tu peux utiliser une valeur qui n'a aucun sens et l'approche Booléen fait que tu peux utiliser par erreur une valeur non initialisée.

                        En Haskell (et Rust, et OCaml, et sans doute d'autres), tu vas utiliser une "union typée safe" représentant le résultat où son absence :

                        data Maybe t = Nothing | Just t

                        Ici c'est un type polymorphique paramétré par t. Il peut donc contenir soit Nothing, soit Just une valeur de type t.

                        Maintenant la fonction find :

                        -- Cette fonction prend une liste de t `[t]`, une valeur
                        -- de type `t` à trouver et renvoie `Just offset` si il est trouvé, ou alors `Nothing`
                        find :: [t] -> t -> Maybe Int

                        Lors de l'utilisation, tu n'as pas de valeur "sentinelle" abstraite ni de valeur non initialisée et c'est garantie par la compilation :

                        case (find maListe maValeur) of
                           Just offset -> "La valeur est trouvée à l'offset: " ++ show (offset)
                           Nothing -> "Valeur non trouvée"

                        Ici la branche Nothing n'a pas le même scope que l'autre branche, ainsi tu ne peux pas utiliser offset dans la mauvaise branche.

                        Note que tu peux faire quelque chose de proche avec les exceptions dans de nombreux langage, mais ce n'est pas aussi propre car l'exception n’apparaît pas dans le type de la fonction et n'est pas forcement vérifiée par le compilateur.

                        Le prochain langage que j'apprendrais, ce sera sûrement Rust, il a pas mal de points intéressants à ce qu'il semble. (pas de GC, se base sur des techniques éprouvées, multi-paradigmes, pas d'héritage direct du C)

                        Rust est assez extra en effet. C'est pas aussi beau et pure que Haskell à mon sens, mais c'est un beau compromis.

                        (Je fournis des examples si vous voulez).

                        Je suis preneur, à titre de curiosité.

                        C'était les exemples de compositions. Continuons sur le Maybe que j'ai décris quelques lignes plus haut. On va supposer que nous avons une fonction getResult :: Int -> Maybe Int qui renvoie peut-être un Int. Tu veux renvoyer un Maybe contenant le résultat multiplié par deux ou Nothing si le résultat n'existe pas. Une implementation naive serait :

                        case (getResult v) of
                                Just res -> Just (res * 2)
                                Nothing -> Nothing

                        Cela devient vite lourd. Mais tu as pleins de fonction pour composer directement dans le Maybe, comme la fonction fmap ou <$> ce qui à la place donne de façon totalement équivalente :

                        (*2) <$> (getResult v)

                        Cela te permet de remonter au cran d'au dessus la gestion de l’échec. Malheureusement cela peut vite devenir un peu l'horreur si tu dois combiner plusieurs fonctions. Example, tu as 3 fonctions pour gérer tes utilisateurs qui associe leur Id à leur nom, prénom et leur salaire. Toutes ces fonctions peuvent échouer pour une raison ou une autre. Tu veux une fonction qui connaissant un Id te retourne une String présentant l'employé, ou Nothing si ce n'est pas possible :

                        getLastName :: Id -> Maybe String
                        getFirstName :: Id -> Maybe String
                        getSalary :: Id -> Maybe Int
                        
                        getGreetings :: Id -> Maybe String
                        getGreetings id = case getFirstName id of
                                            Nothing -> Nothing
                                            Just firstName -> case getLastName id of
                                                 Nothing -> Nothing
                                                 Just lastName -> case getSalary id of
                                                       Nothing -> Nothing
                                                       Just salary -> Just ("Bonjour, " ++ firstName ++ " " ++ lastName ++ ". Vous gagnez " ++ (show salary) ++ " peanuts / jours.")

                        C'est lourd. Il y a d'autres solutions, mais rien de bien sympathique, sauf la syntaxe do qui permet d'exprimer cela :

                        getGreetings :: Id -> Maybe String
                        getGreetings id = do
                            firstName <- getFirstName id
                            lastName <- getLastName id
                            salary <- getSalary id
                        
                            Just ("Bonjour, " ++ firstName ++ " " ++ lastName ++ ". Vous gagnez " ++ (show salary) ++ " peanuts / jours.")

                        Ici le <- signifie de sortir le résultat du Maybe où d’arrêter le calcul directement si celui-ci contient un Nothing. Cette syntaxe est polymorphique et dépend du type manipulé à condition que celui-ci correspond à une interface (la fameuse Monad). Comme cela tu peux faire une API très propre, tu pourrais imaginer une API de réseau :

                        getRessource host port login password ressourceId = do
                            ip <- resolveHost host
                            connection <- connect ip port
                            auth <- authenticate connection (login, password)
                            downloadRessource connection auth ressourceId

                        Sachant que chaque fonction peut échouer sur le chemin en renvoyant un type d'erreur, tu peux crée ta fonction de plus haut niveau getRessource qui te renverra soit l'erreur soit la ressource finale, sans devoir écrire la tuyauterie et l’enchaînement de cas pour gérer toutes les erreurs.

                        Perso, je pense que quand on fait une fonction qui prend en paramètre un jeu de constantes, le plus simple est de définir une enum qui les définit, et si il s'avère qu'un utilisateur normal (du logiciel et non de la lib) peut impacter dessus via de la config, alors on file une fonction de conversion string<->enum.

                        Oui ;) Et si on pouvait avoir des vrais enums typés (i.e: pas de faux int) ce serait mieux ;)

                        • [^] # Re: Moi == pas doué, je suppose....

                          Posté par  . Évalué à 2.

                          Note que tu peux faire quelque chose de proche avec les exceptions dans de nombreux langage, mais ce n'est pas aussi propre car l'exception n’apparaît pas dans le type de la fonction et n'est pas forcement vérifiée par le compilateur.

                          Accessoirement, les exceptions ont un coût à l'exécution nul, mais augmentent la taille du binaire, coûtent cher quand elles sont lancées, et, en C++, nécessitent tout un tas de glue, tout en ne pouvant pas transiter entre deux binaires correctement, sans parler du fait qu'une exception pas rattrapée semble être un comportement indéfini.
                          Bref, je n'aime plus les exceptions, que l'on a trop vendues comme la panacée à mon goût.

                          Au passage, cette histoire de taille de binaire augmenté pour de bonnes performances CPU, c'est un truc qui commence à me titiller en C++: tout est axé sur la performance de calcul, mais on n'a que peu de contrôle sur la performance mémoire je trouve (typiquement, estimer la taille d'un std::map est complexe, et nécessite une connaissance de l'implém sous-jacente). Sauf que le cache augmente moins vite que la vitesse de calcul, donc je ne suis pas sûr que l'approche "tout pour le CPU" soit idéale.

                          C'était les exemples de compositions.

                          Merci.

                          Oui ;) Et si on pouvait avoir des vrais enums typés (i.e: pas de faux int) ce serait mieux ;)

                          Un peu comme ça:

                          enum class Enumeration {
                              Val1,
                              Val2,
                              Val3 = 100,
                              Val4 // = 101
                          };

                          ?

                  • [^] # Re: Moi == pas doué, je suppose....

                    Posté par  . Évalué à 2.

                    je faisais référence par exemple au fait que les tableaux sont souvent passés par référence, et par exemple en Python dans l'exemple donné par Guillaum une copie b = a d'un tableau ne crée pas une copie du tableau, juste une copie de la référence au tableau

                    Oui, cet exemple est assez moche en effet. Mais c'est lié au langage uniquement: en C, la notion de tableaux n'existe pas (dans le meilleurs des cas on à des pointeurs vers des zones mémoire statique, me semble), donc c'est réglé, en C++ elle existe (array, vector, list, map, set…) et on aurais bien une copie. Je crois qu'en Java c'est la même, ça copie (mais ça va faire 5 ans que j'y ai pas touché, et je n'ai jamais utilisé longtemps ce langage).

                    Pour le coup, ça me rappelle cette vidéo qui m'avais bien fait rire.

                    si l'interface est pas claire, tu risque de passer un pire moment en Haskell (pas forcément non plus)

                    Tu devrais lire le code de la libstdc++ ou de boost::program_options. Tu verras que dans le genre mauvais moment à passer quand il y a une erreur de template, c'est pas mal (d'ailleurs, entre l'illisibilité de boost::po et sa lourdeur, j'ai finit par coder ma propre lib, plus primitive mais bien suffisante pour la tâche, faudra que je partage ça).

                    j'ai l'impression que plus un langage propose des moyens de documenter automatiquement et formellement (signatures des fonctions, etc.), moins il y a d'exemples dans la doc en moyenne

                    C'est probablement pas faux, je documente très peu mon code en général, particulièrement quand j'écris un programme (et non une lib). Par contre, je prête beaucoup attention aux signatures de mes fonctions.
                    Par contre, j'ai arrêté avec les commentaires doxygen, je trouve que le gain est franchement limité: la doc qui en résulte est pourrie au final, et ça pourrit plus le code qu'autre chose.

                    • [^] # Re: Moi == pas doué, je suppose....

                      Posté par  . Évalué à 4.

                      en C, la notion de tableaux n'existe pas (dans le meilleurs des cas on à des pointeurs vers des zones mémoire statique, me semble)

                      Ouai donc tu as le même comportement que celui en python. Le fait que ce soit un pointeur plutôt qu'une référence ne change rien.

                      en C++ elle existe (array, vector, list, map, set…)

                      Oui reste à savoir quel profondeur de copie est fait.

                      Je crois qu'en Java c'est la même, ça copie (mais ça va faire 5 ans que j'y ai pas touché, et je n'ai jamais utilisé longtemps ce langage).

                      En java tout est référence mis à part les types primitifs.

                      Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

                      • [^] # Re: Moi == pas doué, je suppose....

                        Posté par  . Évalué à 2.

                        Oui reste à savoir quel profondeur de copie est fait.

                        Si je ne m'abuse, c'est une copie totale des objets stockés. Sauf que si ton objet stocké est un pointeur, seul le pointeur est copié, pas le contenu pointé par. En même temps, c'est le rôle des pointeurs, on pourrait dire.

                        • [^] # Re: Moi == pas doué, je suppose....

                          Posté par  . Évalué à 3.

                          La question n'est pas de savoir si c'est bien ou pas. Il faut juste savoir ce que ça fait.

                          Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

            • [^] # Re: Moi == pas doué, je suppose....

              Posté par  (site web personnel) . Évalué à 4.

              Blague à part, je n'ai jamais fais de OCaml car en voyant que tous les étudiants de prépas "prestigieuses" en faisait, j'ai eu l'impression que c'était un langage théorique pour matheux, surtout sachant que c'était poussé par l'INRIA. J'ai donc choisis Haskell qui me semblait moins élitiste du fait de mon historique ;)

              C'est assez amusant, puisque la réalité semble être l'exact opposé: OCaml est un langage dont l'adoption dans l'industrie progresse énormément dans les dernières années (on peut citer le rachat récent de la startup derrière Mirage OS par docker), tandis que Haskell semble avoir du mal à percer et semble rester un langage essentiellement expérimental.

              Un de mes amis “haskelleur” me confie que ce qu'il déteste le plus dans ce langage est l'imprévisibilité des performances – des modifications apparemment anodines peuvent changer la classe de complexité d'un programme. À côté, OCaml est un peu le C des langages fonctionnels, la simplicité de son modèle d'évaluation facilite la “mise sur métier” des travaux d'optimisation.

              Ceci dit certaines banques (comme HSBC) utilisent Haskell pour faire de la gestion de risque financier – une activité opérationnelle critique pour la banque – mais c'est aussi un domaine où OCaml est bien représenté.

              • [^] # Re: Moi == pas doué, je suppose....

                Posté par  . Évalué à 3. Dernière modification le 23 février 2016 à 10:51.

                Ocaml est aussi utilisé par facebook pour créer hack.

                Ce qui m'a toujours « choqué » avec ce langage c'est qu'il est très performant à ce qu'il paraît alors qu'il y a pleins d'optimisations qui ne sont pas faites par le compilateur de ce qu'on m'a dit.

                Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

                • [^] # Re: Moi == pas doué, je suppose....

                  Posté par  (site web personnel) . Évalué à 2.

                  De ce que j'ai cru comprendre, les optimisations d'OCaml sont assez bien choisies (bon compromis résultat/complexité de mise en œuvre), et le modèle d'évaluation étant assez simple comme a dit Michaël, ça donne de bons résultats sans besoin de faire particulièrement attention (contrairement à Haskell). Et en ce moment il y a de nouvelles passes d'optimisations qui sont faites à l'aide d'un nouveau langage intermédiaire dans le compilateur (de l'inlining surtout, je crois), donc le coût d'utiliser certaines abstractions devrait diminuer encore.

                  J'ai l'impression que des choses similaires doivent pouvoir être dites de go (qui me semble donner des performances très correctes avec des optimisations pas très poussées, même si ils sont en cours d'ajouter des optims en plus en utilisant un langage intermédiaire plus adapté à des optimisations plus poussées).

                  Après, pour passer de « très bonnes performances » à « performances à la GCC », j'ai l'impression qu'en pratique ça passe inévitablement par beaucoup de complications et chaque petit pourcentage de plus demande beaucoup d'énergie.

                  • [^] # Re: Moi == pas doué, je suppose....

                    Posté par  (site web personnel) . Évalué à 3.

                    Après, pour passer de « très bonnes performances » à « performances à la GCC », j'ai l'impression qu'en pratique ça passe inévitablement par beaucoup de complications et chaque petit pourcentage de plus demande beaucoup d'énergie.

                    Sans compter que la mesure des performances est un casse-tête sans fin. Emery Berger a développé un outillage très intéressant pour éliminer de nombreux biais connus (notamment la position des allocations mémoire) pour l'évaluation des performances. Grâce à cela il prétend démontrer que -O3 n'apporte pas d'amélioration par rapport à -O2.

                    Pour revenir à OCaml, j'ai commencé à travailler avec js_of_ocaml récemment, qui permet de compiler OCaml vers node/V8. Cela permet d'écrire facilement des bindings (par exemple pour le AWS Toolkit Javascript) et un aspect intéressant est que les programmes OCaml compilés en Javascript sont généralement plus performants que les programmes OCaml bytecode correspondants – mais pas les natifs. La raison étant simplement que beaucoup plus de travail a été fait dans V8 que dans l'interpréteur bytecode classique.

    • [^] # Re: Moi == pas doué, je suppose....

      Posté par  . Évalué à 4.

      Mon ressenti à la lecture du journal est complètement opposé. D'habitude, quand je lis un journal ou une dépêche traitant d'Haskell, la seule chose que je comprends, c'est que je n'ai rien compris. Ici, au contraire, en suivant les différentes réécritures d'un code simple on se familiarise avec la manière de penser dans ce langage et l'on comprend sa syntaxe particulière. Je conçois parfaitement que la dernière version du code soit considérée comme plus concise et facile à lire (pour qui connaît le langage) alors que la version Python aurait besoin d'une ligne de commentaire.

    • [^] # Re: Moi == pas doué, je suppose....

      Posté par  (site web personnel) . Évalué à 7.

      Je te remercie pour ta critique et je vais me permettre d'y répondre.

      Prof, c'est manifestement ni mon métier (sachant que, pour le moment, j'ai pas de taf, si ça veut dire quelque chose), ni le tien.

      Et merde… 30% de mon salaire vient de mes enseignements à l'université… Mais bon, comme on dit, "Ceux qui savent font, les autres enseignent".

      Je ne dis pas ça pour t'empêcher de faire de nouveaux journaux, au contraire… et puis merde, je vais balancer le reste sans politiquement correct, et faire confiance à ton sens critique, puisqu'après tout, tu postes sur linuxfr.

      Pas de souci ;)

      Avant de continuer je voulais mettre un truc au point. Ce n'est pas un cours d'introduction à Haskell, pour cela il y en a des dizaines écrits par des gens plus brillants que moi et avec de beaux dessins comme LHAH.

      La première chose qu'il me semble importante de noter, c'est l'aspect très… matheux, du langage. C'est con à dire, mais, moi, j'ai jamais été un matheux. Ouai, ok, j'ai un bac sti "génie" électronique, mais je me tapais des 8 en maths, quand, en physique je tapais du 13+ et du 15+ en élec.

      Il n'y a rien de matheux dans mon article, peut-être la notion de relation d'ordre et les monoids, mais j'ai juste donné un nom à un concept connu (l'addition) et montré que ce concept est plus général et que Haskell propose une façon standard de le manipuler. Ca ne choque personne que le + soit utilisé en python ou en C++ pour concaténer des chaines de caractères, mais si je t'explique que c'est parce que on peut voir une similarité entre le + des chaînes de caractère et celui des entiers, que cette similarité se nomme Monoid et que les développeurs d'Haskell ont choisis l'opérateur <> pour la représenter et ainsi éviter la confusion avec le +.

      Soyons sérieux, un instant. Ta fonction, elle prend [a] en argument. Soit. dans ton exemple de déclaration, je vois le [a] 2 fois. Quelle occurrence est donc le paramètre, histoire que l'on puisse au moins deviner quelle autre occurrence est le retour?

      Je l'admet, je ne prend personne par la main dans ce passage et la clarification qui est quelques paragraphes plus bas arrive trop tard, c'est clairement une erreur de pédagogie de ma part. a -> b, a est le paramètre, b est la valeur de retour, la flèche indiquant que a donne b.

      (Remarque sur l'orthographe, rien à dire, j'ai laissé quelques coquilles. Moi c'était plutôt du genre 15+ en math et 2 en français et en physique, chacun son handicap ;)

      Le coup du ordonnable (2n, au passage, me dit mon correcteur), on le remarque comment?

      Grâce à la contrainte Ord a en début de ligne. Je n'ai pas voulu faire un cours de syntaxe Haskell très précis et j'ai supposé que ma phrase "On remarque que a est Ordonable" laisserait comprendre la relation entre Ord et Ordonable…

      Encore une fois, tu me rejette dans mon ignorance crasse, et ça me semble mauvais en terme de pédagogie.

      Oui, toutes mes excuses, tu n'imagines pas le temps que j'ai passé à rajouter des paragraphs pour rendre mon histoire plus clair. Au début je voulais juste hurler "Énorme, (->) est une instance de Monoid et cela permet de faire du tri de façon trop super"…

      Ensuite, une "liste simplement chaînées" (j'ai reproduit le 's' après quelques secondes d'hésitations, parce que ça me fait chier de jouer les nazi du french) et non modifiable. Le haskell semble séduire quelques utilisateur du C++, mais je n'ai jamais compris (ni cherché) pourquoi? Ok, une liste simple, ça veut dire 1 pointeur de moins par élément, soit 8 bytes (ou octets en français, mais le terme est moins précis, niveau sémantique, un byte n'étant pas forcément constitués de 8 bits) et dans le cas de haskell c'est du const. Bien. Pourquoi pas. Mais dans ce cas, quel est l'impact en terme de perfs lorsqu'une fonction à pour simple but de construire une liste chaînée? Doit-elle être copiée-collée? J'espère bien que non.

      Toi tu fais partie de ces étudiants qui n'aime pas comprendre l'ensemble et qui s'attachent aux points de détails. C'est intéressant ;) Ton "pourquoi" est vague donc je vais essayer de répondre à de nombreuses questions possibles :

      • Pourquoi non mutable ? C'est subjectif. Parce que on aime bien cela en fonctionnel et surtout en Haskell. Cela permet de ne pas se demander qui modifie quoi. Cela simplifie le parallélisme (personne ne va venir modifier ta donnée sans te prévenir) donc on préfère commencer non mutable et introduire de la mutabilité contrôlée quand c'est nécessaire. Cela rend aussi les tests plus simple (pas d'état caché) et le raisonnement sur le code plus simple, car il n'y a pas de modification d'état caché possible.
      • Doit elle être copiée collée en permanence ? Non, de temps en temps plus qu'en C++, de temps en temps moins. Exemple, soit une liste chainée list0 qui contient A, B et C, les pointeurs étant representés par des -> :
      list0 -> A -> B -> C -> FIN
      

      Si tu veux ajouter Z en tête de liste pour créer la liste list1, tu peux crée une nouvelle liste et faire pointer Z vers le A d'avant, tu auras :

      list1 -> Z -> list0
      

      Mais pas de copie, puisque Z pointe vers l'ancienne liste en mémoire. Il y a un partage de A B et C entre les deux listes. Si tu veux maintenant supprimer un élément de list0, tu vas faire pointer ta nouvelle liste vers le B.

      • Pourquoi pas doublement chaînée ? Parce que ce n'est pas possible en non mutable, car tu devrais modifier le nœud d'après pour lui faire pointer sur le nœud d'avant, donc en fait tu devrais crée un nouveau nœud, qui invaliderait le pointeur du nœud encore avant, et ainsi de suite…
      • Performance. C'est LENT, comme le diable, mais tout autant que les listes chaînées en C++ ou en python. C'est pour cela que on a aussi des vecteurs et d'autres structures et que le compilateur est généralement pas trop bête, quand tu "crée" un nouveau vecteur en changeant juste une case, il peut utiliser l'ancien si celui-ci n'est plus nécessaire. La plupart des mes algos en Haskell sont en moyenne 2x plus lent que du c++ optimisés, ce qui est intolerable dans mon domaine d'activité, mais est carrément supportable autre part ;)
      • Performance. Haskell est paresseux et les listes sont rarement utilisées comme conteneur, mais plutôt comme générateur de valeur. Souvent cela permet d'exprimer des algorithmes complexes simplement tout en ayant des performances très bonnes.
      • En résumé, c'est un paradigme. Certains algorithmes s'expriment super bien en non mutable, d'autres moins. Certains ne sont pas possibles dans l'un ou l'autre paradigme. A titre de comparaison, à l'université, j'ai fais deux groupe dans une UE, la première semaine, j'ai fais à un groupe des arbres binaires mutables et à l'autre groupe des arbres binaires non mutable. Et bien le groupe mutable galérait encore avec les pointeurs mutée pour faire une simple insertion tandis que le groupe non mutable n'avait eu aucun problème. Cela dépend des fois.

      Bref, ton journal est bien trop… hum… c'est cru, mais mon sentiment, alors, voilà: imbu de ta connaissance.

      Le drame de ma vie, je suis un pauvre type dépressif sans aucune confiance en moi et tout le monde me reproche d'être trop confiant. Désolé si je t'ai donné ce sentiment.

      Le C, à imposé sa syntaxe comme un quasi-standard. Le pascal aussi. Les paradigmes de ces langages sont peut-être moins efficaces (ref. souhaitée) mais plus adaptés à la façon de penser des gens normaux (ref souhaitée aussi), c'est à dire pas théoriciens des langages ni matheux (ref souhaitée encore, notez comme je m'auto-troll).

      Excuses moi de te dire cela, mais tu es biaisé par ton bagage et tu penses que la façon de penser des gens normaux est la tienne. Mais les gens normaux qui n'ont jamais programmé, je peux t'assurer qu'ils ne pensent ni en C ni en Haskell. On peut troller autant que on veut, mais en haskell l'ordre sur la signature d'une fonction vas de la gauche vers la droite, en premier les arguments, à la fin le résultat. En C c'est le résultat en premier, et après les arguments, sauf en C++ où on peut aussi mettre le résultat à la fin. Et une convention fait que on trouve des résultats en argument au milieu… Je rentre dans le troll, mais je n'ai jamais trouvé l'ordre des arguments en C++, python, java, … "Naturel". Et pourtant j'ai utilisé ces langages avant Haskell et la plupart sont plus jeunes que Haskell. Essaye de te rappeler tes débuts en programmation et ce sur quoi tu bloquais et tu verras que rien n'est naturel ;) Mes bêtes noires à moi c'était la post et pré incrémentation (a++ ou ++a), les constructeurs en C++ et les boucles…

      Une question simple, pour clore ce déjà trop long post: pourquoi ne pas nous montrer le traditionnel hello world, en C, puis en C++ (2 langages différents pour moi, bien que très proches), puis en JS (à la mode), python (idem), et enfin haskell?
      Même si le hello world ne montre quasiment rien du langage, il permets au moins aux devs de savoir comment écrire un programme qui ne fait rien. C'est, l'ai de rien, très important de savoir faire un truc qui ne fait rien avec le minimum de code. En fait, c'est la base pour savoir écrire un programme qui fait uniquement ce qu'on lui demande.

      Mais je ne faisais pas un cours d'Haskell… Je me suis embêté à comparer 2 langages (python et Haskell) et j'aurais du en comparer plus en refaisant une introduction complète sur la syntaxe et les subtilités de chaque langage…

      Mais si tu y tient, voila l'implementation comparée de mon histoire de tri en Haskell et en C++… Je sais lequel est le plus naturel pour moi ;)

      #include <iostream>
      #include <algorithm>
      #include <string>
      #include <vector>
      
      // Mon type
      struct Etudiant
      {
          std::string nom;
          float note;
      };
      
      // Et sa fonction d'affichage
      std::ostream& operator<<(std::ostream &stream, const Etudiant &etu)
      {
          stream << "Etudiant{" << etu.nom << ", " << etu.note << "}";
          return stream;
      }
      
      
      int main()
      {
          std::cout << "Hello World" << std::endl;
      
              // Création de la liste, j'ai utilisé un vecteur ici contre une liste en Haskell, c'est débatable.
          std::vector<Etudiant> etudiants{{"Batman", 20}, {"Hulk", 12}, {"Chuck Norris", 12}};
      
              // Tri suivant les notes descendant et les noms ascendants
          std::sort(std::begin(etudiants), std::end(etudiants), [](const Etudiant &etu0, const Etudiant &etu1)
                    {
                        if(etu0.note == etu1.note)
                        {
                            // second critère de tri: le nom, ascendant
                            return etu0.nom < etu1.nom;
                        }
                        else
                        {
                            // premier critère de tri: la note, descendant
                            // ATTENTION, c'est normal d'avoir etu1 < etu0
                            return etu1.note < etu0.note;
                        }
                    });
      
              // affichage de la liste triée
          std::cout << "{";
          for(auto &&item : etudiants)
          {
              std::cout << item;
              std::cout << ", ";
          }
          std::cout << "}";
          std::cout << std::endl;
      
          return 0;
      }
      import Data.List (sortBy)
      import Data.Monoid ((<>))
      import Data.Ord (comparing)
      
      -- Fonctions utilitaires pour rendre le code plus lisible
      ascending = comparing
      descending = flip . comparing
      
      -- Mon type étudiant et sa fonction d'affichage automatiquement derivée
      data Etudiant = Etudiant {nom :: String, note :: Float} deriving (Show)
      
      -- Fonction main
      -- petite subtilité du haskell, il y a une distinction entre les données pures dans le let
      -- et les effets de bords dans le do. Ici je mens et je cache pleins de choses, mais bon ;)
      main = let
               -- ma liste d'étudiant
               etudiants = [Etudiant "Batman" 20, Etudiant "Hulk" 12, Etudiant "Chuck Norris" 12]
               -- Que je tri par ordre descendant de note et ascendant de noms...
               -- Ce commentaire est il inutile ?
               sortedEtudiants = sortBy (descending note <> ascending nom) etudiants
             in do
                -- Affichage
                putStrLn "Hello World"
      
                -- Il faut appeler show pour convertir en String un étudiant
                putStrLn (show sortedEtudiants)

      PS: je n'ai lu que le court passage que j'ai cité. Ça m'a déjà semblé trop inaccessible, trop chiant. Cool, on est dredi.

      Merci d'avoir pris le temps de donner ton avis sur le début. J'espere que mes réponses t’intéresserons.

      • [^] # Re: Moi == pas doué, je suppose....

        Posté par  . Évalué à 2.

        Et merde… 30% de mon salaire vient de mes enseignements à l'université… Mais bon, comme on dit, "Ceux qui savent font, les autres enseignent".

        D'un autre côté j'ai été très crû… sûrement trop, p'tet pas la bonne journée :)

        Avant de continuer je voulais mettre un truc au point. Ce n'est pas un cours d'introduction à Haskell,

        Ce n'est pas un cours, je l'avais bien compris, mais cette phrase me faisait penser que ça serait moins difficile à lire:

        L'idée c'est de vous présenter un peu Haskell et sa syntaxe à partir d'un exemple simple à partir d'une fonction simple, le tri.

        Ca ne choque personne que le + soit utilisé en python ou en C++ pour concaténer des chaines de caractère

        Ce n'est pas exact:

        Another, more subtle, issue with operators is that certain rules from mathematics can be wrongly expected or unintentionally assumed. For example, the commutativity of + (i.e. that a + b == b + a) does not always apply; an example of this occurs when the operands are strings, since + is commonly overloaded to perform a concatenation of strings (i.e. "school" + "bag" yields "schoolbag", while "bag" + "school" yields "bagschool"). A typical counter[citation needed] to this argument comes directly from mathematics: While + is commutative on integers (and more generally any complex numbers), it is not commutative for other "types" of variable.

        Tout n'est pas rose dans la surcharge d'opérateurs.

        Moi c'était plutôt du genre 15+ en math et 2 en français et en physique, chacun son handicap ;)

        Ça peut expliquer les affinités avec les paradigmes je suppose :)

        Oui, toutes mes excuses, tu n'imagines pas le temps que j'ai passé à rajouter des paragraphs pour rendre mon histoire plus clair.

        Ce qui est sûr, c'est que faire simple à toujours été la chose la plus complexe en ingéniérie. Surtout faire simple pour les autres.

        Toi tu fais partie de ces étudiants qui n'aime pas comprendre l'ensemble et qui s'attachent aux points de détails. C'est intéressant ;) Ton "pourquoi" est vague donc je vais essayer de répondre à de nombreuses questions possibles :

        Pourquoi non mutable ? C'est subjectif. Parce que on aime bien cela en fonctionnel et surtout en Haskell. Cela permet de ne pas se demander qui modifie quoi. Cela simplifie le parallélisme (personne ne va venir modifier ta donnée sans te prévenir) donc on préfère commencer non mutable et introduire de la mutabilité contrôlée quand c'est nécessaire. Cela rend aussi les tests plus simple (pas d'état caché) et le raisonnement sur le code plus simple, car il n'y a pas de modification d'état caché possible.

        Pour être honnête, je suis un fanatique des paramètres et méthodes const, en C++. Et pas juste pour le parallélisme, de manière générale ça rend le code plus adaptable. Et ça permets au compilo de vérifier derrière moi.

        La plupart des mes algos en Haskell sont en moyenne 2x plus lent que du c++ optimisés

        Tout dépend de que tu appelles du C++ optimisé, après. Le C++, si on inclue la STL, à un gros défaut niveau perf: on n'a que peu de contrôle sur la consommation mémoire (oui, on connais toutes les complexités des algo, mais il est difficile d'estimer la RAM que ça va consommer, ce qui impacte potentiellement lourdement sur les caches L1/L2/L3.).

        Essaye de te rappeler tes débuts en programmation et ce sur quoi tu bloquais et tu verras que rien n'est naturel ;) Mes bêtes noires à moi c'était la post et pré incrémentation (a++ ou ++a), les constructeurs en C++ et les boucles…

        Mes 1ers langages de programmation, dans l'ordre:

        • Basic de TI82 (1ère 2nde générale)
        • QBasic (1ère 2nde générale)
        • ASM (2ème 2nde)
        • C (2ème 2nde)
        • C++ (BTS)

        Le QBasic avait des fonctions mais je ne m'en suis pas servi dès le début, et quant à l'objet, j'ai lutté à comprendre comment ça marche, jusqu'à tomber sur un type qui montrait comment implémenter de l'objet en C… méthodes virtuelles & co.
        Le truc, je pense, c'est qu'il faudrait arrêter de faire commencer les gens avec du C ou du Java, qui ne sont pas faits pour l'enseignement. Enfin, c'est mon opinion personnelle. Ce qui à aussi du beaucoup m'aider, c'est que je n'ai pas appris en cours (sauf pour mes 50 premières lignes de QB, un prof qui m'a filé le virus :D et le pire c'est que je l'aimais pas, mais bon), avec un rythme ou des exercices imposés. Sauf pour le C++, mais bon, une fois le déclic objet fait, ça à été tout seul.

        Mais si tu y tient, voila l'implementation comparée de mon histoire de tri en Haskell et en C++… Je sais lequel est le plus naturel pour moi ;)

        Juste deux petits points:

        • si tu veux un truc expressif, plutôt que performant, tu aurais pu utiliser un std::set: std::set<Etudiant,bool(*)(Etudiant const&,Etudiant const&)> etudiants({{"foo",20},{"bar",10}}, [](Etudiant const& e1, Etudiant const& e2){...;}. Bon, forcément, si on veut un autre tri à un autre moment, ça n'est peut-être pas le bon choix :) [1]
        • l'opérateur ternaire (?:) rend le code plus lisible parfois (mais ne pas en abuser): etu0.note == etu1.note ? etu0.nom < etu0.nom : etu0.note < etu1.note. Mais la notation Haskell est effectivement belle sur ce coup (doit être codable avec un peu de variadic templates je pense… faudrait tester tiens.).
        • for(auto &&item : etudiants), tu aurais pu écrire for(auto const& item : etudiants). C'est ce que j'aurai fait en fait. Ça aurait permis de pouvoir réutiliser la liste après les yeux fermés, parce que là j'ai un doute sur l'état des éléments de etudiants après coup.

        J'espere que mes réponses t’intéresserons.

        Beaucoup.
        Quant aux '<<', c'est sûrement mon passé de C, mais je trouve ça immonde. J'aime pas les iostreams… un bon vieux printf c'est tellement plus lisible. En tout cas, tu aurais pu mettre les 2 'std::cout' sur la même ligne.

        1:
        Et la taille de cette ligne, c'est une des raisons qui font que quand j'utilise un conteneur STL, j'utilise TOUJOURS un typedef.

        • [^] # Re: Moi == pas doué, je suppose....

          Posté par  (site web personnel) . Évalué à 1. Dernière modification le 20 février 2016 à 11:31.

          Quant aux '<<', c'est sûrement mon passé de C, mais je trouve ça immonde. J'aime pas les iostreams… un bon vieux printf c'est tellement plus lisible.

          Ca dépend du contexte, la notation "<<" est pourtant très expressive dans le sens "j'envoie quelque chose dans mon flux", mais ici, on a besoin de formater une chaîne avec des paramètres dynamiques et effectivement, une notation à la printf est toute indiquée, surtout que la suite logique en industrialisation est la traduction de la chaîne, et pour traduire le résultat d'un ensemble d'envois dans le flux, bon courage :)

        • [^] # Re: Moi == pas doué, je suppose....

          Posté par  (site web personnel) . Évalué à 4.

          Tout n'est pas rose dans la surcharge d'opérateurs.

          C'est pour cela que certains langages (Comme Haskell) interdisent la surcharge d’opérateurs et propose, si tu penses que certains types méritent de partager une interface commune, de définir une interface et des lois pour les types qui suivent cette interface. Haskell ne garantie pas que les lois soient respectés, mais la plupart des auteurs de librairies s'y collent.

          for(auto &&item : etudiants), tu aurais pu écrire for(auto const& item : etudiants). C'est ce que j'aurai fait en fait. Ça aurait permis de pouvoir réutiliser la liste après les yeux fermés, parce que là j'ai un doute sur l'état des éléments de etudiants après coup.

          Oui, erreur de ma part, même si je fais beaucoup d'effort pour ne pas oublier de const, j'en oublie toujours. Preuve que cela devrait être pensé dans l'autre sens ;)

          • [^] # Re: Moi == pas doué, je suppose....

            Posté par  . Évalué à 2.

            et propose […] de définir une interface et des lois pour les types qui suivent cette interface [commune aux types].

            Je suppose donc que les unsigned int (par exemple) disposent de ce type d'interface?

            Preuve que cela devrait être pensé dans l'autre sens ;)

            Je suis d'accord avec ça, tant qu'on garde un moyen simple (et concis) de rendre les données mutables (j'ai un peu trollé sur l'immutabilité, mais dans la pratique mon code est constellé de const, j'aime me faire engueuler par le compilo alors je fais des interfaces qui le font râler le plus possible quand mal utilisées).
            Mais dans le cas que j'ai cité, ça marche juste pas en fait (ça me paraissait bizarre honnêtement, de déclarer une variable en tant que rvalue) ce qui résous mon interrogation. Après une compilation tu aurais sûrement mis un const&.

    • [^] # Re: Moi == pas doué, je suppose....

      Posté par  . Évalué à 5.

      Salut,

      J’arrive après la bataille (et mes vacances…).

      Pour répondre à :

      Le C, à imposé sa syntaxe comme un quasi-standard. Le pascal aussi. Les paradigmes de ces langages sont peut-être moins efficaces (ref. souhaitée) mais plus adaptés à la façon de penser des gens normaux (ref souhaitée aussi), c'est à dire pas théoriciens des langages ni matheux (ref souhaitée encore, notez comme je m'auto-troll).

      Je vois les choses comme ça :

      • langage impératif : tu décris pas à pas la méthode de résolution du problème. Si tu ne sais pas résoudre le problème à la main, tu n’arriveras pas à écrire ton algorithme.
      • langage fonctionnel : tu décris ton système. Une fois le système écris, tu le sollicites avec des données, et tu vois ce qu’il en ressort.

      Pour moi les deux approches sont viable, et complémentaire. Dans certains cas l’une sera plus simple, dans un autre cas ce sera l’inverse.

      Je trouve l’exemple de la généalogie en prolog vraiment parlant. On définit trois propriétés aux données : homme, femme et parentDe. À partir de là, on donne des règles permettant de définir le père, la mère, l’oncle… Ensuite il suffit de demander qui sont les oncles de Anthony. Et il te donne toutes les réponses possibles.

  • # elm

    Posté par  (site web personnel) . Évalué à 10.

    Je profite lâchement de ce journal que j'ai trouvé très didactique, pour causer brièvement d'un langage fortement inspiré d'une sous-partie d'Haskell, appelé Elm et qui a en ce moment le vent en poupe.

    Pour ceux que Haskell effraie, ce langage présente des aspects relativement basique de la FP, il n'utilise pas de termes comme "monoid" ou "monade" ou "functor", mais pourtant permet d'en utiliser sans s'en rendre compte, et je pense que c'est un bon tremplin pour entrer dans le monde de la programmation fonctionnelle.

    Il est particulièrement gratifiant car il permet très facilement de créer des applications web, en partant d'un simple Hello World ou bien d'une page qui affiche la position de la souris, jusqu'à ce genre de chose.
    Bon, je mentirais en disant qu'il offre les mêmes capacités que tout l'écosystème web autour de React ou Angular, mais il embarque tout de même des choses comme un DOM virtuel à la ReactJs, un système de signaux extrêmement pratique permettant de faire (attention, buzzword en approche) de la FRP (Functional Reactive Programming), c'est-à-dire que tout ce qui peut varier dans le temps (l'état du clavier, la souris, le temps, la taille de la fenêtre, etc) est manipulable sous forme de signaux (un signal est une valeur qui change dans le temps) sur lesquels on applique ensuite du filtrage, de la fusion, etc.

    Redux, la petite lib qui est un flux "2.0" qui devient incontournable dans l'écosystème ReactJs est d'ailleurs (et officiellement) fortement inspiré de Elm.

    Je peux vous garantir qu'une fois qu'on a bien pris Elm en main, c'est rude de revenir à Javascript.

    Quelques liens en vrac :

    Le bac à sable Elm pour tester le langage sans rien installer

    Un tutoriel progressif sur la création du jeu de tétris en Elm

    Le site web est une très bonne porte d'entrée également, il fourmille d'exemple permet même au programmeur Javascript de s'acclimater en douceur à ce nouveau langage.

  • # Perl

    Posté par  . Évalué à 4. Dernière modification le 25 février 2016 à 10:56.

    >>> l = [("Bruce Lee", 50), ("Batman", 100), ("Hulk", 200), ("La montagne", 100)]
    
    >>> def compare(t0, t1):
    ...     c = cmp(t1[1], t0[1])
    ...     if c == 0:
    ...         return cmp(t0[0], t1[0])
    ...     else:
    ...         return c
    ... 
    >>> sorted(l, cmp=compare)
    [('Hulk', 200), ('Batman', 100), ('La montagne', 100), ('Bruce Lee', 50)]
    

    L’équivalent en Perl :

    use Data::Dump;
    
    my @liste = (["Bruce Lee", 50], ["Batman", 100], ["Hulk", 200], ["La montagne", 100]);
    
    my @triee = sort { $b->[1] <=> $a->[1]
                    || $a->[0] cmp $b->[0] } @liste;
    
    dd(@triee);

    et le résultat, affiché par dd :

    (
      ["Hulk", 200],
      ["Batman", 100],
      ["La montagne", 100],
      ["Bruce Lee", 50],
    )

    sort accepte en argument un bloc de code effectuant la comparaison, $a et $b correspondant aux éléments à comparer.

    <=> et cmp sont les opérateurs de comparaison, respectivement numérique et lexicale ; ils rendent -1, 0 ou 1 selon le résultat de la comparaison.

    Du point de vue des opérations booléennes, 0 est considéré comme faux et les autres nombres comme vrai. Par ailleurs, les opérateurs booléens (sauf !, not) rendent la valeur d’origine (et n’évaluent que leur premier argument s’il suffit à déterminer que le résultat est vrai ou faux).

    Par conséquent || (ou) rend la valeur de la première comparaison si elle est non nulle et de la seconde sinon.

    Grâce à cela, faire un tri personnalisé est simple ; ça respecte le principe de Larry Wall : « Easy things should be easy and hard things should be possible ».

    « Le fascisme c’est la gangrène, à Santiago comme à Paris. » — Renaud, Hexagone

Suivre le flux des commentaires

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