Pourquoi Java 8 n'inclut-il pas de collections immuables?


130

L’équipe Java a déployé des efforts considérables pour supprimer les obstacles à la programmation fonctionnelle dans Java 8. En particulier, les modifications apportées aux collections java.util permettent d’enchaîner les transformations en opérations très rapides. Compte tenu de la qualité de leur travail d’ajout de fonctions de première classe et de méthodes fonctionnelles sur les collections, pourquoi ont-ils complètement échoué à fournir des collections immuables, voire des interfaces de collection immuables?

Sans modifier aucun code existant, l'équipe Java pourrait à tout moment ajouter des interfaces immuables identiques à celles qui sont mutables, moins les méthodes "set", et en faire s'étendre les interfaces existantes, comme suit:

                  ImmutableIterable
     ____________/       |
    /                    |
Iterable        ImmutableCollection
   |    _______/    /          \   \___________
   |   /           /            \              \
 Collection  ImmutableList  ImmutableSet  ImmutableMap  ...
    \  \  \_________|______________|__________   |
     \  \___________|____________  |          \  |
      \___________  |            \ |           \ |
                  List            Set           Map ...

Bien sûr, des opérations comme List.add () et Map.put () renvoient actuellement une valeur booléenne ou précédente pour la clé donnée pour indiquer si l'opération a réussi ou échoué. Les collections immuables doivent traiter ces méthodes comme des usines et renvoyer une nouvelle collection contenant l'élément ajouté - ce qui est incompatible avec la signature actuelle. Mais cela pourrait être résolu en utilisant un nom de méthode différent comme ImmutableList.append () ou .addAt () et ImmutableMap.putEntry (). Les avantages de travailler avec des collections immuables l'emporteraient sur la verbosité qui en résulterait et le système de types éviterait les erreurs d'appeler la mauvaise méthode. Avec le temps, les anciennes méthodes pourraient être obsolètes.

Victoires des collections immuables:

  • Simplicité - le raisonnement sur le code est plus simple lorsque les données sous-jacentes ne changent pas.
  • Documentation - Si une méthode prend une interface de collection immuable, vous savez qu'elle ne modifiera pas cette collection. Si une méthode retourne une collection immuable, vous savez que vous ne pouvez pas la modifier.
  • Concurrence - les collections immuables peuvent être partagées en toute sécurité entre les threads.

En tant que personne ayant goûté à des langues supposées immuables, il est très difficile de retourner dans le Far West avec une mutation rampante. Les collections de Clojure (séquence abstraite) ont déjà tout ce que les collections de Java 8 fournissent, ainsi que leur immuabilité (bien que l'utilisation de mémoire et de temps supplémentaires soient dus à des listes chaînées synchronisées plutôt qu'à des flux). Scala possède à la fois des collections modifiables et immuables avec un ensemble complet d'opérations. Bien que ces opérations soient ardentes, appeler .iterator vous donne une vue paresseuse (et il existe d'autres moyens de les évaluer paresseusement). Je ne vois pas comment Java peut continuer à rivaliser sans collections immuables.

Quelqu'un peut-il m'indiquer l'histoire ou la discussion à ce sujet? C'est sûrement public quelque part.


9
En rapport avec cela - Ayende a récemment publié un blog sur les collections et les collections immuables en C #, avec des points de repère. ayende.com/blog/tags/performance - tl; dr - l’immutabilité est lente .
Oded

20
avec votre hiérarchie, je peux vous donner une liste Immutable et la modifier ensuite lorsque vous ne vous attendez pas à une chose qui risque de casser beaucoup de choses, car vous ne disposez que de constcollections
reaket freak

18
@Oded L'immuabilité est lente, mais le verrouillage aussi. Il en va de même pour maintenir une histoire. La simplicité / exactitude vaut la vitesse dans de nombreuses situations. Avec les petites collections, la vitesse n'est pas un problème. L'analyse d'Ayende est basée sur l'hypothèse que vous n'avez pas besoin d'historique, de verrouillage ou de simplicité et que vous travaillez avec un ensemble de données volumineux. Parfois, c'est vrai, mais ce n'est pas toujours préférable. Il y a des compromis.
GlenPeterson

5
@GlenPeterson c'est à quoi servent les copies défensives Collections.unmodifiable*(). mais ne les traitez pas comme immuables quand ils ne le sont pas
Ratchet Freak

