Hat sich jemals jemand damit beschäftigt, eine vollständig asynchrone Programmiersprache zu entwickeln? Ich habe dieses Konzept schon seit einer Weile im Kopf. In diesem Post möchte ich eine konkrete Idee ausformulieren.
Zu lösende Probleme
In allen aktuellen nicht-funktionalen Programmiersprachen (z.B. C, C++, Python, Java, Javascript, Rust, Go) wird Code sequenziell (in der Regel Zeile für Zeile) ausgeführt. Moderne Prozessoren dagegen sind gut darin, viele Operationen gleichzeitig auszuführen. Noch dazu sind moderne Programme dominiert von Operationen, die von viel langsameren Drittsystemen abhängig sind, wie Speicherzugriffe oder sogar Netzwerk-Kommunikation. Daher ist es für eine effiziente Nutzung einer modernen CPU-Architektur notwendig, viele Prozesse gleichzeitig (asynchron) auszuführen.
Es gibt auch Diskussionen über das Problem der „gefärbten Funktionen". Viele heutige Programmiersprachen (wie JavaScript und Rust) haben das Async/Await-Paradigma übernommen, das im Wesentlichen eine Möglichkeit ist, auszudrücken, dass bestimmte Funktionen schlafen gehen und andere Funktionen laufen gelassen werden können, während sie auf Daten aus einer langsamen Quelle warten. Das ist großartig und hilft sehr bei der Optimierung der CPU-Auslastung, aber es führt zu dem Problem, dass man nun zwei Arten von Funktionen hat und eine Reihe von Regeln, wie sie sich gegenseitig aufrufen und nicht aufrufen können.
Ich glaube, es gibt einen Weg, all diese Probleme auf einmal zu lösen: Es komfortabler zu machen, mit Futures zu arbeiten (auch bekannt als Promises in JavaScript, also die Datentypen, die Programmiersprachen verwenden, um mit Dingen umzugehen, die noch in der Entstehung oder Lieferung sind).
Die Idee
Es soll möglich sein, tatsächlich mit Futures zu arbeiten. Im Moment kann man mit einer Future in allen mir bekannten Sprachen entweder darauf warten (await) oder Verkettung verwenden (getUser().then(user => user.email)), um neue Futures aus dem eigenen Future abzuleiten.
Was wäre, wenn letzteres Beispiel viel einfacher wäre? Was wäre, wenn ich einfach folgendes schreiben könnte:
const user = getUser()
const email = await user.emailUnd was wäre, wenn ich nicht alles ständig awaiten müsste und einfach die meisten Funktionen aufrufen könnte, indem ich ein Future genauso übergebe, wie ich normalerweise einen Wert übergeben würde?
const currentUser = auth()
const ownProjects = database.getProjects().whereUserIs(currentUser.id)
const posts = database.getLatestPosts()
return (
<Layout>
<Header avatar={currentUser.imageUrl} />
<Sidebar>
<ul>
{ownProjects.map(project => <li>{project.name}</li>)}
</ul>
</Sidebar>
<Timeline posts={posts} />
</Layout>
)Dies ist ein UI-Beispiel und der große Vorteil ist, dass bei dieser Anwendung die UI-Bibliothek die Verantwortung übernehmen kann, so viel von der UI wie möglich vorab aufzubauen und fehlende Werte zu ersetzen, sobald alle asynchronen Operationen aufgelöst sind.
Ich denke, das funktioniert auch gut für Data-Pipelining-Operationen:
const projects = getProjects() // Future<Project[]>
const users = getUsers() // Future<User[]>
const clients = getClients() // Future<Client[]>
const projectMembersByClient = clients.map(client => [
client.id,
projects
.filter(project => project.clientId === client.id)
.map(project => users.filter(user => project.memberIds.includes(user.id)))
]) // Future<[string, User[]][]>Was das bedeutet
Um dieses Muster konsequent umzusetzen, müsste man eine Programmiersprache erstellen, die es nativ unterstützt. Diese Sprache müsste einen eingebauten Future-Typ haben, der automatisch alle Felder und Methoden des enthaltenen Wertes besitzt, mit dem Unterschied, dass sie alle Futures zurückgeben.
Es würde zu einem Programmierstil führen, der mit der Annahme bricht, dass Code in der Reihenfolge ausgeführt wird, in der er geschrieben ist. Es würde Code fördern, der deklarativer ist. Es baut einen Ausführungsgraphen (eine lange Kette von Futures) auf, der zu den spezifischen Werten führt, die das Programm berechnen möchte, anstatt auf den Abschluss lang laufender Operationen zu warten, bevor mit den nächsten Schritten fortgefahren wird.
Mit dieser Art von Future würden Future- und Wert-Typen austauschbar werden. Eine Funktion
function calculateTax(person: { income: number; taxRate: number }) {
return person.income * person.taxRate
}könnte ein Future<Person> genauso verarbeiten wie eine Person, und der Compiler oder die Laufzeitumgebung der Sprache könnte sich darum kümmern, ob die Operationen synchron oder asynchron sind. Daher wäre es nicht mehr sinnvoll, Funktionen explizit als async zu deklarieren, womit das Problem der gefärbten Funktionen gelöst wäre. Was übrig bliebe, ist ein await-Operator, der es dem Programmierer ermöglicht, das Programm zu zwingen, auf einen bestimmten Wert zu warten, bevor die Ausführung fortgesetzt wird.
Am Ende dieser Ketten (die manchmal über Maschinen / HTTP-Anfragen etc. hinweglaufen) gibt es meist irgendeine Art von UI-Komponente, die die Ergebnisse einem Menschen zeigt. UI-Frameworks wie React könnten dann das eigentliche Warten auf die Daten übernehmen und sich um das korrekte Rendern eines deklarativ definierten Ladezustands kümmern, und so dem/der Entwickler:in die Last der Orchestrierung abnehmen.
Ausprobieren
Obwohl meines Wissens keine Sprache existiert, die dies tut (Haskell kommt wahrscheinlich am nächsten), ist es möglich, mit TypeScript auf halbem Weg dorthin zu gelangen. Ich habe eine Proof-of-Concept-Bibliothek namens Lacy implementiert, die dieses Muster umsetzt. Sie wird folgendermaßen verwendet:
const users = lacy(getUsers() /* Promise<User[]> */ ) /* LacyPromise<User[]> */
const userNames = users.map(user => user.name) /* LacyPromise<string[]> */
const userNamesAsRegularPromise = userNames.$ /* Promise<string[]> */
const userNamesAsPlainString = await userNames /* string[] */Zusammenfassung
Die Idee einer wirklich asynchronen Programmierung ist bisher eher ein Gedankenexperiment als eine ausgereifte Lösung – aber ich denke, sie zeigt eine Richtung, die es wert ist, weiterverfolgt zu werden.
Das Async/Await-Modell, das wir heute haben, ist eine große Verbesserung gegenüber Callbacks und Threads, zwingt Entwickler:innen aber immer noch dazu, in sequenziellen Abläufen zu denken. Eine Sprache oder Laufzeitumgebung, die Futures als erstklassige Werte behandelt und synchronen und asynchronen Code wirklich austauschbar macht, könnte uns von diesem mentalen Overhead vollständig befreien.
Wer das Konzept interessant findet oder damit experimentieren möchte, kann lacy ausprobieren – ich freue mich über Feedback.
