11 Informatische Grundbegriffe
Nach allgemeinem Sprachgebrauch (vgl. Wikipedia) handelt es sich bei der Informatik um die Wissenschaft der systematischen Darstellung, Speicherung, Verarbeitung und Übertragung von Informationen, wobei besonders die automatische Informationsverarbeitung mit Computern betrachtet wird. Die Informatik ist dabei zugleich Grundlagen- und Formalwissenschaft als auch Ingenieurdisziplin.
Zentrale Komponenten der Informatik sind zum einen die Computer, zum anderen Algorithmen und Programme. Computer sind Maschinen, die Daten speichern und einfache Datenoperationen ausführen können. Diese einfachen Operationen führen sie mit extrem hoher Geschwindigkeit aus. Die Universalität von Computern beruht darauf, dass sie sowohl Daten als auch Programme speichern können.
Programme sind in einer Programmiersprache formulierte Algorithmen. Algorithmen wiederum sind Folgen von Anweisungen, die bestimmte Operationen vorschreiben. Bei Algorithmen unterscheidet man verschiedene Ebenen: die Beschreibungsebene, wie man sie beispielsweise in Form eines Kochrezepts, einer Bauanleitung oder eines Datenanalyseskripts findet; die Anweisungen eines Algorithmus, wie “Mehl und Wasser vermengen”, die bildhafte Darstellung des Zusammenschraubens zweier Teile oder die Anweisung x = c(1,2,3)
in R, und schließlich die Durchführungsebene, also der eigentliche Kochvorgang, der Zusammenbau eines Möbelstücks oder das Ausführen des Datenanalyseskripts.
Die Informatik gliedert sich in mehrere Teilgebiete, die auch für die Psychologie von Bedeutung sein können. Die Theoretische Informatik beschäftigt sich mit den Grundlagen der Berechnung, wie der Automatentheorie, der Berechenbarkeitstheorie und der Komplexitätstheorie. Die Angewandte Informatik befasst sich mit der Entwicklung und Nutzung von Anwendungssoftware, mit Fragen der Mensch-Computer-Interaktion sowie mit den gesellschaftlichen Auswirkungen der Informatik. Die Technische Informatik untersucht die Hardwareseite der Informatik, darunter die Mikroprozessortechnik, die Rechnerarchitektur und die Netzwerktechnik. Schließlich behandelt die Praktische Informatik die konkrete Umsetzung in Form von Programmierung, der Entwicklung und Analyse von Algorithmen sowie den Aufbau und die Nutzung von Datenbanken. Anwendung in der psychologischen Grundlagenforschung findet vor allem die praktische Informatik. Allerdings sind auch Fragen der Komplexitätstheorie und damit der theoretischen Informatik in der Entwicklung quantitativer psychologischer Theorien von Interesse (vgl. Van Rooij & Wareham (2012)) und die Mensch-Computer-Interaktion bildet eines der Themengebiete der modernen Arbeitspsychologie (vgl. Dahm (2005)).
Darüberhinaus gibt es zahlreiche Spezialgebiete der Informatik, die eng mit der Psychologie verbunden sind. Dazu gehört insbesondere natürlich der Bereich Maschinelles Lernen und Künstliche Intelligenz, der seine Ursrpünge in der quantitativen Kognitionspsychologie hat und es heutzutage ermöglicht, große Datenmengen automatisiert zu analysieren und Vorhersagen zu treffen. Ein weiteres wichtiges Feld ist die Computervisualistik, die sich mit Bilderkennung und Bildsynthese sowie mit der Gestaltung von Virtueller Realität (VR) und Augmented Reality (AR) beschäftigt. Diese Technologien nutzen einerseits Erkenntnisse der Wahrnehmungspschologie und eröffnen andererseits neue Möglichkeiten für Experimente, Therapien und Trainings in der Psychologie. Die Computerlinguistik trägt mit Verfahren der Spracherkennung und Sprachsynthese dazu bei, menschliche Kommunikation zu analysieren, zu modellieren und in Anwendungen wie Chatbots oder Assistenzsystemen nutzbar zu machen. Schließlich spielt für die Psychologie auch die Bioinformatik eine große Rolle, indem sie etwa Methoden zur Auswertung bildgebender Verfahren in der Medizin, die auch in der neuropsychologischen Forschung und Diagnostik genutzt werden, weiter entwickelt.
11.1 Rechnerarchitektur
Auch wenn wir die technischen Aspekte der Informatik hier nicht vertiefen wollen, ist es für die imperative Programmierung hilfreich, zumindest ein rudimentäres Wissen um den typischen physischen Aufbau, also die Hardware, eines Computers zu besitzen. Ein Computer besteht dabei aus mehreren wesentlichen Hardwarekomponenten, die zusammenarbeiten, um alle Rechen- und Speicheraufgaben zu bewältigen. Im Zentrum steht die Zentraleinheit, auch Hauptplatine, Motherboard oder Mainboard genannt. Sie ist die zentrale Leiterplatte, auf der alle wichtigen Bauteile wie Prozessor, Speicher und Anschlüsse miteinander verbunden sind.
Das Herzstück eines Computers ist die CPU (Central Processing Unit, auch Mikroprozessor), die das Rechenwerk, das Steuerwerk und das Leitwerk des Systems vereint. Sie übernimmt alle grundlegenden Berechnungen, steuert den Datenfluss und koordiniert die Abläufe. Zusätzlich verfügt sie über einen kleinen, aber sehr schnellen Cache, einen flüchtigen Speicher, der häufig benötigte Daten zwischenspeichert, um die Verarbeitung zu beschleunigen. Typische Namen von aktuellen CPUs sind Intels’ Core iX Prozessoren, AMD’s Ryzen Prozessoren oder Apple Silicon MX Prozessoren.
Ergänzt wird die CPU durch den RAM (Random Access Memory), den temporären, flüchtigen Arbeitsspeicher des Systems. Hier werden während der Laufzeit des Computers die aktuell benötigten Programme und Daten abgelegt, um schnellen Zugriff zu ermöglichen. Der RAM ist in seiner Kapazität begrenzt, in vielen modernen Systemen zum Beispiel auf 32 - 64 GB.
Für die dauerhafte Speicherung von Daten und Programmen dient der Massenspeicher, der stationäre Speicher des Computers. Heute werden dafür in der Regel SSDs (Solid State Drives) verwendet, die wesentlich schneller und robuster als herkömmliche magnet-basierte Festplatten sind. Auch Cloudspeicher wird zunehmend als Ergänzung oder Ersatz für lokale Massenspeicher genutzt.
Eine weitere wichtige Komponente ist die GPU (Graphics Processing Unit), ein leistungsstarker, speziell auf Visualisierungsaufgaben optimierter Prozessor. Die GPU entlastet die CPU bei grafikintensiven Anwendungen wie 3D-Rendering oder Videobearbeitung und spielt zudem eine wachsende Rolle bei rechenintensiven Aufgaben wie dem Training neuronaler Netze, bei denen sie die CPU effektiv unterstützen kann.
Die grundlegende funktionale Architektur moderner Computern geht zurück auf Von Neumann (1945) und wird entsprechend als Von Neumann-Architektur bezeichnet. Das entscheidende Charakteristikum der von Neumann-Architektur besteht darin, dass sowohl die Befehle (Programmanweisungen) als auch die Daten im selben Speicher abgelegt werden, und zwar in Form von Binärzahlen. Die Zentrale Verarbeitungseinheit (CPU) liest sowohl die Befehle als auch die Daten aus diesem gemeinsamen Speicher. Dadurch können Programme während der Ausführung dynamisch verändert oder sogar neu erzeugt werden, da sie im Speicher lediglich als Daten vorliegen.
Speziell beschreibt Von Neumann (1945) folgenden Rechneraufbau: Der Rechner setzt sich aus den grundlegenden Funktionseinheiten Steuerwerk, Rechenwerk, Speicher, Eingabewerk und Ausgabewerk zusammen. Programme und Daten werden gemeinsam in den Speicher eingegeben, sodass Daten, Programme sowie Zwischen- und Endergebnisse im gleichen Speicherbereich abgelegt sind. Der Speicher selbst ist in gleich große, nummerierte (adressierte) Zellen unterteilt, deren Inhalt über die jeweilige Adresse gezielt abgerufen oder verändert werden kann. Die Befehle eines Programms liegen typischerweise in aufeinanderfolgenden Speicherzellen, sodass das Steuerwerk den jeweils nächsten Befehl durch einfaches Erhöhen der Befehlsadresse um eins aufrufen kann. Dieses Prinzip der sequentiellen Abarbeitung der Befehle stellt dabei das Grundprinzip der imperativen Programmierung dar und ist somit auf engste mit der von Neumann-Architektur verknüpft. Durch spezielle Sprungbefehle ist es weiterhin möglich, von dieser festen Reihenfolge abzuweichen und an eine andere Stelle im Programm zu springen. Zu den grundlegenden Befehlen gehören dabei arithmetische Operationen wie Addition und Multiplikation, logische Vergleiche wie das logische UND oder ODER sowie Transportbefehle, mit denen Daten beispielsweise vom Eingabewerk in den Speicher oder vom Speicher ins Rechenwerk übertragen werden. Sämtliche Daten, also sowohl Befehle als auch Adressen, werden dabei binär codiert. Die binäre Enkodierung und Dekodierung der Informationen übernimmt dabei eine geeignete Schaltungstechnik im Rechner.
11.2 Algorithmen und Programme
Ein Realweltproblem bezeichnet das Problem, das mithilfe eines Computers gelöst werden soll. Ein typisches Beispiel hierfür ist die Auswertung von Fragebogendaten einer psychologischen Studie. Zur Bearbeitung eines solchen Problems erfolgt zunächst eine Problemspezifikation, also die genaue sprachliche Fassung des Realweltproblems, wie man sie etwa im Methodenteil einer wissenschaftlichen Publikation findet. Auf dieser Grundlage wird ein Algorithmus entworfen, also eine Folge von Anweisungen zur Lösung des Problems, beispielsweise das Einlesen der Daten, die Berechnung von deskriptiven Statistiken und die Durchführung eines statistischen Tests. Schließlich wird der Algorithmus in Form eines Programms implementiert, das heißt als in einer Programmiersprache verfasste Textdatei, die von einem Computer ausgeführt werden kann.
Definition 11.1 (Algorithmus) Ein Algorithmus ist eine Folge von Anweisungen, um aus gewissen Eingabedaten bestimmte Ausgabedaten herzuleiten, wobei folgende Bedingungen erfüllt sein müssen
- Finitheit. Die Anweisungsfolge ist in einem endlichen Text vollständig beschrieben.
- Effektivität. Jede Anweisung muss tatsächlich ausführbar sein.
- Terminierung. Der Algorithmus endet nach endlich vielen Anweisungen.
- Determiniertheit. Der Ablauf des Algorithmus ist zu jeder Zeit vorgeschrieben.
Wenn \(E\) die Menge der zulässigen Eingabedaten und \(A\) die Menge der zulässigen Ausgabedaten bezeichnet, dann ist ein Algorithmus also eine Funktion \[\begin{equation} f:E \to A, e \mapsto f(e) \end{equation}\] Umgekehrt heißen Funktionen, die durch einen Algorithmus beschrieben werden können, berechenbare Funktionen.
11.3 Programmiersprachen
Programmiersprachen legen die Regeln fest, denen ein Programm gehorchen muss. Sie definieren eine Syntax, also das Vokabular und den Aufbau eines Programms, sowie die Semantik, also die Bedeutung der erlaubten Anweisungen. Im Folgenden wollen wir einige Begriffsklärungen zur Kategorisierung verschiedener Formen von Programmiersprachen angeben. Dabei unterscheidet man Maschinensprache und sogenannte Höhere Programmiersprachen, verschiedene Generationen von Programmiersprachen, die hier im Vordergrund stehende Imperative Programmierung von der Deklarativen Programmierung, sowie kompilierte von interpretierten Programmiersprachen.
Maschinensprache und höhere Programmiersprachen
Maschinensprache besteht aus elementaren Operationsbefehlen wie Speichern, Vergleichen oder Addieren, die als Binärzahlen kodiert werden. Tabelle 11.1 zeigt einige Beispiele solcher Befehle:
Befehl | Binärcode |
---|---|
Addiere Inhalt R1 zu Inhalt R2 | 1001 0010 |
Erhöhe Inhalt R um 1 | 1001 0110 |
Übertrage Inhalt R1 nach R3 | 0010 0011 |
Programme, die in Maschinensprache geschrieben sind, nennt man Maschinenprogramme. De facto führt ein Computer ausschließlich Maschinenprogramme aus, da diese direkt von der Hardware verstanden werden. Für Menschen ist die Programmierung in Maschinensprache jedoch sehr mühselig, weshalb in der Praxis überwiegend höhere Programmiersprachen verwendet werden.
Höhere Programmiersprachen verwenden an die menschliche Sprache angelehnte Wörter und Sätze, um die Programmierung verständlicher und effizienter zu machen. Programme in höheren Programmiersprachen werden anschließend von einem Compiler oder einem Interpreter in Maschinensprache übersetzt, damit sie vom Computer ausgeführt werden können. Bekannte Beispiele für höhere Programmiersprachen sind FORTRAN, COBOL, C++, Python oder auch R.
Generationen von Programmiersprachen
Die Programmiersprachen der 1. Generation (1GL) sind die Maschinensprachen. Programme werden dabei direkt in Form von Binärzahlen geschrieben, etwa 10110000 01100001
, was in hexadezimaler Darstellung B0 61
entspricht. Solche Maschinensprachen wurden vor allem auf den ersten Röhren- und Relaiscomputern wie der ENIAC oder frühen IBM-Großrechnern der 1940er- und frühen 1950er-Jahre eingesetzt.
Mit der 2. Generation (2GL) entstanden ab den 1950er Jahren die Assemblersprachen als erste Form der symbolischen Programmierung. Hierbei werden menschenlesbare Kürzel verwendet, die jedoch noch prozessorspezifisch sind. Ein Beispiel für eine solche Intelprozessor-spezifische Anweisung ist MOV AL, 61H
. Diese Generation von Programmiersprachen wurde vor allem auf Großrechnern wie der IBM 704 oder frühen Mainframes der 1950er und 1960er Jahre genutzt, die bereits Transistoren statt Röhren verwendeten.
Ab den 1970er Jahren setzten sich die Programmiersprachen der 3. Generation (3GL) durch. Dazu zählen höhere Programmiersprachen wie FORTRAN, C, C++ und Java. Sie sind wesentlich programmierfreundlicher und vor allem prozessorunabhängig, was die Portabilität von Programmen deutlich verbessert. Solche Sprachen wurden zunächst auf Minicomputern wie DEC PDP-11 und später auf Workstations, PCs und Servern weit verbreitet verwendet.
Schließlich entwickelten sich ab den 1980er Jahren die Programmiersprachen der 4. Generation (4GL), zu denen Sprachen wie Python, Matlab und R gehören. Diese zeichnen sich durch die Minimierung von Code-Overhead, hohe Flexibilität, Unterstützung multiparadigmatischer Ansätze und einen hohen Grad an Automatisierung aus. Sie finden vor allem auf modernen PCs, High-Performance-Computing (HPC) Clustern und Cloud-basierten Systemen Anwendung, die seit den 1990er Jahren zunehmend verfügbar wurden.
Imperative und deklarative Programmierung
Bei der Imperativen Programmierung (von imperare, lat. “befehlen”) wird der Problemlösungsweg als Folge von Anweisungen beziehungsweise Befehlen vorgegeben. Diese Befehle verarbeiten Daten, die über Variablen adressiert werden. Innerhalb der imperativen Programmierung unterscheidet man mit der prozeduralen und der objektorientierten imperativen Programmierung zwei verschiedene Ansätze.
In der prozeduralen imperativen Programmierung werden Daten und die sie manipulierenden Befehle getrennt behandelt. Das zentrale Strukturkonzept bilden hier Prozeduren oder Funktionen, die wiederverwendbare Einheiten von Anweisungen kapseln. Demgegenüber werden in der objektorientierten imperativen Programmierung Daten und die auf sie wirkenden Befehle als Objekte zusammengefasst. Diese Objekte sind zugleich Datenträger und Funktionsanbieter und bilden das zentrale Strukturkonzept dieses Ansatzes.
In der Praxis kommen häufig Mischformen aus beiden Paradigmen zum Einsatz, da sich die Ansätze je nach Problemstellung gut kombinieren lassen. Ein Beispiel für eine solche Mischform ist die neuere Programmiersprache Julia, die keinen klassischen objektorientierten Ansatz, sondern stattdessen einen multiple dispatch Ansatz bietet: Funktionen können je nach Typ der Argumente unterschiedliche Implementierungen haben und so Aspekte der objektorientieren und der prozeduralen Programmierung verbinden.
Bei der Deklarativen Programmierung steht nicht der Lösungsweg, sondern die Beschreibung des Problems selbst im Vordergrund. Der Programmierende formuliert, was erreicht werden soll, und nicht im Detail, wie der Computer dieses Ziel schrittweise erreichen soll. Der Lösungsweg wird dabei anhand vorgegebener Regeln und Mechanismen automatisch vom System ermittelt.
Ein typisches Merkmal deklarativer Ansätze ist, dass der Fokus auf der Definition von Beziehungen, Bedingungen oder Zielen liegt, anstatt auf einer expliziten Abfolge von Befehlen. Der Programmierende beschreibt also das gewünschte Ergebnis in einer formalen Sprache, und der Computer sucht eigenständig nach einer Lösung, die diesen Bedingungen genügt.
Bekannte Ansätze der deklarativen Programmierung sind zum Beispiel die logische Programmierung, wie sie in der Sprache Prolog umgesetzt ist, oder die funktionale Programmierung, wie in Haskell. In der logischen Programmierung werden Fakten und Regeln formuliert, aus denen das System per Inferenz Schlüsse zieht. In der funktionalen Programmierung werden Berechnungen durch die Komposition von Funktionen beschrieben, wobei der Zustand der Daten nicht explizit verändert wird. Deklarative Programmierung eignet sich besonders für Probleme, bei denen der Lösungsweg komplex und vielfältig sein kann, aber die gewünschten Eigenschaften des Ergebnisses klar spezifiziert werden können. Typische Anwendungsgebiete sind zum Beispiel Datenbankabfragen (z. B. SQL), Wissensbasen, Constraint-Lösungen und formale Beweise.
Ein modernes Beispiel für den Einsatz deklarativer Programmierung ist das System Lean, ein sogenannter Theorem Prover. Lean wird eingesetzt, um mathematische Sätze formal zu beweisen. Hierbei formuliert der Benutzer in einer Mischung aus deklarativen und interaktiven Befehlen die Axiome, Definitionen und zu beweisenden Aussagen. Das System prüft automatisch, ob die angegebenen Schritte gültig sind, und ergänzt nach festgelegten Regeln die fehlenden Teile des Beweises. Damit verbindet Lean deklarative Programmierung mit automatischer Beweisführung und erlaubt es, große mathematische Theorien formal korrekt zu entwickeln. Für eine Einführung in Lean im Rahmen mathematischer Beweisführung, siehe zum Beispiel The Mechanics of Proof.
Kompilierte und interpretierte Programmiersprachen
Kompilierte Programmiersprachen zeichnen sich dadurch aus, dass der gesamte Quellcode vor der Ausführung in Maschinensprache übersetzt wird. Für diese Übersetzung wird ein spezielles Programm, der sogenannte Compiler, verwendet. Der vom Compiler erzeugte Maschinencode wird direkt vom Prozessor ausgeführt, was eine sehr schnelle Ausführung des Programms ermöglicht. Da das ausführbare Programm bereits in Maschinensprache vorliegt, muss es zur Laufzeit nicht mehr übersetzt werden. Allerdings ist es notwendig, bei jeder Änderung des Quellcodes das Programm erneut zu kompilieren. Typische Beispiele für kompilierte Programmiersprachen sind Java, C und C++.
Im Gegensatz dazu wird bei interpretierten Programmiersprachen der Quellcode während der Ausführung in eine maschinennahe Sprache übersetzt. Dies übernimmt ein spezielles Programm, der sogenannte Interpreter. Da die Übersetzung und Ausführung gleichzeitig stattfinden, ist die Ausführung im Vergleich zu kompilierten Programmen in der Regel langsamer. Ein Vorteil besteht jedoch darin, dass der Quellcode bei Änderungen nicht neu interpretiert werden muss – das Programm kann sofort ausgeführt werden. Typische Beispiele für interpretierte Programmiersprachen sind Python und R.
Im Sinne der hier eingeführten Begriffsbildungen ist R eine interpretierte imperative Programmiersprache der 4. Generation, die perse objektorientiert ist, aber prozedural genutzt werden kann und auf die statistische Datenanalyse zugeschnitten ist.
11.4 R
R ist eine Programmiersprache und ein Softwarepaket, das ursprünglich von Ross Ihaka (Ihaka & Gentleman (1996)) entwickelt wurde. Es handelt sich um einen freien Dialekt der proprietären Software S, die von Becker et al. (Becker et al. (1988)) eingeführt wurde. Heute wird R vom R Core Team und der R Foundation weiterentwickelt und gepflegt. R verfügt über eine große und aktive Community, die bislang rund 20.000 R-Pakete beigetragen hat, die das System um zahlreiche Erweiterungen ergänzen. Dabei bleibt der Kern von R bewusst evolviert und konservativ, während die Vielzahl an R-Paketen eine konsistente und progressive Weiterentwicklung ermöglichen. R lässt sich einfach von CRAN herunterladen und installieren.
Mit R kann man Datensätze laden, manipulieren und in verschiedenen Formaten speichern. Die Sprache ermöglicht es, eine Vielzahl von Berechnungen auf unterschiedlichen Datenstrukturen durchzuführen. Darüber hinaus bietet R ein breites Spektrum statistischer Analysemethoden, die sich direkt auf die Daten anwenden lassen. Typisch ist auch das Schreiben von reproduzierbaren Datenanalyseskripten, mit denen sich Analysen automatisieren und Ergebnisse nachvollziehbar dokumentieren lassen. Schließlich eignet sich R hervorragend zur Erstellung von Abbildungen und Visualisierungen, um Daten anschaulich zu präsentieren.
Allerdings kann mit R kann bisher auch einige Dinge, die in der psychologischen Datenwissenschaft von Bedeutung sind, nicht so gut umsetzen. So fehlt es R perse an einer modernen, ansprechenden Entwicklungsumgebung, allerdings schafft hier die Einbettung von R in VSCode Abhilfe. Im Bereich des Scientific Computing stößt R an seine Grenzen. Hier bieten Sprachen wie Python, Matlab oder Julia oft leistungsfähigere und flexiblere Werkzeuge. Schließlich eignet sich R kaum zum Programmieren psychologischer Experimente, wofür ebenfalls eher Python oder Matlab zum Einsatz kommen.
Um Hilfe zu R zu bekommen, gibt es viele Möglichkeiten. Ganz klassisch hilft oft schon eine schnelle Suche bei Google, eine Anfrage bei ChatGPT oder eine Interaktion mit CoPilot in VSCode. Während der Programmierung und wenn man den Namen einer Funktion bereits kennt, kann man direkt in R Hilfe aufrufen:
Für umfassendere, längere Tutorials und Hintergrundinformationen lohnt sich ein Blick in die sogenannten Vignetten der installierten Pakete:
Darüber hinaus gibt es zahlreiche hilfreiche Online-Ressourcen, zum Beispiel die spezialisierte Suchmaschine rseek oder die vielen Artikel und Anleitungen auf R-Bloggers, wobei die meisten Informationen auf diesen Plattformen sicherlich auch Teil der über ChatBots zugänglichen Datenbanken wie ChatGPT, Gemini, Claude oder DeepSeek sind.
11.5 Visual Studio Code
Visual Studio Code (VSCode) ist ein kostenloser Quelltext-Editor von Microsoft, der seit 2015 für Windows, macOS und Linux verfügbar ist. Grob gesagt ist VSCode so etwas wie die (vielleicht eher bekannte) RStudio Umgebung, nur eben für alle Programmiersprachen. VSCode lässt sich über Extensions flexibel erweitern und als integrierte Entwicklungsumgebung (IDE) für nahezu jede Sprache nutzen, zum Beispiel für R, Python, Julia, Shell, Quarto und viele mehr. Seit 2018 gilt VSCode als der beliebteste Editor überhaupt, wie jährliche Umfragen zeigen. Bemerkenswerterweise ist damit ausgerechnet ein Microsoft-Produkt der meistgenutzte Editor auch in der Linux-Welt. VSCode ist sehr konfigurierbar, stark von der Community geprägt und ermöglicht über Microsofts GitHub die Synchronisation der Einstellungen und Erweiterungen über mehrere Endgeräte hinweg. Eine ausführliche Dokumentation mit Anleitungen zur Nutzung man unter www.code.visualstudio.com/docs. Einen Einstieg zum Arbeiten mit R in VSCode liefert die entsprechende Dokumentation R in Visual Studio Code.