13
Hein? Si votre fonction prend un ImmutableListdans ce diagramme, les gens peuvent-ils passer dans un mutable List? Non, c'est une très mauvaise violation de LSP.
Telastyn

Réponses:


113

Parce que les collections immuables nécessitent absolument un partage pour être utilisables. Sinon, chaque opération place une liste entière dans le tas quelque part. Les langages totalement immuables, comme Haskell, génèrent une quantité incroyable de déchets sans optimisations ni partage agressifs. Avoir une collection utilisable uniquement avec <50 éléments ne vaut pas la peine d’être inséré dans la bibliothèque standard.

De plus, les collections immuables ont souvent des implémentations fondamentalement différentes de celles de leurs homologues mutables. Considérons par exemple ArrayList, une immuable efficace ArrayListne serait pas un tableau du tout! Il devrait être implémenté avec un arbre équilibré avec un grand facteur de ramification, Clojure utilise 32 IIRC. Faire en sorte que les collections mutables soient "immuables" en ajoutant simplement une mise à jour fonctionnelle est un bug de performance au même titre qu'une fuite de mémoire.

De plus, le partage n'est pas viable en Java. Java fournit trop de crochets illimités à la mutabilité et à l'égalité de référence pour que le partage ne soit "qu'une optimisation". Si vous pouviez modifier un élément d'une liste et vous rendre compte que vous venez de modifier un élément dans les 20 autres versions de cette liste, cela vous déplairait probablement un peu.

Cela exclut également d'énormes classes d'optimisations très vitales pour une immutabilité efficace, le partage, la fusion de flux, etc. (Cela ferait un bon slogan pour les évangélistes FP)


21
Mon exemple parle d’ interfaces immuables . Java pourrait fournir une suite complète d' implémentations à la fois mutables et immuables de ces interfaces qui feraient les compromis nécessaires. Il appartient au programmeur de choisir mutable ou immuable, selon le cas. Les programmeurs doivent savoir quand utiliser une liste par rapport à un ensemble maintenant. En règle générale, vous n'avez pas besoin de la version mutable avant d'avoir un problème de performances. Dans ce cas, elle ne sera peut-être nécessaire qu'en tant que générateur. En tout cas, avoir l'interface immuable serait une victoire en soi.
GlenPeterson

4
J'ai relu votre réponse et je pense que vous dites que Java repose sur une hypothèse fondamentale de mutabilité (par exemple, des haricots java) et que les collections ne représentent que la partie visible de l'iceberg et que supprimer cette partie ne résoudra pas le problème sous-jacent. Un point valide. Je pourrais accepter cette réponse et accélérer mon adoption de Scala! :-)
GlenPeterson

8
Je ne suis pas sûr que les collections immuables nécessitent la capacité de partager des parties communes pour être utiles. Le type immuable le plus courant en Java, une collection immuable de caractères, permet le partage mais ne le fait plus. Ce qui est important, c’est la possibilité de copier rapidement des données d’un utilisateur Stringdans un StringBufferordinateur, de les manipuler, puis de les copier dans une nouvelle immuable String. Utiliser un tel modèle avec des ensembles et des listes peut être aussi efficace que d’utiliser des types immuables conçus pour faciliter la production d’instances légèrement modifiées, mais qui pourraient tout de même être meilleurs ...
supercat

3
Il est tout à fait possible de créer une collection immuable en Java en utilisant le partage. Les éléments stockés dans la collection sont des références et leurs référents peuvent être mutés - et alors? Un tel comportement casse déjà les collections existantes telles que HashMap et TreeSet, mais celles-ci sont implémentées en Java. Et si plusieurs collections contiennent des références au même objet, il est tout à fait probable que la modification de l'objet provoque une modification visible lors de son affichage dans toutes les collections.
Le secret de Solomonoff

4
jozefg, il est tout à fait possible de mettre en œuvre des collections immuables efficaces sur JVM avec un partage structurel. Scala et Clojure en font partie de leur bibliothèque standard. Les deux implémentations sont basées sur HAMT (Hash Array Mapped Trie) de Phil Bagwell. Votre déclaration concernant la mise en œuvre de structures de données immuables avec des arbres BALANCED par Clojure est totalement fausse.
sesm

