Die meisten Programmiersprachen (sowohl dynamisch als auch statisch typisierte Sprachen) haben spezielle Schlüsselwörter und / oder Syntax, die sich stark von der Deklaration von Variablen zum Deklarieren von Funktionen unterscheiden. Ich sehe Funktionen so, als würde man eine andere benannte Entität deklarieren:
Zum Beispiel in Python:
x = 2
y = addOne(x)
def addOne(number):
return number + 1
Warum nicht:
x = 2
y = addOne(x)
addOne = (number) =>
return number + 1
Ebenso in einer Sprache wie Java:
int x = 2;
int y = addOne(x);
int addOne(int x) {
return x + 1;
}
Warum nicht:
int x = 2;
int y = addOne(x);
(int => int) addOne = (x) => {
return x + 1;
}
Diese Syntax scheint natürlicher zu sein, um etwas zu deklarieren (sei es eine Funktion oder eine Variable) und ein Schlüsselwort weniger wie def
oder function
in einigen Sprachen. Und, IMO, es ist konsistenter (ich schaue an der gleichen Stelle, um den Typ einer Variablen oder Funktion zu verstehen) und macht es wahrscheinlich ein bisschen einfacher, die Parser / Grammatik zu schreiben.
Ich weiß, dass nur sehr wenige Sprachen diese Idee verwenden (CoffeeScript, Haskell), aber die meisten gängigen Sprachen haben eine spezielle Syntax für Funktionen (Java, C ++, Python, JavaScript, C #, PHP, Ruby).
Sogar in Scala, das beide Möglichkeiten unterstützt (und Typinferenz hat), ist es üblicher zu schreiben:
def addOne(x: Int) = x + 1
Eher, als:
val addOne = (x: Int) => x + 1
IMO, zumindest in Scala, dies ist wahrscheinlich die am leichtesten verständliche Version, aber diese Redewendung wird selten befolgt:
val x: Int = 1
val y: Int = addOne(x)
val addOne: (Int => Int) = x => x + 1
Ich arbeite an meiner eigenen Spielzeugsprache und frage mich, ob es irgendwelche Fallstricke gibt, wenn ich meine Sprache so gestalte und wenn es historische oder technische Gründe gibt, denen dieses Muster nicht weitestgehend folgt.
(int => int) addOne = (x) => {
ist es viel "spezieller" und "komplexer" alsint addOne(int) {
...Antworten:
Ich denke, der Grund dafür ist, dass die meisten populären Sprachen entweder aus der C-Sprachfamilie stammen oder von dieser beeinflusst wurden, im Gegensatz zu funktionalen Sprachen und ihrer Wurzel, der Lambda-Rechnung.
Und in diesen Sprachen sind Funktionen nicht nur ein weiterer Wert:
const
,readonly
oderfinal
Mutation verbieten), aber Funktionen nicht neu zugeordnet werden können.Aus technischer Sicht sind Code (der aus Funktionen besteht) und Daten getrennt. Sie belegen normalerweise verschiedene Teile des Speichers und auf sie wird unterschiedlich zugegriffen: Code wird einmal geladen und dann nur ausgeführt (aber nicht gelesen oder geschrieben), wohingegen Daten häufig ständig zugewiesen und freigegeben werden und geschrieben und gelesen, aber nie ausgeführt werden.
Und da C "nah am Metall" sein sollte, ist es sinnvoll, diese Unterscheidung auch in der Syntax der Sprache widerzuspiegeln.
Der Ansatz "Funktion ist nur ein Wert", der der funktionalen Programmierung zugrunde liegt, hat in den gängigen Sprachen erst seit relativ kurzer Zeit an Bedeutung gewonnen, wie die späte Einführung von Lambdas in C ++, C # und Java (2011, 2007, 2014) zeigt.
quelle
Es ist wichtig, dass der Mensch erkennt, dass Funktionen nicht nur "eine andere benannte Entität" sind. Manchmal ist es sinnvoll, sie als solche zu manipulieren, aber sie sind immer noch auf einen Blick zu erkennen.
Es spielt keine Rolle, was der Computer über die Syntax denkt, da ein unverständlicher Klumpen von Zeichen für eine Maschine in Ordnung ist, um sie zu interpretieren, aber für den Menschen wäre es nahezu unmöglich, dies zu verstehen und aufrechtzuerhalten.
Es ist wirklich derselbe Grund, warum wir while- und for-Schleifen, switch- und if-else-Anweisungen usw. haben, obwohl alle letztendlich auf einen Vergleichs- und Sprungbefehl hinauslaufen. Der Grund ist, dass es zum Wohle der Menschen da ist, die den Code pflegen und verstehen.
Wenn Sie Ihre Funktionen als "eine andere benannte Entität" haben, wie Sie es vorschlagen, wird es schwieriger, Ihren Code zu sehen und damit zu verstehen.
quelle
whatsisname
betraf eher den ersten Punkt (und warnte vor einer gewissen Gefahr, diese ausfallsicheren Punkte zu entfernen), während sich Ihr Kommentar eher auf den zweiten Teil der Frage bezog. Es ist in der Tat möglich, diese Syntax zu ändern (und wie Sie beschrieben haben, wurde dies bereits oft getan ...), aber sie passt nicht zu jedem (da oop auch nicht zu jedem passt.Es könnte Sie interessieren, zu erfahren, dass eine Sprache namens ALGOL 68 schon in prähistorischer Zeit eine Syntax verwendet hat, die der von Ihnen vorgeschlagenen nahe kommt. Wenn Sie erkennen, dass Funktionsbezeichner genau wie andere Bezeichner an Werte gebunden sind, können Sie in dieser Sprache eine Funktion (Konstante) mithilfe der Syntax deklarieren
Konkret würde dein Beispiel lauten
In Anbetracht der Redundanz, dass der ursprüngliche Typ aus dem RHS der Deklaration abgelesen werden kann und ein Funktionstyp immer mit beginnt
PROC
, könnte (und würde) dies vertraglich geregelt werdenbeachte aber, dass das
=
noch vor der parameterliste kommt. Beachten Sie auch, dass Sie, wenn Sie eine Funktionsvariable möchten (der später ein anderer Wert desselben Funktionstyps zugewiesen werden könnte),=
diese durch:=
eine der folgenden ersetzen sollten :In diesem Fall sind jedoch beide Formen tatsächlich Abkürzungen; Da der
func var
Bezeichner einen Verweis auf eine lokal erzeugte Funktion bezeichnet, wäre die vollständig erweiterte FormDiese spezielle syntaktische Form ist leicht zu gewöhnen, hatte aber in anderen Programmiersprachen eindeutig keine große Anhängerschaft. Auch funktionale Programmiersprachen wie Haskell bevorzugen den Stil
f n = n+1
mit=
folgenden in der Parameterliste. Ich denke, der Grund ist hauptsächlich psychologischer Natur. Schließlich bevorzugen selbst Mathematiker nicht oft, wie ich es tue, f = n ⟼ n + 1 gegenüber f ( n ) = n + 1.Übrigens macht die obige Diskussion einen wichtigen Unterschied zwischen Variablen und Funktionen deutlich: Funktionsdefinitionen binden normalerweise einen Namen an einen bestimmten Funktionswert, der später nicht geändert werden kann, wohingegen Variablendefinitionen normalerweise einen Bezeichner mit einem Anfangswert einführen , aber einen solchen kann sich später ändern. (Dies ist keine absolute Regel. Funktionsvariablen und Nichtfunktionskonstanten kommen in den meisten Sprachen vor.) Außerdem ist der in einer Funktionsdefinition gebundene Wert in kompilierten Sprachen normalerweise eine Konstante zur Kompilierungszeit, sodass Aufrufe der Funktion möglich sind kompiliert mit einer festen Adresse im Code. In C / C ++ ist dies sogar eine Voraussetzung; das Äquivalent des ALGOL 68
kann nicht in C ++ geschrieben werden, ohne einen Funktionszeiger einzuführen. Diese Art von spezifischen Einschränkungen rechtfertigen die Verwendung einer anderen Syntax für Funktionsdefinitionen. Sie hängen jedoch von der Sprachsemantik ab, und die Rechtfertigung gilt nicht für alle Sprachen.
quelle
Sie haben Java und Scala als Beispiele genannt. Sie haben jedoch eine wichtige Tatsache übersehen: Das sind keine Funktionen, das sind Methoden. Methoden und Funktionen unterscheiden sich grundlegend . Funktionen sind Objekte, Methoden gehören zu Objekten.
In Scala, das sowohl Funktionen als auch Methoden enthält, gibt es die folgenden Unterschiede zwischen Methoden und Funktionen:
Ihr vorgeschlagener Ersatz funktioniert also einfach nicht, zumindest in diesen Fällen.
quelle
Die Gründe, die mir einfallen, sind:
quelle
Umgekehrt: Wenn Sie nicht daran interessiert sind, den Quellcode auf einem Computer mit extremen RAM-Einschränkungen zu bearbeiten oder die Zeit zum Lesen von einer Diskette zu minimieren, was ist dann falsch an der Verwendung von Schlüsselwörtern?
Sicherlich ist es besser zu lesen
x=y+z
alsstore the value of y plus z into x
, aber das bedeutet nicht, dass Interpunktionszeichen von Natur aus "besser" sind als Stichwörter. Wenn Variableni
,j
undk
sindInteger
undx
sindReal
, berücksichtigen Sie die folgenden Zeilen in Pascal:In der ersten Zeile wird eine ganze Zahl abgeschnitten, in der zweiten eine reelle Zahl. Die Unterscheidung kann gut gemacht werden, weil Pascal
div
als Operator für die Ganzzahltrennung verwendet, anstatt zu versuchen, ein Interpunktionszeichen zu verwenden, das bereits einen anderen Zweck hat (reelle Zahlenteilung).Während es einige Kontexte gibt, in denen es hilfreich sein kann, eine Funktionsdefinition kurz zu fassen (z. B. ein Lambda, das als Teil eines anderen Ausdrucks verwendet wird), sollen Funktionen im Allgemeinen hervorstechen und als Funktionen leicht visuell erkennbar sein. Obwohl es möglich sein könnte, die Unterscheidung viel subtiler zu gestalten und nur Interpunktionszeichen zu verwenden, worum geht es dann? Das Aussprechen
Function Foo(A,B: Integer; C: Real): String
macht deutlich, wie die Funktion heißt, welche Parameter sie erwartet und was sie zurückgibt. Vielleicht könnte man es durch ErsetzenFunction
durch Interpunktionszeichen um sechs oder sieben Zeichen verkürzen , aber was würde man gewinnen?Zu beachten ist auch, dass in den meisten Frameworks ein grundlegender Unterschied besteht zwischen einer Deklaration, die immer einen Namen entweder mit einer bestimmten Methode oder einer bestimmten virtuellen Bindung verknüpft, und einer Deklaration, die eine Variable erstellt, die zunächst eine bestimmte Methode oder Bindung identifiziert, jedoch könnte zur Laufzeit geändert werden , um einen anderen zu identifizieren. Da dies in den meisten prozeduralen Frameworks semantisch sehr unterschiedliche Konzepte sind, ist es sinnvoll, dass sie eine unterschiedliche Syntax haben.
quelle
void f() {}
tatsächlich kürzer ist als das Lambda-Äquivalent in C ++ (auto f = [](){};
), C # (Action f = () => {};
) und Java (Runnable f = () -> {};
). Die Prägnanz von Lambdas beruht auf Typinferenz und Auslassenreturn
, aber ich denke nicht, dass dies mit dem zusammenhängt, was diese Frage stellt.Nun, der Grund könnte sein, dass diese Sprachen sozusagen nicht funktionsfähig genug sind. Mit anderen Worten, Sie definieren Funktionen eher selten. Daher ist die Verwendung eines zusätzlichen Schlüsselworts akzeptabel.
In den Sprachen des ML- oder Miranda-Erbes OTOH definieren Sie die meiste Zeit Funktionen. Sehen Sie sich zum Beispiel einen Haskell-Code an. Es ist buchstäblich meist eine Folge von Funktionsdefinitionen, von denen viele lokale Funktionen und lokale Funktionen dieser lokalen Funktionen haben. Daher wäre ein unterhaltsames Schlüsselwort in Haskell ein Fehler, der ebenso groß ist wie die Anforderung einer Assingment-Anweisung in einer zwingenden Sprache, um mit der Zuweisung zu beginnen . Ursache Zuordnung ist wahrscheinlich die mit Abstand häufigste Aussage.
quelle
Persönlich sehe ich keinen schwerwiegenden Fehler in Ihrer Idee; Möglicherweise stellen Sie fest, dass es schwieriger ist, bestimmte Dinge mit Ihrer neuen Syntax auszudrücken, als Sie erwartet hatten, und / oder Sie müssen sie möglicherweise überarbeiten (indem Sie verschiedene Sonderfälle und andere Funktionen usw. hinzufügen), aber ich bezweifle, dass Sie sich selbst finden die Idee ganz aufgeben zu müssen.
Die von Ihnen vorgeschlagene Syntax ähnelt mehr oder weniger einer Variante einiger der Notationsstile, die manchmal zum Ausdrücken von Funktionen oder Funktionstypen in der Mathematik verwendet werden. Dies bedeutet, dass es, wie alle Grammatiken, wahrscheinlich einige Programmierer mehr ansprechen wird als andere. (Als Mathematiker mag ich es zufällig.)
Sie sollten jedoch beachten , dass in den meisten Sprachen, die
def
-Stil Syntax (dh die traditionelle Syntax) nicht verhalten sich anders als ein Standard - Variablenzuweisung.C
undC++
-Familie werden Funktionen im Allgemeinen nicht als "Objekte" behandelt, dh als Teile von eingegebenen Daten, die kopiert und auf den Stapel gelegt werden sollen und so weiter. (Ja, Sie können Funktionszeiger haben, aber diese zeigen immer noch auf ausführbaren Code, nicht auf "Daten" im typischen Sinne.)self
(was übrigens eigentlich kein Schlüsselwort ist; Sie können jeden gültigen Bezeichner zum ersten Argument einer Methode machen).Sie müssen überlegen, ob Ihre neue Syntax genau (und hoffentlich intuitiv) das darstellt, was der Compiler oder Interpreter tatsächlich tut. Es kann hilfreich sein, sich beispielsweise über den Unterschied zwischen Lambdas und Methoden in Ruby zu informieren. Auf diese Weise erhalten Sie eine Vorstellung davon, wie sich Ihr Funktions-Daten-Paradigma vom typischen OO / prozeduralen Paradigma unterscheidet.
quelle
Für einige Sprachen sind Funktionen keine Werte. In so einer Sprache das zu sagen
ist eine Funktionsdefinition, wohingegen
deklariert eine Konstante, ist verwirrend, weil Sie eine Syntax verwenden, um zwei Dinge zu bedeuten.
Andere Sprachen wie ML, Haskell und Scheme behandeln Funktionen als Werte der ersten Klasse, bieten dem Benutzer jedoch eine spezielle Syntax zum Deklarieren von Konstanten mit Funktionswerten. * Sie wenden die Regel an, dass "Verwendung die Form verkürzt". Dh wenn ein Konstrukt sowohl allgemein als auch ausführlich ist, sollten Sie dem Benutzer eine Kurzform geben. Es ist unelegant, dem Benutzer zwei verschiedene Syntaxen zu geben, die genau dasselbe bedeuten. manchmal sollte Eleganz dem Nutzen geopfert werden.
Wenn Funktionen in Ihrer Sprache erstklassig sind, warum dann nicht versuchen, eine Syntax zu finden, die so präzise ist, dass Sie nicht versucht sind, einen syntaktischen Zucker zu finden?
- Bearbeiten -
Ein weiteres Thema, das (noch) niemand angesprochen hat, ist die Rekursion. Wenn du erlaubst
und du erlaubst
Daraus folgt, dass Sie zulassen sollten
In einer faulen Sprache (wie Haskell) gibt es hier kein Problem. In einer Sprache ohne statische Überprüfungen (wie LISP) gibt es hier keine Probleme. In einer statisch überprüften Sprache müssen Sie jedoch vorsichtig sein, wie die Regeln für die statische Überprüfung definiert sind, wenn Sie die ersten beiden zulassen und die letzte verbieten möchten.
- Ende der Bearbeitung -
* Es könnte argumentiert werden, dass Haskell nicht in diese Liste gehört. Es gibt zwei Möglichkeiten, eine Funktion zu deklarieren, aber beide sind in gewissem Sinne Verallgemeinerungen der Syntax zum Deklarieren von Konstanten anderer Typen
quelle
Dies kann in dynamischen Sprachen hilfreich sein, in denen der Typ nicht so wichtig ist, in statisch typisierten Sprachen jedoch nicht so gut lesbar ist, wenn Sie immer den Typ Ihrer Variablen kennen möchten. Außerdem ist es in objektorientierten Sprachen ziemlich wichtig, den Typ Ihrer Variablen zu kennen, um zu wissen, welche Operationen unterstützt werden.
In Ihrem Fall wäre eine Funktion mit 4 Variablen:
Wenn ich mir den Funktionsheader ansehe und sehe (x, y, z, s), kenne ich aber die Typen dieser Variablen nicht. Wenn ich wissen will,
z
welcher Typ der dritte Parameter ist, muss ich mir den Anfang der Funktion ansehen und mit der Zählung von 1, 2, 3 beginnen und dann sehen, dass der Typ istdouble
. Auf die erstere Art schaue ich direkt und sehedouble z
.quelle
var addOne = (int x, long y, double z, String s) => { x + 1 }
in einer nicht-schwachsinnigen, statisch typisierten Sprache Ihrer Wahl schreiben können (Beispiele: C #, C ++, Scala). Auch die sonst sehr eingeschränkte lokale Typinferenz von C # ist dafür völlig ausreichend. Diese Antwort kritisiert lediglich eine bestimmte Syntax, die an erster Stelle zweifelhaft ist und nirgendwo verwendet wird (obwohl Haskells Syntax ein sehr ähnliches Problem hat).Es gibt einen sehr einfachen Grund für eine solche Unterscheidung in den meisten Sprachen: Es muss zwischen Bewertung und Erklärung unterschieden werden . Ihr Beispiel ist gut: Warum nicht Variablen mögen? Nun, Variablenausdrücke werden sofort ausgewertet.
Haskell hat ein spezielles Modell, bei dem es keinen Unterschied zwischen Auswertung und Deklaration gibt, weshalb kein spezielles Schlüsselwort erforderlich ist.
quelle
Funktionen werden in den meisten Sprachen anders deklariert als Literale, Objekte usw., da sie unterschiedlich verwendet, unterschiedlich getestet und mit unterschiedlichen potenziellen Fehlerquellen behaftet sind.
Wenn einer Funktion eine dynamische Objektreferenz oder ein veränderbares Objekt übergeben wird, kann die Funktion den Wert des Objekts während der Ausführung ändern. Diese Art von Nebeneffekt kann es schwierig machen, die Funktionsweise einer Funktion zu verfolgen, wenn sie in einem komplexen Ausdruck verschachtelt ist. Dies ist ein häufiges Problem in Sprachen wie C ++ und Java.
Erwägen Sie das Debuggen einer Art Kernelmodul in Java, bei dem jedes Objekt eine toString () - Operation hat. Es ist zwar zu erwarten, dass die Methode toString () das Objekt wiederherstellt, das Objekt muss jedoch möglicherweise zerlegt und neu zusammengesetzt werden, um seinen Wert in ein String-Objekt zu übersetzen. Wenn Sie versuchen, die Methoden zu debuggen, die toString () aufruft (in einem Hook-and-Template-Szenario), um seine Arbeit zu erledigen, und versehentlich das Objekt im Variablenfenster der meisten IDEs markieren, kann dies zum Absturz des Debuggers führen. Dies liegt daran, dass die IDE versucht, das Objekt, das genau den Code aufruft, den Sie gerade debuggen, an String () zu übergeben. Kein primitiver Wert macht jemals so einen Mist, weil die semantische Bedeutung primitiver Werte von der Sprache und nicht vom Programmierer bestimmt wird.
quelle