Ist es möglich, gleichzeitig currying und variadic Funktion zu haben?

13

Ich denke darüber nach, sowohl Currying- als auch Variadic-Funktionen in einer dynamisch typisierten funktionalen Programmiersprache verfügbar zu machen, aber ich frage mich, ob dies möglich ist oder nicht.

Hier sind einige Pseudocodes:

sum = if @args.empty then 0 else @args.head + sum @args.tail

das soll angeblich alle seine Argumente zusammenfassen. Wenn dann sumselbst eine Zahl behandelt wird, dann ist das Ergebnis 0. beispielsweise,

sum + 1

ist gleich 1, vorausgesetzt, dass +nur mit Zahlen gearbeitet werden kann. Auch wenn dies sum == 0zutrifft, sumbleiben Wert und Funktionseigenschaft erhalten, egal wie viele Argumente gleichzeitig angegeben werden (daher "teilweise angewendet" und "variadisch"), zum Beispiel, wenn ich dies erkläre

g = sum 1 2 3

dann gist gleich 6aber wir können uns noch weiter bewerben g. Zum Beispiel g 4 5 == 15ist wahr. In diesem Fall können wir das Objekt nicht gdurch ein Literal ersetzen 6, da sie zwar denselben Wert ergeben, wenn sie als Ganzzahl behandelt werden, aber unterschiedliche Codes enthalten.

Wenn dieses Design in einer echten Programmiersprache verwendet wird, führt es dann zu Verwirrung oder Mehrdeutigkeit?

Michael Tsang
quelle
1
Streng genommen bedeutet die Verwendung von Currying als Grundlage einer Sprache, dass alle Funktionen unär sind - es gibt nicht nur keine variablen Funktionen, es gibt nicht einmal binäre Funktionen! Programme in dieser Sprache sehen jedoch immer noch so aus, als hätten sie mehrere Argumente, und das gilt für verschiedene Funktionen genauso wie für normale.
Kilian Foth
Dann lautet meine vereinfachte Frage: "Kann ein Objekt gleichzeitig eine Funktion und ein Nichtfunktionswert sein?" Im obigen Beispiel sumist 0ohne Argument und ruft sich rekursiv mit einem Argument auf.
Michael Tsang
Ist das nicht der Job von reduce?
Ratschenfreak
1
Werfen Sie einen Blick auf die Funktionen , die Sie verwenden auf args: empty, head, und tail. Dies sind alles Listenfunktionen, was darauf hindeutet, dass es möglicherweise einfacher und unkomplizierter ist, eine Liste zu verwenden, in der sich die verschiedenen Funktionen befinden. (Also, sum [1, 2, 3]anstatt sum 1 2 3)
Michael Shaw

Antworten:

6

Wie können Varargs implementiert werden? Wir brauchen einen Mechanismus, um das Ende der Argumentliste zu signalisieren. Dies kann entweder sein

  • ein spezieller Terminatorwert, oder
  • Die Länge der Vararg-Liste, die als zusätzlicher Parameter übergeben wird.

Beide Mechanismen können im Kontext des Lehrgangs zum Implementieren von Varargs verwendet werden, aber die richtige Typisierung wird zu einem Hauptproblem. Nehmen wir an, wir haben es mit einer Funktion zu tun, mit der sum: ...int -> intAusnahme, dass diese Funktion Currying verwendet (wir haben also einen ähnlichen Typ sum: int -> ... -> int -> int, außer dass wir die Anzahl der Argumente nicht kennen).

Case: terminator value: Sei endder spezielle Terminator und Tder Typ von sum. Wir wissen jetzt , dass angewendet enddie Funktion zurückkehrt: sum: end -> intund das in einem int wir eine weitere Summe ähnliche Funktion erhalten angewandt: sum: int -> T. Daher Tist die Vereinigung dieser Art: T = (end -> int) | (int -> T). Durch substituierende T, erhalten wir verschiedene mögliche Typen wie end -> int, int -> end -> int, int -> int -> end -> intusw. Allerdings sind die meisten Typ Systeme beherbergen nicht solche Typen.

Fall: explizite Länge: Das erste Argument für eine vararg-Funktion ist die Anzahl der varargs. So sum 0 : int, sum 1 : int -> int, sum 3 : int -> int -> int -> intusw. Dies ist in einigen Typ - Systemen unterstützt und ist ein Beispiel für abhängige Typisierung . Eigentlich wäre die Anzahl der Argumente ein Typparameter sein und keine regulärer Parameter - es keinen Sinn für die arity der Funktion auf einem Laufzeitwert abhängig machen würde, s = ((sum (floor (rand 3))) 1) 2ist offensichtlich schlecht getippt: diese auswertet , um entweder s = ((sum 0) 1) 2 = (0 1) 2, s = ((sum 1) 1) 2 = 1 2oder s = ((sum 2) 1) 2 = 3.

In der Praxis sollte keine dieser Techniken verwendet werden, da sie fehleranfällig sind und in herkömmlichen Typsystemen keinen (aussagekräftigen) Typ haben. Stattdessen nur eine Liste von Werten als eine Paramter übergeben: sum: [int] -> int.

Ja, ein Objekt kann sowohl als Funktion als auch als Wert auftreten, z. B. in einem Typensystem mit Zwängen. Sei suma SumObj, das zwei Zwänge hat:

  • coerce: SumObj -> int -> SumObjerlaubt sum, als eine Funktion verwendet zu werden, und
  • coerce: SumObj -> int ermöglicht es uns, das Ergebnis zu extrahieren.

