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 Ord
onable, 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 CrEv (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 Guillaum (site web personnel) . Évalué à 2.
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 Perthmâd (site web personnel) . Évalué à 1.
Et les monades sont aussi des monoïdes. Mouahahaha !
[^] # Re: merci
Posté par Guillaum (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 Perthmâd (site web personnel) . Évalué à 1.
Ç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 Guillaum (site web personnel) . Évalué à 2.
(->) r
est une instance deMonad
, 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 freem . Évalué à 5.
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 Guillaum (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 :
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 sousascending
etdescending
), 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<>
enandThenSortBy
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 freem . Évalué à 4.
Chacun son truc :)
Je confirme qu'en C++ cette ligne-ci sera moins claire:
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 Guillaum (site web personnel) . Évalué à 3.
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++ :
C'est en discussion pour c++17 et j’espère que cela aboutira.
[^] # Re: merci
Posté par freem . Évalué à 2.
Mais non, c'est juste que c'est la meilleure façon pour que je sois en haut du classement en maths :D
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.)
Ç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 Guillaum (site web personnel) . Évalué à 2.
Non, j'ai inversé l'ordre des champs ;)
[^] # Re: merci
Posté par freem . É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 barmic . Évalué à 3.
Avec l'indentation qui va bien pour les autres lecteurs :
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 barmic . Évalué à 3.
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 freem . É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:
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 barmic . Évalué à 3.
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 Dr BG . Évalué à 6. Dernière modification le 19 février 2016 à 10:38.
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 freem . Évalué à 2.
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 Dr BG . É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 freem . Évalué à 1.
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?
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:
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.
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;}
.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 Dr BG . Évalué à 3.
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.
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.
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 freem . Évalué à 1.
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.
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 Dreammm . É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 Jiehong (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 Guillaum (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 ;)
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.
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 deobj.begin()
ou l'ensemble<algorythms>
, où le discours dernier sur le fait quef.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 moinsPoint
,Direction
,Normal / DirectionNormalisée
,Couleur
avec un sous ensemble de méthodes limitées aux besoins du domaine. (Par exemple, pas d'addition entrePoint
, pas de produit scalaire entreCouleur
…). 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 reno . É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 reno . É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 Kalenx . Évalué à 3.
Très intéressant comme journal, mais, pointilleux comme je le suis, je ne peux m'empêcher de faire remarquer que :
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 Guillaum (site web personnel) . Évalué à 3.
Merci, je ne connaissais pas
cmp_to_key
.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.
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 enO(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 enO(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, typenumpy
.[^] # Re: En ce qui concerne Python
Posté par Kalenx . Évalué à 1.
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é.
[^] # Re: En ce qui concerne Python
Posté par Guillaum (site web personnel) . Évalué à 2.
Au final je suis d'accord avec toi, je crois que je suis juste nostalgique du bon vieux temps ;)
# Moi == pas doué, je suppose....
Posté par freem . É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…
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 Jiehong (site web personnel) . Évalué à 4.
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 =>
indiquea
doit être un type implémentantOrd
(d'ordonnable).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.
Il faudrait peut-être rajouter des pré-requis à certains journaux / dépêches.
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 freem . Évalué à 2.
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 :)
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 anaseto . Évalué à 6. Dernière modification le 20 février 2016 à 15:14.
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
etfilter
me semblent vraiment apporter quelque chose, je n'ai pas toujours l'impression que lesfold_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 Guillaum (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 ;)
(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 Haskellmain = 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 avecreturn
.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
oustd::cin
) ou le trop classiqueusing 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.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.
À 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 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.
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.
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 anaseto . Évalué à 3. Dernière modification le 21 février 2016 à 22:25.
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 :) ).
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).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.
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 :) ).
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).
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, 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.
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 freem . Évalué à 2. Dernière modification le 22 février 2016 à 00:11.
Juste pour savoir, quelle langage passe par défaut des références?
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).
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 anaseto . É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). 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.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 Guillaum (site web personnel) . Évalué à 3.
Techniquement ce n'est pas
=
qui est en faute ici, mais la sémantique de+=
;)Pour la suite je répond au deux ;)
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 retourJust v
soitNothing
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 syntaxedo
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).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.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 anaseto . Évalué à 2.
Aussi :)
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).
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 freem . Évalué à 2.
Qu'entends-tu par union sécurisée? Une union avec de la "RTTI"?
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 suis preneur, à titre de curiosité.
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 Shuba . Évalué à 2.
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 Guillaum (site web personnel) . Évalué à 5.
Exemple :
Ici, le type
Object
peut-être soit un carréSquare
avec un argumentFloat
(le coté), soit unRectangle
avec deux arguments Float (les deux cotés). Pour faire la difference entre les différents objets, tu es forcé de "pattern match", examples :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.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 :
Ici c'est un type polymorphique paramétré par
t
. Il peut donc contenir soitNothing
, soitJust
une valeur de type t.Maintenant la fonction
find
: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 :
Ici la branche
Nothing
n'a pas le même scope que l'autre branche, ainsi tu ne peux pas utiliseroffset
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.
Rust est assez extra en effet. C'est pas aussi beau et pure que Haskell à mon sens, mais c'est un beau compromis.
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 fonctiongetResult :: Int -> Maybe Int
qui renvoie peut-être un Int. Tu veux renvoyer unMaybe
contenant le résultat multiplié par deux ou Nothing si le résultat n'existe pas. Une implementation naive serait :Cela devient vite lourd. Mais tu as pleins de fonction pour composer directement dans le
Maybe
, comme la fonctionfmap
ou<$>
ce qui à la place donne de façon totalement équivalente :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 uneString
présentant l'employé, ou Nothing si ce n'est pas possible :C'est lourd. Il y a d'autres solutions, mais rien de bien sympathique, sauf la syntaxe
do
qui permet d'exprimer cela :Ici le
<-
signifie de sortir le résultat duMaybe
où d’arrêter le calcul directement si celui-ci contient unNothing
. Cette syntaxe est polymorphique et dépend du type manipulé à condition que celui-ci correspond à une interface (la fameuseMonad
). Comme cela tu peux faire une API très propre, tu pourrais imaginer une API de réseau :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.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 freem . Évalué à 2.
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.
Merci.
Un peu comme ça:
?
[^] # Re: Moi == pas doué, je suppose....
Posté par freem . Évalué à 2.
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.
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).
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 barmic . Évalué à 4.
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.
Oui reste à savoir quel profondeur de copie est fait.
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 freem . Évalué à 2.
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 barmic . É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 Michaël (site web personnel) . Évalué à 4.
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 barmic . É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 anaseto . É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 Michaël (site web personnel) . Évalué à 3.
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 nigaiden . É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 Guillaum (site web personnel) . Évalué à 7.
Je te remercie pour ta critique et je vais me permettre d'y répondre.
Et merde… 30% de mon salaire vient de mes enseignements à l'université… Mais bon, comme on dit, "Ceux qui savent font, les autres enseignent".
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.
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+
.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 quea
donneb
.(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 ;)
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 quea
estOrd
onable" laisserait comprendre la relation entreOrd
etOrd
onable…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 deMonoid
et cela permet de faire du tri de façon trop super"…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 :
list0
qui contient A, B et C, les pointeurs étant representés par des->
: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 :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.
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.
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…
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 ;)
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 freem . Évalué à 2.
D'un autre côté j'ai été très crû… sûrement trop, p'tet pas la bonne journée :)
Ce n'est pas un cours, je l'avais bien compris, mais cette phrase me faisait penser que ça serait moins difficile à lire:
Ce n'est pas exact:
Tout n'est pas rose dans la surcharge d'opérateurs.
Ça peut expliquer les affinités avec les paradigmes je suppose :)
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.
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.
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.).
Mes 1ers langages de programmation, dans l'ordre:
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.
Juste deux petits points:
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]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 écrirefor(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.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 Guillaume Denry (site web personnel) . Évalué à 1. Dernière modification le 20 février 2016 à 11:31.
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 Guillaum (site web personnel) . Évalué à 4.
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.
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 freem . Évalué à 2.
Je suppose donc que les unsigned int (par exemple) disposent de ce type d'interface?
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 Anthony Jaguenaud . Évalué à 5.
Salut,
J’arrive après la bataille (et mes vacances…).
Pour répondre à :
Je vois les choses comme ça :
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 deAnthony
. Et il te donne toutes les réponses possibles.# elm
Posté par Guillaume Denry (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 Arthur Accroc . Évalué à 4. Dernière modification le 25 février 2016 à 10:56.
L’équivalent en Perl :
et le résultat, affiché par
dd
:sort
accepte en argument un bloc de code effectuant la comparaison,$a
et$b
correspondant aux éléments à comparer.<=>
etcmp
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.