Was ist mit LISP, wenn überhaupt, das Implementieren von Makrosystemen erleichtert?


21

Ich lerne Schema von der SICP und habe den Eindruck, dass ein großer Teil dessen, was Schema und vor allem LISP ausmacht, das Makrosystem ist. Aber, da Makros zur Kompilierungszeit erweitert werden, warum stellen die Leute keine äquivalenten Makrosysteme für C / Python / Java / whatever her? Zum Beispiel könnte man den pythonBefehl an expand-macros | pythonoder was auch immer binden . Der Code ist für Benutzer, die das Makrosystem nicht verwenden, weiterhin portierbar. Sie müssen lediglich die Makros erweitern, bevor Sie den Code veröffentlichen. Aber ich kenne so etwas nicht, außer Vorlagen in C ++ / Haskell, von denen ich erfahre, dass sie nicht wirklich gleich sind. Was ist mit LISP, wenn überhaupt, das Implementieren von Makrosystemen erleichtert?


3
"Der Code wäre für Leute, die das Makrosystem nicht verwenden, immer noch portabel. Man würde die Makros einfach erweitern, bevor man den Code veröffentlicht." - Nur um Sie zu warnen, dies funktioniert normalerweise nicht gut. Diese anderen Personen könnten den Code ausführen, aber in der Praxis ist der Code mit erweiterten Makros oft schwer zu verstehen und in der Regel schwer zu ändern. Es ist in der Tat "schlecht geschrieben" in dem Sinne, dass der Autor den erweiterten Code für das menschliche Auge nicht zugeschnitten hat, sondern die eigentliche Quelle zugeschnitten hat. Versuchen Sie einem Java-Programmierer mitzuteilen, dass Sie Ihren Java-Code über den C-Präprozessor ausführen und beobachten, welche Farbe sie annehmen ;-)
Steve Jessop

1
Makros müssen jedoch ausgeführt werden. Zu diesem Zeitpunkt schreiben Sie bereits einen Interpreter für die Sprache.
Mehrdad

Antworten:


29

Viele Lispers werden Ihnen sagen, dass das Besondere an Lisp die Homoikonizität ist , was bedeutet, dass die Syntax des Codes unter Verwendung derselben Datenstrukturen wie bei anderen Daten dargestellt wird. Hier ist zum Beispiel eine einfache Funktion (unter Verwendung der Schemasyntax) zum Berechnen der Hypotenuse eines rechtwinkligen Dreiecks mit den angegebenen Seitenlängen:

(define (hypot x y)
  (sqrt (+ (square x) (square y))))