Technisch gesehen ist dies eine Variation des obigen Terminatorwert-Falls, wobei T = SumObjund coerceein Unwrapper für den Typ ist. In vielen objektorientierten Sprachen ist dies mit einer Überladung von Operatoren trivial umsetzbar, zB C ++:

#include <iostream>
using namespace std;

class sum {
  int value;
public:
  explicit sum() : sum(0) {}
  explicit sum(int x) : value(x) {}
  sum operator()(int x) const { return sum(value + x); }  // function call overload
  operator int() const { return value; } // integer cast overload
};

int main() {
  int zero = sum();
  cout << "zero sum as int: " << zero << '\n';
  int someSum = sum(1)(2)(4);
  cout << "some sum as int: " << someSum << '\n';
}
amon
quelle
Geniale Antwort! Der Nachteil beim Packen von Variationen in eine Liste besteht darin, dass Sie die teilweise Anwendung des Currys verlieren. Ich habe mit einer Python-Version Ihres Terminator-Ansatzes gespielt und ein Schlüsselwortargument verwendet ..., force=False), um die Anwendung der Anfangsfunktion zu erzwingen.
ThomasH
Sie können eine eigene Funktion höherer Ordnung erstellen, die teilweise eine Funktion anwendet, die eine Liste annimmt, wie z curryList : ([a] -> b) -> [a] -> [a] -> b, curryList f xs ys = f (xs ++ ys).
Jack
2

Möglicherweise möchten Sie diese Implementierung von printf in Haskell zusammen mit dieser Beschreibung der Funktionsweise betrachten . Auf der letzten Seite finden Sie einen Link zu Oleg Kiselyovs Artikel über diese Art von Dingen, der ebenfalls lesenswert ist. Wenn Sie eine funktionale Sprache entwerfen, sollte die Website von Oleg wahrscheinlich obligatorisch sein.

Meiner Meinung nach sind diese Ansätze ein bisschen hacken, aber sie zeigen, dass es möglich ist. Wenn Ihre Sprache jedoch voll abhängig von der Eingabe ist, ist dies viel einfacher. Eine variable Funktion zum Summieren ihrer Ganzzahlargumente könnte dann ungefähr so ​​aussehen:

type SumType = (t : union{Int,Null}) -> {SumType, if t is Int|
                                         Int,     if t is Null}
sum :: SumType
sum (v : Int) = v + sum
sum (v : Null) = 0

Eine Abstraktion zum Definieren des rekursiven Typs, ohne dass ein expliziter Name angegeben werden muss, kann das Schreiben solcher Funktionen erleichtern.

Bearbeiten: Natürlich habe ich die Frage gerade noch einmal gelesen und Sie sagten eine dynamisch getippte Sprache, bei der die Typmechanik offensichtlich nicht wirklich relevant ist, und daher enthält die Antwort von @ amon wahrscheinlich alles, was Sie brauchen. Na ja, ich lasse das hier, falls jemand darauf stößt, während er sich fragt, wie es in einer statischen Sprache gemacht wird ...

Jules
quelle
0

Hier ist eine Version zum Aufrufen verschiedener Funktionen in Python3, die den "Terminator" -Ansatz von @amon verwendet, indem sie die optionalen Argumente von Python ausnutzt:

def curry_vargs(g):
    actual_args = []
    def f(a, force=False):
        nonlocal actual_args
        actual_args.append(a)
        if force:
            res = g(*actual_args)
            actual_args = []
            return res
        else:
            return f
    return f

def g(*args): return sum(args)
f = curry_vargs(g)
f(1)(2)(3)(4,True) # => 10

Die zurückgegebene Funktion fsammelt Argumente, die in aufeinanderfolgenden Aufrufen an sie übergeben wurden, in einem Array, das im äußeren Gültigkeitsbereich gebunden ist. Nur wenn das forceArgument wahr ist, wird die ursprüngliche Funktion mit allen bisher gesammelten Argumenten aufgerufen.

Ein Nachteil dieser Implementierung ist, dass Sie immer ein erstes Argument übergeben müssen, fdamit Sie kein "thunk" erstellen können, eine Funktion, bei der alle Argumente gebunden sind und nur mit der leeren Argumentliste aufgerufen werden können (aber ich denke, dies stimmt überein mit die typische Umsetzung von Curry).

Eine weitere Einschränkung ist, dass Sie nach dem Übergeben eines falschen Arguments (z. B. eines falschen Typs) die ursprüngliche Funktion erneut abarbeiten müssen. Es gibt keine andere Möglichkeit, das interne Array zurückzusetzen. Dies erfolgt erst nach erfolgreicher Ausführung der Curry-Funktion.

Ich weiß nicht, ob Ihre vereinfachte Frage "Kann ein Objekt gleichzeitig eine Funktion und ein Nichtfunktionswert sein?" In Python implementiert werden kann, da ein Verweis auf eine Funktion ohne Klammern das interne Funktionsobjekt auswertet . Ich weiß nicht, ob dies gebogen werden kann, um einen beliebigen Wert zurückzugeben.

In Lisp wäre es wahrscheinlich einfach, da Lisp-Symbole gleichzeitig einen Wert und einen Funktionswert haben können. Der Funktionswert wird einfach ausgewählt, wenn das Symbol an der Funktionsposition erscheint (als erstes Element in einer Liste).

ThomasH
quelle