78

Une collection mutable n'est pas un sous-type d'une collection immuable. Au lieu de cela, les collections mutables et immuables sont des descendants frères de collections lisibles. Malheureusement, les concepts de "lisible", "lecture seule" et "immuable" semblent s'estomper, même s'ils signifient trois choses différentes.

  • Une classe de base ou un type d'interface de collection lisible garantit la possibilité de lire des éléments et ne fournit aucun moyen direct de modifier la collection, mais ne garantit pas que le code recevant la référence ne puisse pas le transtyper ni le manipuler de manière à permettre sa modification.

  • Une interface de collection en lecture seule n'inclut pas de nouveaux membres, mais ne devrait être implémentée que par une classe qui promet qu'il n'y a aucun moyen de manipuler une référence à celle-ci de manière à muter la collection ou à recevoir une référence à quelque chose cela pourrait le faire. Cependant, cela ne promet pas que la collection ne sera pas modifiée par quelque chose d'autre faisant référence aux éléments internes. Notez qu'une interface de collection en lecture seule peut ne pas être capable d'empêcher l'implémentation par des classes mutables, mais peut spécifier que toute implémentation, ou classe dérivée d'une implémentation, qui permet une mutation doit être considérée comme une implémentation "illégitime" ou le dérivé d'une implémentation. .

  • Une collection immuable est une collection qui contiendra toujours les mêmes données tant que toute référence à celles-ci existe. Toute implémentation d'une interface immuable qui ne renvoie pas toujours les mêmes données en réponse à une requête particulière est interrompue.

Il est parfois utile d'avoir des types de collecte mutables et immuables fortement associés à la fois la mise en œuvre qui ou dériver du même type « lisible », et d'avoir le type lisible comprennent AsImmutable, AsMutableet des AsNewMutableméthodes. Une telle conception peut permettre au code qui veut faire persister les données d’une collection d’appeler AsImmutable; cette méthode fera une copie défensive si la collection est modifiable, mais ignore la copie si elle est déjà immuable.


1
Très bonne réponse. Des collections immuables peuvent vous donner une garantie assez forte en matière de sécurité des threads et de la façon dont vous pouvez les raisonner au fil du temps. Une collection en lecture / lecture seule ne le fait pas. En fait, pour respecter le principe de substitution de liskov, Read-Only et Immutable devraient probablement être du type de base abstrait avec la méthode finale et des membres privés afin de garantir qu'aucune classe dérivée ne puisse détruire la garantie donnée par le type. Ou bien ils doivent être de type tout à fait concret, soit envelopper une collection (lecture seule), soit toujours prendre une copie défensive (immuable). C'est comme ça que la goyave ImmutableList le fait.
Laurent Bourgault-Roy

1
@ LaurentBourgault-Roy: Il existe des avantages à la fois pour les types immuables scellés et héritables. Si on ne veut pas laisser une classe dérivée illégitime briser ses invariants, les types scellés peuvent offrir une protection contre cela, alors que les classes héritables n'en offrent aucune. D'un autre côté, un code connaissant quelque chose sur les données qu'il détient peut le stocker beaucoup plus compactement qu'un type ne le sachant pas. Considérons, par exemple, un type ReadableIndexedIntSequence qui encapsule une séquence de int, avec méthodes getLength()et getItemAt(int).
Supercat

1
@ LaurentBourgault-Roy: Donné un ReadableIndexedIntSequence, on pourrait produire une instance d'un type immuable basé sur un tableau en copiant tous les éléments dans un tableau, mais supposons qu'une implémentation particulière renvoie simplement 16777216 pour la longueur et ((long)index*index)>>24pour chaque élément. Ce serait une séquence immuable légitime d'entiers, mais la copier dans un tableau serait une énorme perte de temps et de mémoire.
Supercat

1
Je suis complètement d'accord. Ma solution vous donne la correction (jusqu'à un certain point), mais pour obtenir des performances avec un jeu de données volumineux, vous devez disposer d'une structure et d'une conception persistantes permettant l'immutabilité dès le début. Pour les petites collections, vous pouvez toutefois prendre de temps en temps une copie immuable. Je me souviens que Scala avait analysé divers programmes et avait constaté qu'environ 90% des listes instanciées comptaient 10 éléments ou moins.
Laurent Bourgault-Roy