Homoikonizität besagt nun, dass der obige Code tatsächlich als Datenstruktur (insbesondere Listen von Listen) in Lisp-Code darstellbar ist. Betrachten Sie daher die folgenden Listen und sehen Sie, wie sie zusammenkleben:

  1. (define #2# #3#)
  2. (hypot x y)
  3. (sqrt #4#)
  4. (+ #5# #6#)
  5. (square x)
  6. (square y)

Mit Makros können Sie den Quellcode wie folgt behandeln: Listen von Dingen. Jede dieser 6 „Teil - Listen“ enthält entweder Verweise auf andere Listen oder auf Symbole (in diesem Beispiel: define, hypot, x, y, sqrt, +, square).


Wie können wir also Homoikonizität nutzen, um die Syntax auseinanderzunehmen und Makros zu erstellen? Hier ist ein einfaches Beispiel. Lassen Sie uns das letMakro neu implementieren , das wir aufrufen my-let. Als eine Erinnerung,

(my-let ((foo 1)
         (bar 2))
  (+ foo bar))

sollte in erweitern

((lambda (foo bar)
   (+ foo bar))
 1 2)

Hier ist eine Implementierung Scheme „explizite Umbenennung“ Makros :

(define-syntax my-let
  (er-macro-transformer
    (lambda (form rename compare)
      (define bindings (cadr form))
      (define body (cddr form))
      `((,(rename 'lambda) ,(map car bindings)
          ,@body)
        ,@(map cadr bindings)))))

Der formParameter ist an die tatsächliche Form gebunden, in unserem Beispiel also (my-let ((foo 1) (bar 2)) (+ foo bar)). Lasst uns also das Beispiel durcharbeiten:

  1. Zuerst rufen wir die Bindungen aus dem Formular ab. cadrschnappt sich den ((foo 1) (bar 2))Teil des Formulars.
  2. Dann holen wir den Körper aus der Form. cddrschnappt sich den ((+ foo bar))Teil des Formulars. (Beachten Sie, dass dies dazu gedacht ist, alle Unterformulare nach dem Binden zu erfassen

    (my-let ((foo 1)
             (bar 2))
      (debug foo)
      (debug bar)
      (+ foo bar))
    

    dann wäre der Körper ((debug foo) (debug bar) (+ foo bar)).)

  3. Jetzt erstellen wir den resultierenden lambdaAusdruck und rufen ihn mit den von uns gesammelten Bindings und Body auf. Der Backtick wird als "Quasiquot" bezeichnet. Dies bedeutet, dass alles innerhalb des Quasiquots als wörtliche Bezugspunkte behandelt wird, mit Ausnahme der Nachkommastellen ("unquote").
    • Die (rename 'lambda)Möglichkeit, die bei der Definitionlambda dieses Makros gültige Bindung zu verwenden , und nicht die Bindung, die bei der Verwendung dieses Makros auftreten kann . (Dies wird als Hygiene bezeichnet .)lambda
    • (map car bindings)gibt zurück (foo bar): das erste Datum in jeder der Bindungen.
    • (map cadr bindings)gibt zurück (1 2): das zweite Datum in jeder der Bindungen.
    • ,@ Tut "Splicing", das für Ausdrücke verwendet wird, die eine Liste zurückgeben: Es bewirkt, dass die Elemente der Liste in das Ergebnis und nicht in die Liste selbst eingefügt werden.
  4. Wenn wir das alles zusammenfassen, erhalten wir als Ergebnis die Liste (($lambda (foo bar) (+ foo bar)) 1 2), in der $lambdahier auf die umbenannte verwiesen wird lambda.

Unkompliziert, richtig? ;-) (Wenn es für Sie nicht einfach ist, stellen Sie sich vor, wie schwierig es wäre, ein Makrosystem für andere Sprachen zu implementieren.)


Sie können also über Makrosysteme für andere Sprachen verfügen, wenn Sie in der Lage sind, den Quellcode auf unkomplizierte Weise zu "trennen". Es gibt einige Versuche dazu. Zum Beispiel macht sweet.js dies für JavaScript.

† Für erfahrene Schemas, die dies lesen, habe ich absichtlich explizite Umbenennungsmakros als mittleren Kompromiss zwischen defmacros verwendet, die von anderen Lisp-Dialekten verwendet werden, und syntax-rules(dies wäre die Standardmethode, um ein solches Makro in Scheme zu implementieren). Ich möchte nicht in anderen Lisp-Dialekten schreiben, aber ich möchte keine Nicht-Schemas entfremden, die es nicht gewohnt sind syntax-rules.

Als Referenz ist hier das my-letMakro, das verwendet syntax-rules:

(define-syntax my-let
  (syntax-rules ()
    ((my-let ((id val) ...)
       body ...)
     ((lambda (id ...)
        body ...)
      val ...))))

Die entsprechende syntax-caseVersion sieht sehr ähnlich aus:

(define-syntax my-let
  (lambda (stx)
    (syntax-case stx ()
      ((_ ((id val) ...)
         body ...)
       #'((lambda (id ...)
            body ...)
          val ...)))))

Der Unterschied zwischen den beiden besteht darin, dass auf alles in syntax-rulesein implizites #'angewendet wird, sodass Sie nur Muster- / Vorlagenpaare in syntax-rulesdiesem Element haben können. Daher ist es vollständig deklarativ. Im Gegensatz dazu ist in syntax-casedas Bit nach dem Muster tatsächlicher Code, der am Ende ein Syntaxobjekt ( #'(...)) zurückgeben muss, aber auch anderen Code enthalten kann.


2
Ein Vorteil, den Sie nicht erwähnt haben: Ja, es gibt Versuche in anderen Sprachen, z. B. sweet.js für JS. In lisps wird das Schreiben eines Makros jedoch in derselben Sprache ausgeführt wie das Schreiben einer Funktion.
Florian Margaine

Richtig, Sie können prozedurale (versus deklarative) Makros in Lisp-Sprachen schreiben, was es Ihnen ermöglicht, wirklich fortgeschrittene Dinge zu tun. Übrigens, das gefällt mir an Scheme-Makrosystemen: Es gibt mehrere zur Auswahl. Für einfache Makros verwende ich syntax-rules, was rein deklarativ ist. Für komplizierte Makros kann ich verwenden syntax-case, was teilweise deklarativ und teilweise prozedural ist. Und dann gibt es eine explizite Umbenennung, die rein prozedural ist. (Die meisten Scheme-Implementierungen bieten entweder syntax-caseoder ER. Ich habe noch keine gesehen, die beides bietet. Sie haben eine äquivalente Leistung.)
Chris Jester-Young

Warum müssen Makros den AST ändern? Warum können sie nicht auf einer höheren Ebene arbeiten?
Elliot Gorokhovsky

1
Warum ist LISP dann besser? Was ist das Besondere an LISP? Wenn man Makros in js implementieren kann, kann man sie sicherlich auch in einer anderen Sprache implementieren.
Elliot Gorokhovsky

3
@ RenéG Wie ich in meinem ersten Kommentar sagte, ist ein großer Vorteil, dass du immer noch in derselben Sprache schreibst.
Florian Margaine

23

Eine abweichende Meinung: Lisps Homoikonizität ist weitaus weniger nützlich, als die meisten Lisp-Fans glauben machen würden.

Um syntaktische Makros zu verstehen, ist es wichtig, Compiler zu verstehen. Die Aufgabe eines Compilers besteht darin, von Menschen lesbaren Code in ausführbaren Code umzuwandeln. Aus einer sehr allgemeinen Perspektive besteht dies aus zwei Phasen: Analyse und Codegenerierung .

Beim Parsen wird Code gelesen, gemäß einer Reihe von formalen Regeln interpretiert und in eine Baumstruktur umgewandelt, die allgemein als AST (Abstract Syntax Tree) bezeichnet wird. Bei aller Vielfalt der Programmiersprachen ist dies eine bemerkenswerte Gemeinsamkeit: Im Wesentlichen wird jede universelle Programmiersprache in einer Baumstruktur analysiert.

Bei der Codegenerierung wird der AST des Parsers als Eingabe verwendet und durch Anwendung formaler Regeln in ausführbaren Code umgewandelt. Aus Sicht der Leistung ist dies eine viel einfachere Aufgabe. Viele hochrangige Sprachkompilierer verbringen 75% oder mehr ihrer Zeit mit dem Parsen.

Das Besondere an Lisp ist, dass es sehr, sehr alt ist. Unter den Programmiersprachen ist nur FORTRAN älter als Lisp. Vor langer Zeit galt das Parsen (der langsame Teil des Kompilierens) als dunkle und mysteriöse Kunst. John McCarthys Originalarbeiten über die Theorie von Lisp (damals war es nur eine Idee, von der er nie gedacht hatte, dass sie tatsächlich als echte Computerprogrammiersprache implementiert werden könnten) beschreiben eine etwas komplexere und ausdrucksstärkere Syntax als die modernen "S-Ausdrücke überall für alles" "notation. Das kam später, als die Leute versuchten, es tatsächlich umzusetzen. Da das Parsen damals nicht gut verstanden wurde, haben sie es im Grunde genommen auf den Kopf gestellt und die Syntax in eine homoikonische Baumstruktur getaucht, um die Arbeit des Parsers äußerst trivial zu machen. Das Endergebnis ist, dass Sie (der Entwickler) viel Parser machen müssen. ' s arbeiten dafür, indem Sie das formale AST direkt in Ihren Code schreiben. Homoikonizität "macht Makros nicht so viel einfacher", sondern macht das Schreiben von allem anderen noch viel schwieriger!

Das Problem dabei ist, dass es für S-Ausdrücke, insbesondere bei dynamischer Typisierung, sehr schwierig ist, viele semantische Informationen mit sich herumzutragen. Wenn alle Ihre Syntax dieselbe Art von Dingen (Listen von Listen) ist, gibt es nicht viel in der Art von Kontext, die durch die Syntax bereitgestellt wird, und daher hat das Makrosystem sehr wenig zu arbeiten.

Die Compilertheorie hat seit den 1960er Jahren, als Lisp erfunden wurde, einen langen Weg zurückgelegt, und obwohl die von ihr geleisteten Arbeiten für seine Zeit beeindruckend waren, sehen sie jetzt eher primitiv aus. Schauen Sie sich als Beispiel für ein modernes Metaprogrammiersystem die (leider unterschätzte) Boo-Sprache an. Boo ist statisch typisiert, objektorientiert und Open Source, sodass jeder AST-Knoten einen Typ mit einer genau definierten Struktur hat, in den ein Makroentwickler den Code einlesen kann. Die Sprache hat eine relativ einfache Syntax, die von Python inspiriert ist, mit verschiedenen Schlüsselwörtern, die den darauf aufbauenden Baumstrukturen eine eigene semantische Bedeutung verleihen, und ihre Metaprogrammierung hat eine intuitive Quasiquot-Syntax, um die Erstellung neuer AST-Knoten zu vereinfachen.

Hier ist ein Makro, das ich gestern erstellt habe, als ich feststellte, dass ich dasselbe Muster auf eine Reihe von verschiedenen Stellen im GUI-Code angewendet habe, wo ich BeginUpdate()ein UI-Steuerelement aufrief, eine Aktualisierung in einem tryBlock durchführte und dann aufrief EndUpdate():

macro UIUpdate(value as Expression):
    return [|
        $value.BeginUpdate()
        try:
            $(UIUpdate.Body)
        ensure:
            $value.EndUpdate()
    |]

Der macroBefehl ist in der Tat ein Makro selbst , das einen Makrotext als Eingabe verwendet und eine Klasse zur Verarbeitung des Makros generiert. Der Name des Makros wird als Variable verwendet, die für den MacroStatementAST-Knoten steht, der den Makroaufruf darstellt. Die [| ... |] ist ein Quasiquote-Block, der den AST generiert, der dem Code innerhalb des Quasiquote-Blocks entspricht, und innerhalb des Quasiquote-Blocks stellt das $ -Symbol die Funktion "unquote" bereit, die in einem Knoten wie angegeben ersetzt wird.

Damit ist es möglich zu schreiben:

UIUpdate myComboBox:
   LoadDataInto(myComboBox)
   myComboBox.SelectedIndex = 0

und lassen Sie es erweitern auf:

myComboBox.BeginUpdate()
try:
   LoadDataInto(myComboBox)
   myComboBox.SelectedIndex = 0
ensure:
   myComboBox.EndUpdate()

Ausdruck den Makro diese Art und Weise ist einfacher und intuitiver als es in einem Lisp Makro sein würde, da der Entwickler , die die Struktur kennt MacroStatementund weiß , wie die Argumentsund BodyEigenschaften der Arbeit, und das inhärente Wissen kann verwendet werden , um die Konzepte in einem sehr intuitiven beteiligt auszudrücken Weg. Es ist auch sicherer, weil der Compiler die Struktur von kennt MacroStatementund wenn Sie versuchen, etwas zu codieren, das für a nicht gültig ist MacroStatement, wird der Compiler es sofort abfangen und den Fehler melden, anstatt dass Sie es nicht wissen, bis etwas bei Ihnen explodiert Laufzeit.

Das Übertragen von Makros auf Haskell, Python, Java, Scala usw. ist nicht schwierig, da diese Sprachen nicht homoikonisch sind. Dies ist schwierig, da die Sprachen nicht für sie entwickelt wurden. Dies funktioniert am besten, wenn die AST-Hierarchie Ihrer Sprache von Grund auf so entwickelt wurde, dass sie von einem Makrosystem überprüft und bearbeitet werden kann. Wenn Sie mit einer Sprache arbeiten, die von Anfang an unter Berücksichtigung der Metaprogrammierung entwickelt wurde, sind Makros viel einfacher und einfacher zu handhaben!


4
Freude zu lesen, danke! Gehen Nicht-Lisp-Makros so weit, dass sie die Syntax ändern? Da eine der Stärken von Lisp darin besteht, dass die Syntax alle gleich ist, ist es einfach, eine funktionsbedingte Anweisung hinzuzufügen, auch wenn sie alle gleich sind. Während sich bei Nicht-Lisp-Sprachen eines von dem anderen unterscheidet - if...sieht es beispielsweise nicht wie ein Funktionsaufruf aus. Ich kenne Boo nicht, aber stelle dir vor, Boo hätte keine Mustererkennung. Könnten Sie es mit einer eigenen Syntax als Makro einführen? Mein Punkt ist - jedes neue Makro in Lisp fühlt sich 100% natürlich an, in anderen Sprachen funktionieren sie, aber Sie können die Stiche sehen.
Greenoldman

4
Die Geschichte, wie ich sie immer gelesen habe, ist etwas anders. Eine alternative Syntax zu s-expression war geplant, aber die Arbeit daran wurde verzögert, da die Programmierer bereits begonnen hatten, s-expressions zu verwenden, und diese als praktisch erachteten. So geriet die Arbeit an der neuen Syntax irgendwann in Vergessenheit. Können Sie bitte die Quelle angeben, die die Mängel der Compilertheorie als Grund für die Verwendung von s-Ausdrücken angibt? Außerdem entwickelte sich die Lisp-Familie über viele Jahrzehnte weiter (Schema, Common Lisp, Clojure), und die meisten Dialekte entschieden sich, sich an s-Ausdrücke zu halten.
Giorgio

5
"einfacher und intuitiver": sorry, aber ich verstehe nicht wie. "Updating.Arguments [0]" ist nicht aussagekräftig, ich hätte lieber ein Argument mit Namen und lasse den Compiler prüfen, ob die Anzahl der Argumente übereinstimmt: pastebin.com/YtUf1FpG
coredump

8
"Aus Sicht der Leistung ist dies eine viel einfachere Aufgabe. Viele Hochsprachen-Compiler verbringen 75% oder mehr ihrer Zeit mit dem Parsen." Ich hätte erwartet, nach Optimierungen zu suchen und diese anzuwenden, um die meiste Zeit in Anspruch zu nehmen (aber ich habe noch nie einen richtigen Compiler geschrieben). Vermisse ich hier etwas?
Doval

5
Leider zeigt Ihr Beispiel das nicht. Es ist primitiv, in jedem Lisp mit Makros zu implementieren. Tatsächlich ist dies eines der primitivsten zu implementierenden Makros. Dies lässt mich vermuten, dass Sie nicht viel über Makros in Lisp wissen. "Lisps Syntax steckt in den 1960er Jahren fest": Tatsächlich haben Makrosysteme in Lisp seit 1960 große Fortschritte gemacht (1960 hatte Lisp noch nicht einmal Makros!).
Rainer Joswig

3

Ich lerne Schema von der SICP und habe den Eindruck, dass ein großer Teil dessen, was Schema und vor allem LISP ausmacht, das Makrosystem ist.

Wieso das? Der gesamte Code in SICP ist makrofrei geschrieben. In SICP gibt es keine Makros. Nur in einer Fußnote auf Seite 373 werden Makros jemals erwähnt.

Da Makros jedoch zur Kompilierungszeit erweitert werden

Sie sind nicht unbedingt. Lisp stellt Makros sowohl für Interpreter als auch für Compiler bereit. Daher gibt es möglicherweise keine Kompilierungszeit. Wenn Sie einen Lisp-Interpreter haben, werden Makros zur Ausführungszeit erweitert. Da viele Lisp-Systeme über einen Compiler verfügen, kann Code generiert und zur Laufzeit kompiliert werden.

Testen wir das mit SBCL, einer Common Lisp-Implementierung.

Lassen Sie uns SBCL auf den Interpreter umstellen:

* (setf sb-ext:*evaluator-mode* :interpret)

:INTERPRET

Nun definieren wir ein Makro. Das Makro druckt etwas, wenn es für Code erweitert aufgerufen wird. Der generierte Code wird nicht gedruckt.

* (defmacro my-and (a b)
    (print "macro my-and used")
    `(if ,a
         (if ,b t nil)
         nil))

Verwenden wir nun das Makro:

MY-AND
* (defun foo (a b) (my-and a b))

FOO

Sehen. In obigem Fall tut Lisp nichts. Das Makro wird zum Definitionszeitpunkt nicht erweitert.

* (foo t nil)

"macro my-and used"
NIL

Zur Laufzeit wird das Makro jedoch erweitert, wenn der Code verwendet wird.

* (foo t t)

"macro my-and used"
T

Wiederum wird zur Laufzeit, wenn der Code verwendet wird, das Makro erweitert.

Beachten Sie, dass SBCL bei Verwendung eines Compilers nur einmal erweitert wird. Verschiedene Lisp-Implementierungen bieten aber auch Interpreten - wie SBCL.

Warum sind Makros in Lisp einfach? Nun, sie sind nicht wirklich einfach. Nur in Lisps, und es gibt viele, die eine eingebaute Makrounterstützung haben. Da viele Lisps über umfangreiche Maschinen für Makros verfügen, sieht es so aus, als ob es einfach ist. Makromechanismen können jedoch äußerst kompliziert sein.


Ich habe viel über Scheme im Internet gelesen und auch SICP gelesen. Werden Lisp-Ausdrücke nicht kompiliert, bevor sie interpretiert werden? Sie müssen zumindest analysiert werden. Also denke ich, dass "Kompilierungszeit" "Analysezeit" sein sollte.
Elliot Gorokhovsky

@ RenéG Rainer meint, dass wenn Sie evaloder loadCode in einer beliebigen Lisp-Sprache, Makros in diesen auch verarbeitet werden. Wenn Sie hingegen ein Präprozessorsystem verwenden, wie in Ihrer Frage vorgeschlagen, wird evaldie Makroerweiterung keine Vorteile für Sie haben.
Chris Jester-Young

@ RenéG Auch "parse" heißt readin Lisp. Diese Unterscheidung ist wichtig, da evaldie Listendatenstrukturen (wie in meiner Antwort erwähnt) tatsächlich bearbeitet werden und nicht die Textform. Du kannst also 2 benutzen (eval '(+ 1 1))und zurückholen, aber wenn du(eval "(+ 1 1)") , erhalten Sie zurück "(+ 1 1)"(die Zeichenfolge). Sie verwenden read, um von "(+ 1 1)"(eine Zeichenfolge von 7 Zeichen) zu (+ 1 1)(eine Liste mit einem Symbol und zwei Fixnummern) zu gelangen.
Chris Jester-Young

@ RenéG Nach diesem Verständnis funktionieren Makros nicht zur readgleichen Zeit. Sie funktionieren zur Kompilierungszeit in dem Sinne, dass, wenn Sie Code wie haben (and (test1) (test2)), dieser (if (test1) (test2) #f)nur einmal erweitert wird, wenn der Code geladen wird, und nicht jedes Mal, wenn der Code ausgeführt wird, aber wenn Sie so etwas tun (eval '(and (test1) (test2))), das diesen Ausdruck zur Laufzeit entsprechend kompiliert (und makro-expandiert).
Chris Jester-Young

@ RenéG Homoikonizität ermöglicht es Lisp-Sprachen, die Listenstrukturen anstelle der Textform zu bewerten und diese Listenstrukturen vor der Ausführung (über Makros) zu transformieren. Die meisten Sprachen evalarbeiten nur mit Textzeichenfolgen, und ihre Möglichkeiten zur Syntaxänderung sind weitaus weniger ausgeprägt und / oder umständlicher.
Chris Jester-Young

1

Homoiconicity erleichtert die Implementierung von Makros erheblich. Die Idee, dass Code Daten und Daten Code sind, macht es möglich, mehr oder weniger (außer bei versehentlicher Erfassung von Identifikatoren, die durch hygienische Makros gelöst werden ) frei voneinander zu ersetzen. Lisp und Scheme vereinfachen dies durch ihre Syntax von S-Ausdrücken, die einheitlich strukturiert sind und sich daher leicht in ASTs umwandeln lassen, die die Grundlage für syntaktische Makros bilden .

Sprachen ohne S-Ausdrücke oder Homoikonizität werden Probleme haben, Syntaktische Makros zu implementieren, obwohl dies mit Sicherheit noch möglich ist. Das Projekt Kepler versucht, sie beispielsweise in Scala einzuführen.

Das größte Problem bei der Verwendung von Syntaxmakros ist neben der Nichthomoikonizität das Problem der willkürlich generierten Syntax. Sie bieten enorme Flexibilität und Leistung, aber zu dem Preis, den Ihr Quellcode möglicherweise nicht mehr so ​​einfach zu verstehen oder zu warten ist.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.