1
@ LaurentBourgault-Roy: La question fondamentale est de savoir si on fait confiance aux personnes qui ne produisent pas d'implémentations brisées ou de classes dérivées. Si tel est le cas, et si les interfaces / classes de base fournissent des méthodes asMutable / asImmutable, il est possible d'améliorer les performances de plusieurs ordres de grandeur [par exemple, comparer le coût d'un appel asImmutabled'une instance de la séquence définie ci-dessus par rapport au coût de la construction une copie immuable sauvegardée sur un tableau]. Je dirais qu'il est probablement préférable de définir des interfaces à ces fins que d'essayer d'utiliser des approches ad hoc; IMHO, la plus grande raison ...
Supercat

15

Java Collections Framework offre la possibilité de créer une version en lecture seule d'une collection au moyen de six méthodes statiques dans la classe java.util.Collections :

Comme quelqu'un l'a souligné dans les commentaires sur la question initiale, les collections renvoyées ne peuvent pas être considérées comme immuables, car même si les collections ne peuvent pas être modifiées (aucun membre ne peut être ajouté ou supprimé d'une telle collection), les objets réels référencés par la collection peuvent être modifiés si leur type d'objet le permet.

Toutefois, ce problème persisterait, que le code retourne un seul objet ou une collection d'objets non modifiable. Si le type autorise la mutation de ses objets, alors cette décision a été prise dans la conception du type et je ne vois pas en quoi une modification de la JCF pourrait modifier cela. Si l'immuabilité est importante, les membres d'une collection doivent être d'un type immuable.


4
La conception des collections non modifiables aurait été grandement améliorée si les enveloppes indiquaient si la chose emballée était déjà immuable, et s'il existait des immutableListméthodes d'usine qui renverraient une enveloppe en lecture seule autour de la copie d'un document transmis. liste sauf si la liste transmise était déjà immuable . Il serait facile de créer de tels types définis par l'utilisateur, mais pour un problème: il n'y aurait aucun moyen pour la joesCollections.immutableListméthode de reconnaître qu'elle n'aurait pas besoin de copier l'objet renvoyé par fredsCollections.immutableList.
Supercat

8

C'est une très bonne question. J'apprécie l'idée que, de tout le code écrit en java et utilisé sur des millions d'ordinateurs du monde entier, chaque jour et vingt-quatre heures sur vingt-quatre, il faut gaspiller environ la moitié du nombre total de cycles d'horloge pour ne faire que des copies de sécurité des collections être retourné par des fonctions. (Et ramasser les ordures quelques millisecondes après leur création.)

Un pourcentage de programmeurs java est conscient de l'existence de la unmodifiableCollection()famille de méthodes de la Collectionsclasse, mais même parmi elles, beaucoup ne s'en préoccupent pas.

Et je ne peux pas leur en vouloir: une interface qui prétend être en lecture-écriture mais jettera un UnsupportedOperationExceptionsi vous faites l'erreur d'invoquer l'une de ses méthodes d'écriture est tout à fait un mal!

Maintenant, une interface comme celle Collectionqui manquerait add(), remove()et clear()méthodes ne serait pas une interface "ImmutableCollection"; ce serait une interface "UnmodifiableCollection". En fait, il ne pourrait jamais y avoir d'interface "ImmutableCollection", car l'immutabilité est une nature d'une implémentation, pas une caractéristique d'une interface. Je sais, ce n'est pas très clair. laissez-moi expliquer.

Supposons que quelqu'un vous remette une telle interface de collection en lecture seule; Est-il prudent de le passer à un autre thread? Si vous saviez avec certitude qu'il s'agit d'une collection véritablement immuable, la réponse serait «oui». Malheureusement, comme il s’agit d’une interface, vous ne savez pas comment elle est mise en œuvre, la réponse doit donc être un non : pour autant que vous sachiez, il peut s’agir d’une vue non modifiable (pour vous) d’une collection qui est en fait modifiable, (comme ce que vous obtenez avec Collections.unmodifiableCollection()), donc tenter de le lire pendant qu'un autre thread le modifie entraînerait la lecture de données corrompues.

Donc, ce que vous avez essentiellement décrit est un ensemble d’interfaces de collection non "immuables", mais "non modifiables". Il est important de comprendre que "non modifiable" signifie simplement que quiconque ayant une référence à une telle interface est empêché de modifier la collection sous-jacente, et ce simplement parce que l'interface manque de méthodes de modification, et non pas parce que la collection sous-jacente est nécessairement immuable. La collection sous-jacente pourrait très bien être mutable; vous n'avez aucune connaissance et aucun contrôle sur cela.

Pour avoir des collections immuables, il faudrait que ce soit des classes , pas des interfaces!

Ces classes de collections immuables doivent être définitives. Ainsi, lorsque vous recevez une référence à une telle collection, vous savez avec certitude qu’elle se comportera comme une collection immuable, peu importe ce que vous-même, ou quiconque faire avec.

Donc, pour avoir un ensemble complet de collections en java (ou tout autre langage impératif déclaratif), nous aurions besoin des éléments suivants:

  1. Un ensemble d' interfaces de collection non modifiables .

  2. Un ensemble d' interfaces de collection mutables , étendant celles non modifiables.

  3. Un ensemble de classes de collection mutables implémentant les interfaces mutables et, par extension, les interfaces non modifiables.

  4. Un ensemble de classes de collections immuables , implémentant les interfaces non modifiables, mais généralement transmises sous forme de classes afin de garantir l’immuabilité

J'ai mis en œuvre tout ce qui précède pour le plaisir, et je les utilise dans des projets, et ils fonctionnent à merveille.

La raison pour laquelle ils ne font pas partie de l'exécution de Java est probablement due au fait que cela aurait été trop / trop complexe / trop difficile à comprendre.

Personnellement, je pense que ce que j'ai décrit ci-dessus ne suffit même pas; une dernière chose qui semble nécessaire est un ensemble d’interfaces et de classes mutables pour l’ immutabilité structurelle . (Ce qui peut simplement s'appeler "Rigide" car le préfixe "StructurallyImmutable" est trop long.)


Bons points. Deux détails: 1. Les collections immuables nécessitent certaines signatures de méthode, en particulier (en utilisant une liste comme exemple): List<T> add(T t)- toutes les méthodes "mutateur" doivent renvoyer une nouvelle collection qui reflète le changement. 2. Pour le meilleur ou pour le pire, les interfaces représentent souvent un contrat en plus d'une signature. Sérialisable est l'une de ces interfaces. De même, Comparable nécessite que vous implémentiez correctement votre compareTo()méthode pour fonctionner correctement et être parfaitement compatible avec equals()et hashCode().
GlenPeterson

Oh, je ne pensais même pas à une immuabilité mutuelle par copie. Ce que j'ai écrit ci-dessus fait référence à des collections simples, immuables et qui n'ont vraiment aucune méthode add(). Mais je suppose que si des méthodes mutatrices devaient être ajoutées aux classes immuables, elles devraient alors renvoyer également des classes immuables. Donc, s'il y a un problème qui se cache là-bas, je ne le vois pas.
Mike Nakis

Votre implémentation est-elle accessible au public? J'aurais dû demander cela il y a des mois. Quoi qu'il en soit, le mien est: github.com/GlenKPeterson/UncleJim
GlenPeterson

4
Suppose someone hands you such a read-only collection interface; is it safe to pass it to another thread?Supposons que quelqu'un vous transmette une instance d'une interface de collection mutable. Est-il prudent d’invoquer une méthode? Vous ne savez pas que l'implémentation ne fonctionne pas en boucle, ne génère pas d'exception ou ignore complètement le contrat de l'interface. Pourquoi avoir un double standard spécifiquement pour les collections immuables?
Doval

1
IMHO votre raisonnement contre les interfaces mutables est faux. Vous pouvez écrire une implémentation modifiable d'interfaces immuables, puis casser. Sûr. Mais c'est de votre faute si vous violez le contrat. Arrête de faire ça. Ce n'est pas différent de casser un SortedSeten sous-classant l'ensemble avec une implémentation non conforme. Ou en passant une incohérente Comparable. Presque tout peut être cassé si vous voulez. J'imagine que c'est ce que @Doval voulait dire par "deux poids deux mesures".
Maaartinus

2

Les collections immuables peuvent être profondément récursives, comparées les unes aux autres, et pas excessivement inefficaces si l'égalité d'objet est définie par secureHash. C'est ce qu'on appelle une forêt de merkle. Il peut s'agir d'une collection ou de parties de celles-ci, par exemple un arbre AVL (auto-équilibré) pour une carte triée.

À moins que tous les objets java de ces collections aient un identifiant unique ou une chaîne de caractères à hachage, la collection n'a rien à hacher pour se nommer de manière unique.

Exemple: sur mon ordinateur portable 4x1,6 GHz, je peux exécuter 200 Ko sha256 par seconde de la plus petite taille pouvant être insérée dans un cycle de hachage (jusqu'à 55 octets), par rapport à 500 Ko HashMap ou 3M dans une table de hachage de longue durée. 200 Ko / log (collectionSize) Le nombre de nouvelles collections par seconde est suffisamment rapide pour permettre l’intégrité des données et l’évolutivité globale anonyme.


-3

Performance. Les collections de par leur nature peuvent être très volumineuses. Copier 1000 éléments dans une nouvelle structure avec 1001 éléments au lieu d'insérer un seul élément est tout simplement horrible.

La concurrence. Si vous avez plusieurs threads en cours d'exécution, ils peuvent vouloir obtenir la version actuelle de la collection et non la version passée il y a 12 heures lorsque le thread a démarré.

Espace de rangement. Avec des objets immuables dans un environnement multi-thread, vous pouvez vous retrouver avec des dizaines de copies du "même" objet à différents moments de son cycle de vie. Peu importe pour un objet Calendrier ou Date, mais quand il s'agit d'une collection de 10 000 widgets, cela vous tuera.


12
Les collections immuables ne doivent être copiées que si vous ne pouvez pas les partager en raison de la mutabilité généralisée comme celle de Java. La concurrence est généralement plus facile avec des collections immuables car elles ne nécessitent pas de verrouillage; et pour plus de visibilité, vous pouvez toujours avoir une référence mutable à une collection immuable (courante dans OCaml). Avec le partage, les mises à jour peuvent être essentiellement gratuites. Vous pouvez faire logarithmiquement plus d'allocations qu'avec une structure mutable, mais lors de la mise à jour, de nombreux sous-objets expirés peuvent être libérés immédiatement ou réutilisés, vous n'avez donc pas nécessairement une surcharge de mémoire.
Jon Purdy

4
Problèmes de couple Les collections de Clojure et de Scala sont immuables, mais supportent des copies légères. Ajouter un élément 1001 signifie copier moins de 33 éléments et créer quelques nouveaux pointeurs. Si vous partagez une collection mutable sur plusieurs threads, vous rencontrez toutes sortes de problèmes de synchronisation lorsque vous le modifiez. Des opérations comme "remove ()" sont cauchemardesques. De plus, les collections immuables peuvent être construites de manière mutuelle, puis copiées une fois dans une version immuable sécurisée à partager entre les threads.
GlenPeterson

4
L'utilisation de la simultanéité comme argument contre l'immuabilité est inhabituelle. Les doublons aussi.
Tom Hawtin - tackline

4
Un peu moqué de la baisse des votes ici. Le PO a demandé pourquoi il n’avait pas mis en place de collections immuables et j’ai fourni une réponse réfléchie à la question. Vraisemblablement, la seule réponse acceptable parmi les personnes conscientes de la mode est "parce qu'elles ont commis une erreur". En fait, je suis un peu habitué à ce que je doive refactoriser de gros morceaux de code en utilisant la classe par ailleurs excellente BigDecimal uniquement à cause de mauvaises performances dues à l’immuabilité 512 fois supérieure à celle de l’utilisation d’un double et de quelques bêtises pour corriger les décimales.
James Anderson

3
@JamesAnderson: Votre problème avec votre réponse: "Performance" - vous pouvez dire que les collections immuables dans la vie réelle implémentent toujours une forme de partage et de réutilisation pour éviter exactement le problème que vous décrivez. "Concurrence" - l'argument se résume à "Si vous voulez la mutabilité, un objet immuable ne fonctionne pas." Je veux dire que s’il existe une notion de "dernière version de la même chose", alors il faut que quelque chose mute, soit la chose elle-même, soit quelque chose qui la possède. Et dans "Storage", vous semblez dire que la mutabilité n'est parfois pas souhaitée.
Jhominal
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.