\usepackage{todonotes} \usepackage{minitoc} \usepackage{makeidx} \usepackage{sidenotes} \usepackage{tikz} \makeindex \setcounter{secnumdepth}{3}
WAST
auf *AST
: Öffnung und Aufbereitung für andere Editionsprojekte.Dieses Hauptseminar erklärt Grundlagen der Softwarearchitektur und des Projektmanagements am konkreten Beispiel von WAST, den Wittgenstein Advanced Search Tools, das am CIS in Kooperation mit dem Wittgenstein Archiv in Bergen (WAB) aktiv entwickelt wird.
Dabei richtet sich der Fokus zunächst auf die Architektur dieser Plattform, und die Erklärung der einzelnen Komponenten sowie ihres Zusammenspiels. Desweiteren werden die Schnittstellen und die Projektinfrastruktur an diesem konreten Projekt erläutert und die Prinzipien dahinter vorgestellt.
Begleitet wird das Seminar von praktischen (kleinen) Beispiel-Übungen, die helfen, die wesentlichen Schritte tatsächlich selbst nachzuvollziehen und moderne Technologien und Frameworks kennenzulernen.
Besprochen und praktisch probiert werden in diesem Seminar: die Versionsverwaltung git und das am IFI angebotene gitlab als Web-Oberfläche zur Softwareverwaltung sowie Kollaboration mit anderen Entwicklern; das Wrappen von Libraries, die in C geschreiben wurden in C++; das Erweitern derselben C-Libraries mit neuen Funktionalitäten; das Einbinden der entstandenen C++-Libraries in Javascript, und damit in einem nächsten Schritt in den Browser; die Architektur von modernen Webseiten (auch als Single Page Applications – SPA – bekannt) in Form von Server/Client-Kommunikation; die Verwendung des sogenannten MEAN-Stacks zur Umsetzung von Single Page Applications, d.h. das Zusammenspiel von MongoDB + ExpressJS + AngularJS + NodeJS.
Erwartet werden nicht zwingend Kenntnisse in allen vorgestellten, o.g. Technologien und Frameworks, dafür aber ein hohes Maß an Willen, Belastbarkeit, Spielfreude, generellen, gut-bis-sehr-guten Programmierkenntnissen und, nicht zuletzt, die spielerische Neugierde, die leidenschaftliche Software-Entwickler antreibt (nicht zuletzt diejenigen, deren Technologien wir verwenden). Dazu gehört auch das exakte Lesen von Aufgaben-Spezifikationen und Programm-Dokumentation.
Als Lohn des dicht gepackten, umfangreichen Programms des Seminars mit, z.T. bedeutenden Sprüngen von Woche zu Woche, erhält man einen praktischen und einen theoretischen Einblick in konkrete, existierende Frameworks und Strukturen, sowie die Antwort auf die Frage: ich habe eine Kommandozeilen-Applikation in C++ – wie kann ich diese nun in den Browser transportieren und der Welt zugänglich machen?
Die Wittgenstein Advanced Search Tools (WAST) sind eine Kollektion von verschiedenen Anwendungen des CIS, die spezifisch auf das Korpus des Archivs in Bergen, Norwegen (WAB) entwickelt wurde.
WAST ist entstanden aus einer zunehmend gewachsenen Kooperation mit dem des WAB Archivs in Bergen (vertreten durch Alois Pichler) und Max Hadersbeck (CIS).
Für die Zusammenarbeit spielen folgende Aspekte eine wesentliche Rolle:
Wittgenstein wiederum ist ein Subteil des ehemaligen(?) europäischen Projekts unter der Leitung von Paolo D’Iorio (darunter, u.a. )
WAST Projektstruktur
Die Tool-Landschaft von WAST besteht aus zwei Dingen:
Zu den Komponenten zählen alle Dinge, die …, darunter:
Zu den Tools zählen alle Dinge, die …, darunter:
Die Landschaft von WAST
Die WAST-Infrastruktur (siehe ab Seite ) erlaubt es, den deploy-Prozess und den Versionierungs-Prozess zu abstrahieren, indem alle Komponenten unter einem Dach versammelt sind.
Zum Beispiel kompiliert und startet man dann die feedback-Komponente folgendermaßen:
./wast build wast-feedback-app && ./wast start wast-feedback-app
Die weiteren Komponenten funktionieren dann alle gleich und es ist wesentlich einfacher, den Überblick zu behalten.
Alle von der WAST-Infrastruktur verwalteten Komponenten findet man ganz einfach heraus, indem man
./wast
ohne Argumente startet. Hierbei werden sowohl alle Komponenten als auch Aktionen angezeigt.
Komponenten-Maintainer können ihre Komponente in die Gruppe cis
(Gitlab cis group 2014) überführen lassen – dazu ist dann eine Mail an Thomas notwendig (glaube ich). Vorteile daraus:
das Projekt wird im gitlab nicht mehr gegen das persönliche Projektlimit bei der RBG gezählt
das Projekt ist weniger “personenbezogen” und kann besser von Nachfolgern “geerbt” werden
Komponenten-Maintainer, die ihre Komponente in die WAST-Infrastruktur hinzufügen wollen, machen folgendes:
git submodule add git@gitlab.cip.ifi.lmu.de:cis/wf.git components/wf
Am besten direkt die Remote-Bezeichnung[link: http://git-scm.com/book/en/Git-Basics-Working-with-Remotes] origin
in upstream
umbenennen (der Einheitlichkeit halber)
git remote rename origin upstream
Im eigenen Projekt die wesentlichen Dateien anlegen (oder die, die man für nötig hält):
wast.build
wast.check
wast.start
wast.stop
wast.restart
Beispiele hierzu finden sich hier:
In den folgenden Teilen werden die wichtigsten Komponenten wittfind ( ab Seite ), SIS ( ab Seite ), feedback ( ab Seite ), wab2cis ( ab Seite ) und WIndex ( ab Seite ) besprochen.
sloccount [link: http://www.dwheeler.com/sloccount/]
The web-based frontend WiTTFind consists of several web programs which offer an easy to use interface to query Wittgenstein’s Nachlass and presents all search results as HTML-transformation of the edited text together with an excerpt from the original facsimile of Wittgenstein’s remark. For the texts available on Wittgenstein Source1, every result is linked to Wittgenstein Source as well as to the Semantic web annotation tool Pundit Grassi et al. (2013 ^7) (see Figure [fig:search])
The XML-Transcription of Wittgenstein’s Nachlass Pichler (2010), which was developed at WAB in Bergen, annotates the texts in greatest detail. All deletions and substitutions etc. of Wittgenstein are annotated in the XML-texts. The latter is too detailed in order to be used by WAST and thus a new TEI P5 TEI Consortium (2009) compatible and reduced XML-Format (CISWAB) was defined. This text format is an optimal base for the cooperation between the Wittgenstein researchers and programmers in the field of computational linguistic. The CISWAB texts are extracted via XSLT-transformation from WAB’s XML-transcriptions of Wittgenstein’s Nachlass Pichler et al. (2009).
Successful text searches crucially rely on the availability of an electronic full-form lexicon. For the work on Wittgenstein’s Nachlass, we used CISLEX Guenthner and Maier (1994), one of the biggest electronic German full-form lexica, which has been developed at CIS over the last 20 years. We extracted all words from the texts available on Wittgenstein Source, the word information from CISLEX and extended it to the special lexicon, called WiTTLex. Each word-entry in WiTTLex is formatted according to the DELA format, defined at the Laboratoire d’Automatique Documentaire et Linguistique (LADL), Paris Gaston (1991). The lexicon entries contain the word’s full-form, lemma, lexicographical word form, inflection and semantic information. Search queries to WiTTFind can be grammatically processed with the help of WiTTLex. Figure [fig:dic] shows a short excerpt from the lexicon.
[H]
Advokat, Advokat.N+HUM:neM
gesagt, sagen.V:OZ
gesamte, gesamt.ADJ+NUM:aeFxp:aeFyp:aeFzp:aeNyp:\
amUxp:neFxp:neFyp:neFzp:neMyp:neNyp:nmUxp
dagestanden, dastehen.V+#2
glänzende, glänzend.ADJ+ER+COL+Glanz
Fermat, .EN
Fermatsche, Fermat'sch.ADJ+EN
rot, rot.ADJ+COL+Grundfarbe
rötlicher, rötlich.ADJ+COL+Zwischenfarbe+KOMP:\
deFxp:geFxp:gmUxp:neMxp:neMzp
[fig:dic]
In order to provide extended linguistic search capabilities for explorations from non-technical and non-linguistic backgrounds, a suite of tools was developed at CIS Seebauer (2012; Krey 2013; Fink 2013; Volos 2013). With a mixture of implicit and explicit conversions we are able to provide advanced linguistic search capabilities to researchers from different fields of the humanities. The next section provides a bottom up overview on the program suite ``, one section out of WAST, that is used to find text, i.e. to find utterances of Wittgenstein.
Every query to `` is internally transformed to a local-grammar-search-graph of the search terms Maurice (1997). In its simplest form these graphs are a chain of the search terms. But in general search-graphs can contain an arbitrary number of loops and branches to express complex search patterns Fink (2013). The system uses various strategies to match tokens in the text. It is able to use i.a. string-based matching, regular expressions, semantic and morphological information from dictionaries and arbitrary annotations from the searched text itself.
In order to provide a simple interface to the user and hide much of the complexities involved, queries from the user are automatically converted to local-grammar-search-graphs. A simple query language enables the user to search for sequences of arbitrary length or combine tokens with boolean operators Fink (2013). Furthermore, the system applies implicit conversions on the queries to generate more natural search results.
The tool is implemented in a client-server architecture. There are three main tools involved in the processing of queries: A server-, a client- and a displaytool. The client merely takes queries from the user and applies some very basic transformation to them before sending the actual query to the server. These transformations mostly convert convenient wildcard expressions to valid more complex regular expressions.
On the server side each query is parsed and then transformed to a local-grammar, that is applied asynchronously to the different documents the query wishes to search. One running server instance is able to provide parallel searches over a various set of different documents. All results are then send back to the client.
The third tool is a simple displaying tool, that is able to output the result of the queries in different formats2. Together these three tools provide the backend to present query results, that are further used by the WiTTFind front end to present the final results of queries.
[H]
[tab:wfbenchmark]
Query type | Median (seconds) |
---|---|
strict | 0.082 |
word phrase | 0.080 |
lemma-based | 0.088 |
inverted lemma-based | 0.088 |
sentence | 0.169 |
local grammar (particle verbs) | 0.214 |
Table [tab:wfbenchmark] lists a view benchmarking results of different query types to a server instance running on a modern desktop PC3. The numbers each show the median of 10 different queries to the text of the Big Typescript All queries where limited to a maximum of 100 hits4. It should be noted, that the last two lines show the results of complex queries, that generate very few hits. These queries take a lot more time to finish (more than twice as long), since the whole document has to be processed and local grammars are used to disambiguate.
The electronic full-form lexicon WiTTLex allows to perform lemmatized queries to the Nachlass. For example, the search for the word denken (think) returns all sentences which contain morphological variants of the word, like dachte, gedacht. We also implemented with the help of WiTTLex an inverted lemmatized search where we used the lemma of the queried word to produce all morphological variants of it. The word dachte leads to the lemma denken again, from which all word variants are derived.
In the full-form lexicon, WiTTLex, every word is stored together with its lexical word form as it is defined in the German CISLEX (see Table [tab:tags]).
[H]
[tab:tags]
Name | Tag | Translation |
---|---|---|
Nomen | <N> |
noun |
Adjektiv | <ADJ> |
adjective |
Verb | <V> |
verb |
Determinativ | <DET> |
determiner |
Adverb | <ADV> |
adverb |
Partikel | <PART> |
particle |
Präposition | <PREP> |
preposition |
Präposition + Artikel | <PDET> |
preposition + article |
Konjunktion | <KONJ> |
conjunction |
Interjektion | <INTJ> |
interjection |
Verbpartikel | <VPART> |
verb particle |
Eigenname | <EN> |
proper name |
The finder allows for the use of word forms as placeholders for words in a query. For example, the query Die <ADJ>
Farbe finds all sentences that contain the nominative feminine definite article Die, followed by an adjective, which precedes the noun Farbe (colour) as in sentence ([ex:farbe]).
[ex:farbe]
[ex:farbe:q] Die <ADJ>
Farbe [ex:farbe:a] Ich könnte Dir die genaue Farbe der Tapete zeigen, wenn hier etwas wäre was diese Farbe hat.5
To further reduce the syntactic ambiguity of the word forms in the text, we additionally tagged the data with the Part-of-Speech (POS) treetagger Schmid (1994). The finder permits POS-tags to be used as placeholders for the selection of syntactic word forms, as defined in the Stuttgart-Tübingen-Tagset Schiller, Thielen, and Teufel (1999). The user can decide to search for a lexical word form (e.g. <ADJ>
) or the syntactic word-form within the sentence (e.g. [ADJ*]
).
In WiTTLex we classified nouns Strutynska (2012) and adjectives Krey (2013) semantically. According to the work by Langer and Schnorbusch Langer and (Hrsg.) (2005) we defined eleven classes for nouns (see: Table [tab:semnoun]) and eleven classes for adjectives (see: Table [tab:semadj]).
[H]
[tab:semnoun]
Name | Tag | Translation | Occurrences |
---|---|---|---|
Menschen | <HUM> |
humans | 140 |
Tiere | <T> |
animals | 96 |
Pflanzen | <PF> |
plants | 26 |
Objekte | <OBJ> |
objects | 1402 |
Ereignisse | <ER> |
events | 589 |
Zustände | <ZU> |
states | 51 |
Eigenschaften | <EIG> p |
properties | 236 |
Temporalia | <TEMP> |
time | 49 |
Eigennamen | <EN> |
proper names | 60 |
Numeralia | <NUM> |
number | 47 |
Diversa | <SONST> |
other | 713 |
Name | Tag | Translation | Occurrences |
---|---|---|---|
Farben | <COL> |
colour | 974 |
Numeralia | <NUM> |
number | 1258 |
Relation | <REL> |
relation | 2517 |
Eigennamen | <EN> |
proper names | 17 |
Temporalia | <TEMP> |
time | 619 |
Evaluation | <EVAL> |
evaluation | 1732 |
Zustände | <ZU> |
states | 6629 |
Komparativa | <KOMP> |
compariative | 2080 |
Stilistika | <STIL> |
style | 1917 |
Eigenschaft | <EIG> |
property | 382 |
Ereignisse | <ER> |
proppperty | 187 |
In our investigations in Wittgenstein’s Big Typescript (BT)6, we found that there are about 1800 nouns out of all 46000 words in the data (see: Table [tab:semnoun] and [tab:semadj]). All the Tags from the Tables can be used in WiTTFind. For example, the query <EN>
und <EN>
returns the sentence ([ex:frege]) in which the two proper names Fregeschen and Russellschen are joined by the coordinating conjunction and.
[ex:frege]
[ex:frege:q] <EN>
und <EN>
[ex:frege:a]Unzulänglichkeit der Fregeschen und Russellschen Allgemeinheitsbezeichnung.7
In cooperation with the Faculty of Philosophy, Philosophy of Science and the Study of Religion, at the University of Munich (Rothhaupt Rothhaupt (1996)), we implemented a first semantic classification of Wittgenstein’s colour vocabulary together with a special HTML-interface for querying colours in the Nachlass. We found, that five different categories for colours are optimal for Wittgenstein’s Nachlass: Grundfarbe (primary colour), Zwischenfarbe (intermediate colour), Transparenz (transparency), Glanz (gloss) and Farbigkeit (colourness) Krey (2013). Table [tab:sem] shows the different labels for colours and the number of their occurrences in the text.
In the web-frontend WiTTFind, the user can select between different colour categories (see: Table [tab:sem]), view statistics and query Wittgenstein’s Nachlass.
For the Wittgenstein researchers it is very important to take the sentence structure into account. To enable users to specify the sentence structure within queries, we introduced sentence structuring tags in queries, such as <BOS>
(the beginning of a senctence), <EOS>
(the end of a sentence) and <PUNCT>
for punctuation characters. The wildcard operator *
can be used as placeholder for arbitrary character sequences. The query: <BOS>
Ich meine nur <PUNCT>
* * *
<PUNCT>
<EOS>
would return all sentences that consist of six words starting with the sequence of three tokens Ich meine nur
followed by a punctuation character as in example ([ex:sage])
[ex:sage]
[ex:sage:q] <BOS>
Ich meine nur <PUNCT>
* * *
<PUNCT>
<EOS>
[ex:sage:a] Ich meine nur, was ich sage.8
[tab:sem]
|c|c|p2.0cm|c| Name & Tag & Translation & Occurrences
Grundfarbe & <Grundfarbe>
& basic colour & 454
Zwischenfarbe & <Zwischenfarbe>
& intermediate colour & 301
Transparenz & <Transparenz>
& transparency & 105
Glanz & <Glanz>
& gloss & 2
Farbigkeit & <Farbigkeit>
& colourness & 29
To enrich the number of found verbs concerning a search query, we implemented an automatic particle-verbs detection and distinction. Particle-verbs are marked in their lexical entry and divided into verb and particle. In the Big Typescript we found almost 750 verbs with separated particles. To disambiguate the separated particles from prepositions, we use Part-of-Speech tagging and local grammars. For example, the query with the particle verb dastehen would extract instances as: steht klar da … in which the verb particle da, which may occur in German separately from the verb stehen, is recognized as such and not as a preposition (see: Figure [fig:dastehen]).
One characteristic feature of Wittgenstein’s Nachlass is, that Wittgenstein changed his texts very often and as a result the Nachlass offers a lot of different readings. To enable the finding of remarks in all of the latter, in a background process of the finder application, we generate all different readings, which are processed simultaneously Seebauer (2012).
Ludwig Wittgenstein’s Nachlass is highly heterogeneous. It consists of a large number of texts, some of them handwritten, with numerous passages edited from the author himself. The researchers can only appreciate the found remarks to the fullest with all its editions, errors and overall form, if they see not only the HTML-transformation of the edition, but as well their original facsimile which should be displayed simultaneously Rothhaupt (2006). Only this combined view provides the possibility for comparison and analysis of the original material, which carries the complete set of information. In order to implement this layout, it was necessary to OCR all facsimile of Wittgenstein’s Nachlass, extract the coordinates of his remarks and link them to the search result according to their siglum Gotscharek et al. (2011). All the work was done with ABBYY FineReader and additionally developed PERL programs. The highlighting in the display of the facsimile is done with CSS techniques within the HTML Page Lindinger (2013).
SIS (und sein Web-Teil) besteht aus mehreren Schichten und dem ineinandergreifen verschiedener Technologien und Frameworks.
Alle diese Teile werden in den nächsten Kapiteln besprochen:
SIS, die Symmetrischen Index Strukturen sind Endliche Automaten in Form von SCDAWGs (Symmetric Compacted Directed Acyclic Word Graphs), welche es erlauben, Texte zu indexieren und während des Lookups Vorschläge und Vervollständigungen sowohl klassisch nach-rechts als auch nach links zu geben.
Diese werden detailliert in (Daniel Bruder 2012) besproche, welches aufbaut auf (Gerdjikov et al. 2013), und dieses im Wesentlichen mit (Blumer et al. 1987) verknüpft.
Die Grundidee hinter SIS ist folgende: Man nehme zwei nicht-symmetrische CDAWG-Automaten – in linearer Zeit und auf linearem Raum direkt, on-line erstellt (Inenaga et al. 2001) –, und befülle einen mit einem Text in “normaler”, links-nach-rechts-Richtung (left to right, LTR), und den anderen in umgekehrter Richtung (RTL). Dann identifiziere man die identischen States dieser beiden Automaten (Gerdjikov et al. 2013). Daraufhin annotiere man die Automaten mit den Hinweisen auf die Dokumente, für welche sie stehen ((Daniel Bruder 2012))
Die ausführbare Datei sis
(im Verzeichnis build/bin/sis
) erlaubt zwei verschiedene Modi: einmal zum Indexieren eines vorhandenen Korpus (sis index
) und zum anderen zur Suche auf einem bestehenden Index (sis context
).
Ein erster Versuch auf Kommandozeile ergibt folgendes Ergebnis:
> bin/sis
This is SIS. Please see `sis --help' for available commands and options
You can get additional help using `sis <COMMAND> --help'
Available COMMANDs are:
context
index
Und der Aufruf der Hilfe ergibt folgendes:
> bin/sis --help
sis
---
Usage:
sis [GENERAL OPTIONS] COMMAND [COMMAND OPTIONS] ...
Allowed general options:
-h [ --help ] show help message
-v [ --version ] show version
--commands show available commands
-i [ --input ] arg input files
SIS funktioniert also ähnlich wie git
mit dem Hauptbefehl sis
und den Unter-Kommandos index
und context
.
Zu jedem Unterkommando gibt es nun noch eine weitere Hilfe, welche die speziellen Argumente erklärt – hier für sis index
:
> bin/sis index --help
Options for command 'index':
-o [ --output ] arg output name
-f [ --thefile ] arg file listing input files
Und hier für sis context
:
> bin/sis context --help
Options for command 'context':
-w [ --width ] arg (=10) lr-context width
-q [ --query ] arg query string
-p [ --print-filename ] print the filenames
Beide Sub-Kommandos erwarten also (laut den general options) mindestens ein Inputfile mit --input
bzw. -i
:
bin/sis index -i INPUTFILE
bin/sis context -i INPUTFILE
Für das Sub-Kommando index
wird als Inputfile entweder
-i
eine Auflistung von Dateien erwartet:
bin/sis index -i file1 -i file2
-f
eine Datei, welche die Input-Dateien auflistet (falls die Input-Dateien mit -i
zu viele werden):
bin/sis index -f file.list
Die Input-Dateien für sis index
müssen folgendermaßen beschaffen sein:
(Um hier mehr Flexibilität anbieten zu können müssen noch mehr features für einen Importer geschrieben werden, siehe https://gitlab.cip.ifi.lmu.de/bruder/sis3/issues/93; desweiteren werden bei plain-text Dateien die Dateinamen als Siglum interpretiert).
Solange die Importer noch nicht komplett vollendet sind, gibt es eine Zwischenlösung, plain text Dateien herzustellen:
Man kann die Testdaten aus src/features/data/Ts-213
und den Extractor unter src/features/data/Ts-213/extract.sh
verwenden um plain-text Dateien herzustellen.
Der Extractor liefert die Dateien in das Verzeichnis work/
aus, man ist aber selbst dafür zuständig, dieses frei zu halten (also gegebenenfalls zu löschen vor der nächsten Extraktion).
> ./extract.sh
Usage:
./extract.sh [7bit|utf8]
(results will be written to work/)
(make sure you erase work/ before)
Der Extractor ist, wie gesagt, nur eine Zwischenlösung lässt sich aber verwenden. Der Befehl …
> ./extract.sh utf8
… wird plain text Dateien nach work/
extrahieren, welche sich für bin/sis index -i file1 -i file2
verwenden lassen.
Idealerweise legt man eine Datei allfiles.list
o.ä. an …
> find ../src/features/data/Ts-213/work/utf8 \
-type f \
-iname '*.satz' \
-exec echo `pwd`/{} \; \
> Ts-213.allfiles.list
… um diese in einem Stück füttern zu können:
> bin/sis index -f Ts-213.allfiles.list -o allfiles.index
Letzterer Befehl erstellt einen Index für alle Dateien, die in Ts-213.allfiles.list
gelistet sind und legt – wegen der zweistufigen Serialisierung (siehe das zugehörige Kapitel) –, zwei Serialisierungsdateien an: allfiles.index
für den C++-Teil des Automaten und allfiles.index.c
für den C-Teil des Automaten (es sei an dieser Stelle kurz darauf hingewiesen, dass sich allfiles.index.c
auch mit der executable der C-Schicht, unter bin/caut
verwenden lässt, ohne den C++-Teil zu benötigen):
> ls -lh allfiles.index*
-rw-r--r-- 1 dbruder staff 6,1M 11 Mär 13:26 allfiles.index
-rw-r--r-- 1 dbruder staff 12M 11 Mär 13:26 allfiles.index.c
Nachdem also der Index angelegt ist, lässt sich auf diesem Suchen – hierbei fungiert der vorhergehende Output (-o allfiles.index
) diesmal als Input für bin/sis context
:
> bin/sis context -i allfiles.index -q erst
Automaton loaded
size is 5548
results.size() = 599
'oder erinnerst Du Dich a'
'Erinnerst Du Dich d'
'oran erinnerst Du Dich? '
'e verifizierst Du das? ?'
' daß man erst ahnungslo'
' ich doch erst aufbauen;'
[...]
Laut der Hilfe zu sis context
(bin/sis context --help
) lässt sich unter anderem die Breite der ausgeschnittenen Ergebnisse einstellen (-w WIDTH
):
> bin/sis context -i allfiles.index -q erst -w 20
Automaton loaded
size is 5548
results.size() = 599
's gesagt; oder erinnerst Du Dich an die Stim'
'Erinnerst Du Dich daran, daß'
'“ Woran erinnerst Du Dich? — '
'Wie verifizierst Du das? — dann we'
' nicht so, daß man erst ahnungslos ist, und'
'rt müßte ich doch erst aufbauen; oder: von'
'gen: Ein Satz folgt erst aus ihm, wenn er da'
'ch mir vielmehr nun erst ausrechnen. '
' an, die deren Sinn erst bestimmen; was nich'
[...]
Im Folgenden ein Überblick über die Projektstruktur von SIS, die Schichten, aus welchen SIS sich zusammensetzt, und die wichtigsten Klassen innerhalb von SIS.
Hier ein Überblick und eine Beschreibung der wichtigsten Teile der Projektstruktur:
.
|-- build # Out-of-source dir für cmake / Kompilierung
| |-- bin # Platz für die gebildeten exeucatables
| |-- lib # --- " --- libraries
| `-- src # --- " --- *.o
|-- cmake # cmake-module für Makefile-Generation
|-- etc # Test-Daten und anderes
|-- src # C++-Sources
| |-- adapter # Alle Adapter-Klassen (wrappen vendor/cautomata)
| | |-- VoidSequence
| | `-- inenagaCDAWG
| |-- features # Tests
| | `-- data # Test-Daten
| `-- indexer # Indexer-Klassen
|-- vendor # 3rdparty libs (eingebunden als git submodules)
| |-- cautomata # C-Schicht von Petar Mitankin, Sofia
| |-- make_unique # wurde im C++11-Standard schlicht vergessen
| |-- pugixml # XML-Modul
| |-- serialization # ...
| `-- testing
`-- web # Web-Schicht: MEAN-Stack
|-- bower_components # Client-seitige Javascript-Bibliotheken
|-- build # hier wird das node-addon gebildet
|-- cpp # Hier liegen die Klassen, die src/ nach JS wrappen
|-- node_modules # Server-Seitige Module
|-- public # Client-Seite
| |-- javascripts
| `-- stylesheets
|-- routes # Server-Seite: Routes
`-- views # Client-Seite: Views
`-- partials
Schicht 1: C9
Schicht 2: C++(11)10
Schicht 3: Web11 mit:
SIS Schichten
Zunächst ein Überblick:
Klassenarchitektur von SIS
Im Folgenden eine Kurz-Besprechung der (wichtigsten) Klassen, ihres Zwecks und ihrer grundlegenden Eigenschaften aus der C++-Schicht, welche die struct
s aus der C-Schicht wrappen und dokumentindexierende Eigenschaften zur Verfügung stellen:
*Adapter
Jedem *Adapter
liegt ein C-Struct zugrunde, vergleiche das entsprechende Kapitel zum grundlegenden Wrapping-Pattern
Ein sis::Document
stellt die grundsätzliche Basis dessen dar, was letztendlich von einem DocumentIndexingAutomaton
verarbeitet und indexiert wird.
Es hat zwei wesentliche members:
filename_
undcontents_
wobei filename
das Siglum darstellt und contents
den tatsächlichen Inhalt.
In einem Stück wie diesem etwa…
<s n="Ms-115,3[2]_1" ana="facs:Ms-115,3 abnr:7 satznr:22">
“Was heißt es: ‘dieser Gegenstand ist mir<lb/> wohlbekannt?’”
</s>
entspräche:
filename == Ms-115,3[2]_1
contents == “Was heißt es: ‘dieser Gegenstand ist mir wohlbekannt?’”
Document
ist hinsichtlich des string
-Typs templatisiert: damit lässt sich besser mit dem unterliegenden string
-Typ der C-Schicht, VoidSequence
umgehen.
Die C-Schicht verlangt unbedingt, dass zu einem gegebenen Zeitpunkt einmal das Makro mSetEndianness()
aufgerufen wird. Anonsten passieren können sehr böse Dinge während der Serialisierung geschehen (bytes können falsch herum eingelesen werden, da die Endianness nicht richtig definiert ist) und damit nur sehr schwer nachvollziehbare Fehler entstehen. Daher ruft der Konstruktor von Document dieses Makro auf.
Die contents
eines Document
werden aus Platzgründen nicht serialisiert. Schlussendlich sind diese Inhalte in automaton()->data_
verfügbar (welches serialisiert wird) und über entsprechende VoidSequenceAdapter::iterator
s rekonstruierbar (in Zusammenarbeit mit DocumentIndex
)
DocumentCollection
ist im wesentlichen eine Liste von Document
s.
Ihm können auch XML-Dateien als Input gegeben werden und er erstellt daraus “virtuelle” Dokumente (durch die Methode import()
), als wären die einzelnen Document
s in tatsächlichen Dateien vorgelegen (s.o. das Beispiel mit Siglum/Contents im Kapitel Document).
Die Methode import()
verwendet pugixml
(Arseny Kapoulkine 2014a), ein externes Modul aus dem vendor
-Teil des Projekts (als git submodule von (Arseny Kapoulkine 2014b)).
Ein DocumentIndex
ist eine std::map
die Document
s und ihre Positionen verwaltet:
typedef Document<StringType> key_type;
typedef DocumentPos<StringType> value_type;
typedef std::map<key_type, value_type> docinfo_t;
/// @todo rename: value_type -> mapped_type (do this everywhere)
Document
und ihre DocumentPos
werden im index
-ing-Schritt des DocumentIndexingAutomaton
angelegt:
Document
gehört ein sinkstate im Automaten, d.h. ein Final-StateDocumentPos
, DocumentPositions
gedacht waren. Diese werden Stück für Stück entfernt werden, sind aber mit deprecated gekennzeichnet.Der DocumentIndexingAutomaton
(Abbildung auf Seite ist die komplexeste Klasse hinsichtlich seiner Member. Er ist die zentrale Stelle, an der alle Informationen zusammenlaufen:
Document
s zu den C-Automaten und entsprechendesDocument
sDocumentIndexingAutomaton UML
Die Basis-Klasse für DocumentIndexingAutomaton
(Abbildung auf Seite )
DocumentIndexingAutomatonAbstract UML
SIS setzt sich aus mehreren Ebenen bzw. Schichten zusammen, welche im folgenden besprochen werden:
Hier ein Beispiel für ein C-Header-File, das ein struct beschreibt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Und hier die wrappende C++-Klasse mit “sauberen” Konstruktoren (CTORs) und (automatischen) Desktruktoren (DTORs)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
(Vergleiche auch das Kapitel Techniken/TMP)
Wie im Kapitel [Techniken/TMP] ausgeführt, erlaubt es Template Meta Programmierung, oder kurz: TMP, bestimmte Entscheidungen und Berechnungen zur Compile-Zeit auszuführen, bzw. diese in die Compile-Zeit zu verlagern.
Das kann einerseits erhöhte Typ-Sicherheit gewährleisten (bei weniger Code-Aufwand), schlicht Zeit ersparen, oder, wie in diesem Fall, die Konstruktion, oder besser: die “Zusammenstellung” von Klassen übernehmen.
Konkret verstanden werden soll unter “Zusammenstellung” in diesem Fall, dass durch die Technik der TMP eine kohärente Klasse geschrieben wird, die sich je nach Instantiation unterschiedlich “verhält”.
Ganz gezielt wird diese Technik in Form der Automaten – kurz gesagt –, auf folgende Weise genutzt:
Das C-struct wird, wie im vorhergehenden Beispiel mit einer symbolSize
konstruiert. Die UINT symbolSize
wird anderswo definiert und kann (bzw: soll) zwei unterschiedliche unsigned int
-Werte annehmen:
1
, um 7-bit Strings zu verarbeiten4
, um UTF-8 zu verarbeitenVergleiche folgende Code-Teile aus der C-Schicht:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
und folgenden Teil aus voidsequence.h
:
1 2 3 |
|
und – nicht zuletzt –, folgenden Teil aus compressedAutomaton.c
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Das Problem mit diesem Code nun gestaltet sich folgendermaßen:
UINT symbolSize
gemeint sein sollUINT symbolSize
annehmen darf. Es ist nicht definiert, was passiert, sollte ein Anwender den Wert 3
hineingebenAnders ausgedrückt verstößt dieser Code gegen mehrere Richtlinien für “sauberen” Code:
float
übernehmen sollte, die die Verzögerungsgeschwindigkeit anzeigen sollte. Dabei war nicht klar, ob in km/h
oder in meile/h
angegeben sollte. Mit strong typing und Verwendung von Klassen, die SI-Einheiten darstellen, wäre so etwas nicht passiert, da ein Objekt km/h
nicht in eine Methode hineingegeben werden kann, das meile/h
verlangt. Vergleiche hierzu unbedingt das Kapitel über SI-Einheiten in Stroustrups “the C++-Language” für C++11Zurück zu unserem Code:
Um es so einfach wie möglich zu machen die Wrapper-Klassen richtig zu benutzen, und so schwer wie möglich, diese falsch zu benutzen (und dabei auch noch Platz und Größe zu sparen), benutzen die Wrapper-Klassen Template-Meta-Programmierung, die folgendes erreicht:
class CompressedAutomatonAdapter
mit der Definition template<class StringType> struct CompressedAutomatonAdapter
ist templatisiert, wobei ein Template-Parameter erwartet wird, der:
std::string
iststd::wstring
iststd::string
abgeleitete Klasse darstellttemplate class CompressedAutomatonAdapter
, dass:
CompressedAutomaton
mit genau dem richtigen Wert für UINT symbolSize
aufgerufen wirdstatic_assert
), bzw. schon zur Compile-Zeit über eine falsche Verwendung (mit entsprechender Fehlermeldung) aufgeklärt wird – und der Code gar nicht erst kompiliert (statt später ohne Warnung in “undefined behavior” hineinzurennen)Konkret wird dies erreicht und umgesetzt durch folgenden Code:
Zunächst noch einmal die Konstruktion der adaptierten Klasse:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Bemerkungen:
StringType
in Zeile 2 ist lediglich deskriptiv, aber nicht präskritiptiv: sie ist lediglich dazu geeignet, dem Leser zu sagen, was hier erwartet wird, prüft dies aber nicht (oder kann dies ohne Concepts, die erst in C++14 kommen werden, gar nicht)adaptee_
mit einer symbol_size
, die von CompressedAutomatonAbstract<StringType>::traits::symbol_size
geliefert wirdCompressedAutomatonAbstract<StringType>
wiederum ist ein gemeinsames Interface, das alle Automaten teilen werdenHiermit also zur Betrachtung des Interfaces CompressedAutomatonAbstract<StringType>
1 2 3 4 5 6 7 8 9 10 11 |
|
Dinge, die man hier erkennen kann:
CompressedAutomaton
beruft sich auf ein noch grundlegenderes Interface: AutomatonAbstract<StringType>
und “erbt” dessen class traitsCompressedAutomaton
deklariert pure virtuals für alle erbenden Klassen und zeichnet sich damit durch folgendes aus:
Nun ist aber nocht nicht die Herkunft der traits geklärt.
Traits erlauben es, “kostenlos”, d.h. ohne Speicher zu belegen, Informationen in Klassen zu halten, die während der Compile-Zeit genutzt werden können.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
Die Ziele, die dieser (C++11-)Code erreicht, sind mehrere:
template<class T>
std::string
,std::wstring
std::is_base_of
)Diese Überprüfung findet zur Compile-Zeit (!) statt (constexpr
) und das Ergebnis wird in einem bool
::value
abgespeichert
traits
definiert, die wiederum an anderer Stelle abfragbar sind:
ElementSize<mystring>::type
würde den Typ mystring
zurückgebenElementSize<mystring>::string_type
würde das selbe Ergebnis liefernElementSize<mystring>::value_type
würde den value_type
der Klasse zurückgeben mit der ElementSize
instantiiert ist; im Falle von std::string
also char
Der wichtigste traits jedoch ist static constexpr unsigned int value
: an dieser Stelle wird, je nach Instantiierung ein numerischer Wert von 1
oder 4
zurückgegeben, nämlich die exakte Größe des value_type
s der Klasse template<class T>
:
Bespiel:
ElementSize<std::string>::value
der Wert 1, da der value_type
von std::string
ein char
ist, und char
(zwar plattform-abhängig) aber in den meisten Fällen die “Breite” 1
hat (also ein 1 Byte “breit” ist)ElementSize<std::wstring>::value
der Wert 4, da der value_type
von std::wstring
ein wchar_t
ist, und wchar_t
(zwar plattform-abhängig) aber in den meisten Fällen die “Breite” von 4
hat (also 4 Byte “breit” ist)Passenderweise sind dies genau die Werte, die unser adaptierter Automatot CompressedAutomaton
in seinem Konstruktor erwartet! Allerdings kann die Adapter-Klasse CompressedAutomatonAdapter
nun nicht mehr “falsch” verwendet werden!
Nicht zuletzt ist noch das trait stream_type
interessant:
Je nachdem, ob die Automaten mit std::string
oder std::wstring
instantiiert wurden, werden Ausgaben auf std::cout
oder std::wcout
gelenkt werden müssen. Um auch dieses “wegzuabstarhieren” wird – wiederum zur Compile-Zeit –, entschieden, was der passende Typ von Streams sein wird.
Hierfür bietet sich std::conditional
aus dem Header <type_traits>
an, das stream_type
für eine Instantiierung mit std::string
auf den stream_type
-Typ auf std::fstream
festlegt und mutatis mutandis das selbe für std::wstring
erledigt.
Wenn nun eine Automaten-Klasse also Ausgaben vornehmen wird, so kann sie dies abstrakt erledigen, indem Sie – in etwa – folgendes tut:
#include "ElementSize.hpp"
template<class T>
class Foo {
using stream_type = ElementSize<T>::stream_type;
stream_type& operator<<(const T&);
}
Sind hiermit alle Anforderungen erfüllt?
UINT
oder unsigned int
nicht klar ist, welche konkreten Werte an dieser Stelle erwartet werden.class ElementSize
aufbauen, wesentlich flexibler, da sie gleichzeitig unabhänigig hinsichtlich des stream_type
s operieren können.Letztendlich also bietet sich folgendes Szenario:
Der Nutzer verwendet eine Klasse CompressedAutomatonAdapter<std::string>
und bekommt – zur Compile-Zeit – die für ihn “passende” (d.h. kleinstmögliche, platzsparendste, sicherste) Klasse “zusammengebaut” und hat dabei auch noch Sicherheit für die Verwendung gewonnen. Sollte er die Klasse “falsch” (d.h. mit einem “nicht-string”) instantiieren wollen, würde ihm dies beim kompilieren durch das static_assert
inklusive einer Fehlermeldung mitgeteilt werden.
Zum Erlernen von Techniken zur Template Meta Programmierung, verwenden Sie bitte die Übungen, die im Kapitel TMP auf Seite angegeben werden.
Eine weitere Technik, die innerhalb der SIS code base zu finden ist, ist ein Member Dispatching Pattern. Obwohl man das selbe Ziel auch über Template-Spezialisierung erreichen könnte, ist es dennoch ratsam, dieses Pattern zu kennen um es im Falle des Falles auch tatsächlich zu erkennen.
Wie mit allen Patterns, sehen diese am Anfang komplett unverständlich aus, kennt man sie aber, so sieht man sofort, was geschieht, ohne lange nachdenken zu müssen.
Das “Member Dispatch”-Pattern funktioniert so:
Angenommen, ich habe eine Template-Klasse Foo
mit einer Methode bar()
. Nun möchte ich komplett unterschiedliche Methoden bar()
ausführen, je nachdem womit die Klasse Foo
instanziiert ist – also z.B. als Foo<std::string>
und Foo<int>
.
Der member-dispatch funktioniert dann so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Es gibt eine zentrale Methode auto bar() [[ dispatcher ]]
, welche dann einfach weiterreicht an die eigentlichen Methoden, int bar(tag<int>) [[ dispatched_to ]]
oder [[ dispatched_to ]] std::string bar(tag<std::string>)
.
Das struct tag
dient dabei lediglich einer “Markierung” und wird vom Optimizer im Laufe der Kompilierung entfernt.
Fast die gesamte Funktionalität der SIS-Komponente wurde mit dem Verfahren des TDD, Test Driven Developments entwickelt.
Mehr zum Thema TDD findet sich im Kapitel TDD – Test Driven Development ab Seite .
Kurz zur Struktur der Tests innerhalb von SIS:
src/features
src/
sollte ein Test entsprechen:
Document
, z.B. in Document.hpp
, sollte eine Fixture und ein Feature besitzen:src/features/DocumentFixture.hpp
und src/features/DocumentFeature.hpp
Zur tieferen Beschreibung von Fixtures und Features, siehe nochmal das Kapitel TDD – Test Driven Development ab Seite ; an dieser Stelle nur so viel:
In SIS findet sich eine Klasse Document
, mit den folgenden Fixtures/Features um die Spezifikation zu prüfen:
#ifndef DOCUMENTFIXTURE_HPP_8JMP5XLF
#define DOCUMENTFIXTURE_HPP_8JMP5XLF
#include "sis_config.hpp"
#include <vector>
#include <string>
typedef std::string file_t; /// @todo change this to fs::path
struct DocumentFixture
{
std::vector<file_t> filenames = {
sis_TEST_DATA_DIR"ascii/1.txt",
sis_TEST_DATA_DIR"ascii/2.txt",
sis_TEST_DATA_DIR"ascii/3.txt",
};
std::vector<file_t> wfilenames = {
sis_TEST_DATA_DIR"utf8/1.txt",
sis_TEST_DATA_DIR"utf8/2.txt",
sis_TEST_DATA_DIR"utf8/3.txt",
};
std::vector<std::string> contents = {
"Das Verstehen, die Meinung, faellt aus unsrer Betrachtung heraus.",
"Also: Kann man Etwas anders, als als Satz verstehen?",
"Oder aber: Ist es nicht erst ein Satz, wenn man es versteht."
};
std::vector<std::wstring> wcontents = {
L"Das Verstehen, die Meinung, fällt aus unsrer Betrachtung heraus.",
L"Also: Kann man Etwas anders, als als Satz verstehen? Ääääh...",
L"Oder aber: Ist es nicht erßt ein Satz, wenn man es versteht."
};
};
#endif /* end of include guard: DOCUMENTFIXTURE_HPP_8JMP5XLF */
#include "sis_config.hpp"
#include "testing/lest.hpp"
#include "DocumentFixture.hpp"
#include "indexer/Document.hpp"
#include <fstream>
#include <locale>
#include <cereal/types/vector.hpp>
#include <cereal/types/string.hpp>
#include <cereal/access.hpp> // For LoadAndAllocate
#include <cereal/types/memory.hpp>
#include <cereal/archives/binary.hpp>
#include <cereal/archives/json.hpp>
#include <cereal/archives/xml.hpp>
#include <cwchar>
DocumentFixture f;
const lest::test specification[] =
{
"standard CTOR callable",[]
{
sis::Document<std::string> d;
EXPECT( d.filename() == "" );
},
"CTOR with filename", []
{
sis::Document<std::string> d{ f.filenames.front() };
EXPECT( d.filename() == f.filenames.front() );
},
"can serialize (automaton with added document)", []
{
sis::Document<std::string> d{ f.filenames.front() };
std::ofstream ofs{ "document.bin" };
cereal::BinaryOutputArchive oarchive{ ofs };
oarchive( d ); // Write the data to the archive
},
"can deserialize (automaton with added document)", []
{
sis::Document<std::string> d{ f.filenames.front() };
std::ifstream ifs{ "document.bin" };
cereal::BinaryInputArchive iarchive{ ifs };
iarchive( d ); // Read the data
EXPECT( d.filename() == f.filenames.front() );
EXPECT( d.contents() == f.contents. front() );
},
"can serialize (automaton without added document)", []
{
sis::Document<std::string> d;
std::ofstream ofs{ "document.bin" };
cereal::BinaryOutputArchive oarchive{ ofs };
oarchive( d ); // Write the data to the archive
},
"can deserialize (automaton without added document)", []
{
sis::Document<std::string> d;
std::ifstream ifs{ "document.bin" };
cereal::BinaryInputArchive iarchive{ ifs };
iarchive( d ); // Read the data
EXPECT( d.filename() == "" );
EXPECT( d.contents() == "" );
},
"can access content", []
{
sis::Document<std::string> d{ f.filenames.front() };
EXPECT( d.contents() == f.contents.front() );
},
"can access wide content", []
{
sis::Document<std::wstring> d{ f.wfilenames.front() };
/// Make sure to properly imbue!
EXPECT( d.contents() == f.wcontents.front() );
},
"can access content (for default CTOR)", []
{
sis::Document<std::string> d;
EXPECT( d.contents() == "" );
},
"can access filename (for default CTOR)", []
{
sis::Document<std::string> d;
EXPECT( d.filename() == "" );
},
"can access filename (with given filename)", []
{
sis::Document<std::string> d{ f.filenames.front() };
EXPECT( d.filename() == f.filenames.front() );
},
"can serialize (automaton with added document) [as std::wstring]", []
{
sis::Document<std::wstring> d{ f.wfilenames.front() };
std::ofstream ofs{ "document.json.w" };
cereal::JSONOutputArchive oarchive{ ofs };
oarchive( d ); // Write the data to the archive
},
"can deserialize (automaton with added document) [as std::wstring]", []
{
sis::Document<std::wstring> d{ f.wfilenames.front() };
std::ifstream ifs{ "document.json.w" };
cereal::JSONInputArchive iarchive{ ifs };
iarchive( d ); // Read the data
std::wstring expected = L"Das Verstehen, die Meinung, fällt aus unsrer Betrachtung heraus.";
/// Make sure to properly imbue!
std::wcout << L"[" << d.contents() << L"]" << std::endl;
std::wcout << L"[" << expected << L"]" << std::endl;
EXPECT( d.filename() == f.wfilenames.front() );
EXPECT( d.contents() == expected );
},
"can serialize (automaton with added document) [as std::wstring]", []
{
sis::Document<std::wstring> d{ f.wfilenames.front() };
std::ofstream ofs{ "document.bin" };
cereal::BinaryOutputArchive oarchive{ ofs };
oarchive( d ); // Write the data to the archive
},
"can deserialize (automaton with added document) [as std::wstring]", []
{
sis::Document<std::wstring> d{ f.wfilenames.front() };
std::ifstream ifs{ "document.bin" };
cereal::BinaryInputArchive iarchive{ ifs };
iarchive( d ); // Read the data
std::wstring expected = {L"Das Verstehen, die Meinung, fällt aus unsrer Betrachtung heraus."};
/// Make sure to properly imbue!
// std::wcout << L"[" << d.contents() << L"]" << std::endl;
// std::wcout << L"[" << expected << L"]" << std::endl;
EXPECT( d.filename() == f.wfilenames.front() );
EXPECT( d.contents() == expected );
},
};
int main() {
std::setlocale(LC_ALL, "");
return lest::run( specification );
}
Das Thema Serialisierung wird allgemein beschrieben im Kapitel Serialisierung.
In diesem Kapitel wird die zweistufige Serialisierung innerhalb von SIS beschrieben.
Warum die Serialisierung innerhalb von SIS zweistufig läuft ist durch das Wrapping-Pattern zu erklären:
Da die C-Schicht ihre eigene Serialisierung hat, kann diese selbst angesprochen werden. Um die zusätzlichen Datenstrukturen aus der C++-Schicht zu serialisieren, ist jedoch noch weitere Serialisierung notwendig.
Hierbei spielen folgende Überlegungen eine Rolle:
Abgesehen von den generellen Überlegungen zur Serialisierung wie im Kapitel Methoden zur Serialisierung beschrieben, bieten sich zur Serialisierung in ein Binärformat vor allem folgende Optionen an:
Im Falle von SIS fiel die Wahl auf cereal, da die Funktionalität den Bedürfnissen entspricht und die Verwendung als einfach bezeichnet werden kann.
Im besonderen Falle von SIS hinsichtlich des Wrappings und der Beibehaltung der Serialisierungs-Funktionalität der C-Schicht ist jedoch im besonderen zu beachten, dass die C++-Sicht beim Speichern die Serialisierung der C-Schicht anstoßen muss und beim Laden die Ergebnisse der C-Deserialisierung entsprechend berücksichtigen (und möglicherweise in die Klassenhierarchie entsprechend integrieren muss).
Hierzu stehen für einen generischen(!) template<AutomatonType> struct DocumentIndexingAutomaton<AutomatonType<StringType>>
folgende Schnittstellen zur Verfügung:
template <class F = std::ofstream, class A = cereal::BinaryOutputArchive>
void save(std::string & fn /* fs::path & p */);
template <class F = std::ifstream, class A = cereal::BinaryInputArchive>
static
DocumentIndexingAutomaton<AutomatonType>
load(std::string & fn);
Mit diesen Methoden lassen sich alle Kombinationen der folgenden Dimensionen von DocumentIndexingAutomaton
speichern und lesen:
Automaten | StringType | Formate | Ziele |
---|---|---|---|
SCDAWGAdapter |
std::string |
JSON | std::fstream , |
CDAWGAdapter |
std::wstring |
XML | d.h. std::cout , |
CompressedAutomatonAdapter |
Binär | std::ofstream (Datei), … |
Um zu verdeutlichen, wie die (De-)Serialisierung und dabei das Anstoßen der Serialisierungsfunktionen der C-Schicht funktioniert, hier die Implementierung:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
Zur Verdeutlichung der Kombinations-Möglichkeiten hier ein Auszug aus den dazugehörigen Tests:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
Um ein Beispiel von der API von cereal zu geben, hier ein Ausschnitt aus DocumentIndexingAutomaton
.
Dabei geht es im wesentlichen darum, eine member-Methode template <class Archive> void serialize( Archive & archive )
für die Klasse zu definieren und dieser mitzuteilen, welche member-Variablen serialisiert werden sollen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Hinzuweisen ist noch auf CEREAL_NVP
: hiermit wird festgelegt, dass, wenn nach json oder XML serialisiert wird, die Daten in der Serialisierung benannt werden (NVP = named value pair).
In diesem Kapitel werden die wesentlichen Bausteine der Web-Ebene von SIS besprochen. Der Web-Teil ist im wesentlichen eine SPA (Singe Page App) und baut hierbei weitestgehend auf den sogenannten MEAN-Stack15 auf, lässt aber u.a. die Datenbank weg, da die Daten von den Automaten in SIS geliefert werden.
Für Detail-Besprechungen dieser Technologien siehe auch das eigene Buch “Teilbesprechungen”.
Wie im Kapitel Wesentliche Technologien und ihr Zweck angedeutet handelt es sich bei der Web-Schicht um eine SPA in Form des MEAN-Stack.
Dabei spielt der Node-Webserver eine wesentliche Rolle. Mit Express, dem Web-Framework für node lassen sich routen definieren und Antworten an den Client zurücksenden (vergleiche hierzu auf Seite ).
Das Zusammenspiel von Node und den C++-Automaten von SIS funktioniert nun auf folgende Art und Weise:
Im Ordner web/
befindet sich eine weitere Schicht, welche die nun in C++ vorliegenden Automaten derart wrapped, dass diese direkt in Javascript verwendet werden können:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Konkret bedeutet dies, dass das C++-Objekt an dieser Stelle in Javascript nutzbar gemacht worden ist, und zwar als “node-addon” über v8!
Die Erklärung, wie dies bewerkstelligt werden kann, folgt in diesem Kapitel.
Selbstverständlich stellt sich die Frage, warum überhaupt man C++-Objekte so wrappen sollte, dass man mit diesen in javascript arbeiten kann. Die Antwort lautet
Die Überlegung ist, nur 1x zu deserialisieren.
Zunächst einmal aber zum Lauffähig-machen der Web-Componente (auch hier gilt: bitte vergleichen Sie auch die einschlägigen Kapitel in [Teilbesprechungen][]).
Im Normalfall sollte der Kompilierungs- und Installationsprozess für die Web-Komponente durch den cmake-Workflow automatisch übernommen werden, dennoch an dieser Stelle ein paar Hinweise im Speziellen und zum Vorgang allgemein.
Der cmake-Workflow bildet die Web-Komponente (aus Zeitgründen) nicht (!) per default und muss gesondert aktiviert werden:
17 18 19 20 21 |
|
Das heisst, es gibt zwei Möglichkeiten, den Build der Web-Komponente anzustossen:
CMakeLists.txt
-Datei auszutauschen, odereine environment-Variable auf ON
zu setzen, womit der “volle” cmake-Befehl folgendermaßen aussieht:
sis/build> SIS_BUILD_WEB_COMPONENT=ON cmake ..
(Der cmake-Befehl wird entsprechend eine Ausgabe liefern, die bestätigt oder verneint, ob die Web-Komponente gebaut wird):
[...]
-- [WEB COMPONENT]
-- Will NOT build web component
[...]
bzw.
[...]
-- [WEB COMPONENT]
-- Will build web component
[...]
Die Teile der Build-Kette für die Web-Komponente, die cmake automatisch anstößt, umfassen:
die server-seitigen Dependencies werden in node immer mit dem node package manager npm
installiert. Dieser liest die Datei package.json
im obersten Verzeichnis aus und installiert die dort aufgeführten Dependencies (und rekursiv deren Sub-Dependencies).
sis/web> npm install
Hinweis: npm installiert seine projekt-spezifischen Pakete nach node_modules/
.
Die client-seitigen Dependencies werden mit bower installiert. bower kümmert sich ebenfalls um das Auflösen von Abhängigkeiten von Javascript-Paketen – nur eben, im Gegensatz zu npmˆ–, für die Client-Seite, also den Browser.
sis/web> bower install
Hinweis: bower installiert seine projekt-spezifischen Pakete nach bower_components/
oder app/bower_components/
Vergleiche auch noch einmal die Beschreibung der Projektstruktur im Kapitel Projektstruktur!
Nicht zuletzt muss noch das node addon kompiliert werden, dass die C++-Objekte in Javascript zur Verfügung stellt. Dafür wird klassischerweise node-gyp16 als Build-Tool verwendet.
node-gyp liest die Datei binding.gyp
aus und orientiert daran seinen build-Prozess.
node-gyp umfasst wiederum mehrere sub-befehle, im wesentlichen aber reicht ein…
web/sis> node-gyp rebuild
… um (implizit) ein node-gyp build
anzustossen.
Eine minimale binding.gyp
-Datei sieht folgendermaßen aus:
{
"targets": [
{
"target_name": "hello",
"sources": [ "hello.cc" ]
}
]
}
An dieser Stelle sollen die wichtigsten Ausschnitte der binding.gyp
von SIS in aller Kürze besprochen werden:
{
'targets': [
{
'target_name': 'sis',
'sources': [
'cpp/sis.cpp',
'cpp/DocumentIndexingAutomaton.cpp'
],
'include_dirs': [
'/usr/include',
'<!@(echo `pwd`/../src/)',
'<!@(echo `pwd`/../vendor)',
'<!@(echo `pwd`/../vendor/serialization/include)',
'<!@(echo `pwd`/../vendor/make_unique)',
'<!@(echo `pwd`/../vendor/pugixml/src)'
],
# global flags go here
# `cflags_cc!` is supposed to mean: remove these flags
'cflags_cc!': [ '-fno-rtti' ], # '-fno-exceptions'
# `cflags_cc` is supposed to mean: add these flags
'cflags_cc': [ '-fPIC', '-std=c++11', '-fexceptions', '-Wall', '-Wextra', '-Wno-attributes', '-Wno-pointer-arith' ],
'conditions': [
/* [......] */
Zu beachten sind folgende Punkte:
include_dirs
verlangt vollständige Pfade, daher der “Hack” über die commandlinecflags_cc!
nimmt vorhandene Compiler-Flags wegcflags_cc
fügt Compiler-Flags an die bereits vorhandenen hinzuHinweis für Server omega bzw. 64-bit-Systeme: Omega hat 2 verschiedene Verzeichnisse, in denen libs liegen können: /usr/lib
und /usr/lib64
! Im Falle von fehlenden shared-objects auch dort suchen. Beim Build-Prozess
g++-4.8
oder ähnlich vor. Daher unbedingt den Standard-Compiler des akutellen Systems genauer unter die Lupe nehmen:
which gcc; gcc --version
;ls /usr/bin/g++*
CXX
aus. Diese lässt sich am besten gleich auf den vollen Pfad von gcc setzen, der Befehl lautet also ungefähr:CXX=which g++-4.8
node-gyp rebuild
Manchmal kann es vorkommen, dass beim Start des node Servers die shared objects aus dem node wrapping nicht gefunden werden; da hilft es die Variable LD_LIBRARY_PATH
entsprechend anzupassen: LD_LIBRARY_PATH=/srv/www/sis/sis3/build/lib:$LD_LIBRARY_PATH coffee app.coffee
um den Web-Server zu starten, reicht es mit node die entsprechende javascript-Datei zu starten.
Da für SIS coffeescript statt javascript verwendet wird, stattdessen den server also mit dem Befehl coffee
statt node
starten: coffee app.coffee
.
Mit den o.g. Hinweisen im Gepäck lässt sich die Applikation nun auch auf den Server deployen (beachte hierzu bitte auch das Kapitel[Der ausführende user wastd][]).
Um im Falle eines Crashs den Web-Server sofort wieder neu zu starten (speziell dieses Addon mit seiner zugrundliegenden C-Schichit ist sehr gefährdet für SEGFAULTs…), lässt sich das node-module forever17 verwenden. Siehe die Datei web/wast.start
für nähere Details.
Die Applikation Feedback18 dient als Web-Front-end für das reporting von bugs, feature requests und aller anderen Dinge, die relevant sind für den Bug tracker und die dahinterliegenden Konzepte der “Bug Tracking Best Practices”.
Bugs und feature requests submitten zu können kann interessant sein für
Als Web-Applikation ist Feedback konzipiert, da generell jeder, der einen Bug im Bug Tracker eintragen möchte, eine Registrierung auf der Web-Oberfläche des gitlab19 benötigen würde, dieses aber eine zu hohe Hürde darstellt und Bug Reporter aufgrund des Aufwands davon abhalten könnte, wichtige Informationen bereitzustellen.
Darüber hinaus garantiert Feedback eine geordenete Weise, in der Bugs abgelegt werden, indem es bugs automatisch nach Komponenten und Maintainern sortiert (durch tags) direkt in die Bug-Liste einträgt.
Der Aufbau von Feedback gestaltet sich in Form ein MEAN-Stack Web-Applikation, welche das Frontend und Backend bildet.
Im Backend wird im wesentlichen die API des Gitlab genutzt, um Bugs eintzutragen.
Um mit der API von Gitlab sprechen zu können bedarf eines eines sogenannten “Private Token” zur Authentifizierung. Hierfür wurde ein gesonderter User (“FeedbackBot”) angelegt, dessen Token verwendet wird, um Bugs zu submitten (dieses geheimzuhaltende Token wird auf der Serverseite bereitgehalten und nicht in das Frontend, zum Client übertragen). Um innerhalb von Wittgenstein-Issues20 bugs submitten zu können, muss FeedbackBot selbstverständlich als Member im Projekt vorhanden sein.
.
|-- Makefile # targets zum starten, stoppen, restart
|-- README.md
|-- bower.json # ini-file für client-seitige dependencies
|-- bower_components # Ordner für client-seitige dependencies
|-- database # MongoDB-Anbindung
|-- node_modules # Ordner für Server-seitige dependencies
|-- npm-shrinkwrap.json # ini-file für server-seitige dependencies (unwichtig)
|-- package.json # ini-file für server-seitige dependencies (wichtig)
|-- public # client-seitiger Applikationsteil: Application-Logic (AngularJS)
|-- routes.js # Server-seitige Routes
|-- views # client-seitiger Teil: Views (Jade)
`-- wast-feedback-server.js # Der Web-Server selbst (node)
Der operationale Ablauf der Web-Applikation gestaltet sich folgendermaßen:
curl
-Request eine Bug submission an die API von gitlab.Hinweis: Für eine genauere Beschreibung von Client-Seite/Server-Seite siehe das Kapitel Web-Applikationen, für eine Beschreibung der einzelnen Komponenten siehe das Kapitel MEAN-Stack und für eine Beschreibung der Applikations-Logik hinsichtlich Controller, View, etc., vergleiche das Kapitel Design Patterns.
Verwendung von TEI, [siehe auch Sonderkapitel TEI]
Zur Verwendung von git, siehe Kapitel git submodules.
Im Rahmen der Entwicklung von WAST spielt das selbst-gehostete gitlab des CIS unter
Zu den wesentlichen Aufgaben, die über gitlab laufen, zählt:
status:verify
)Unter Continuous Integration (CI) versteht man:
Continuous Integration is a software development practice where members of a team integrate their work frequently, usually each person integrates at least daily - leading to multiple integrations per day. Each integration is verified by an automated build (including test) to detect integration errors as quickly as possible. Many teams find that this approach leads to significantly reduced integration problems and allows a team to develop cohesive software more rapidly. This article is a quick overview of Continuous Integration summarizing the technique and its current usage.21
auf Seite verdeutlicht den Workflow einer CI.
Continuous Integration
Die Webseite für die CI am CIS findet sich unter https://gitlabci.cis.lmu.de/22.
Innerhalb der CI lassen sich dann Projekte, die im gitlab verfügbar sind, zu automatisierten Tests hinzufügen.
Dafür gibt man im wesentlichen an, wie die Software zu testen ist und erhält dann nach jedem push von Änderungen in sein Repository automatisch dieses getestet.
Damit ergibt sich eine gute Rückversicherung – und nicht zuletzt eine gut nach außen hin sichtbare From, dass sämtliche Software sich in einem “ship-baren”, also gesunden Zustand befindet. Das schafft Vertrauen und ist auch innerhalb des Teams höchst praktisch.
Eine ausführliche Anleitung wie die CI zu benutzen ist findet man hier: https://gitlab.cis.lmu.de/schweter/cis-ci/tree/master (erreichbar über https://gitlab.cis.lmu.de/public).
Der Maintainer der CI ist Stefan Schweter (schweter@cip.ifi.lmu.de) und die CI umfasst derzeit etwa 4 virtuelle Maschinen (ein bunter Mix aus Debian, Ubuntu, Suse, etc.) welche zufällig ausgewählt werden und die Tests ausführen. Damit wird sogar noch ein bisschen “Smoke-Testing” verhalten emuliert und sichergestellt, dass die innerhalb von WAST entwickelte Software auch auf anderen Betriebssystem installierbar ist und die Tests bestehen.
Das Thema CI hängt eng zusammen mit den Themen:
Makefiles und das dazugehörige Programm make
werden in der Software-Entwicklung seit jeher als Build-Systeme verwendet.
Build-Systeme beschreiben jeden einzlnen Schritt eines Build-Prozesses und führen diese auf intelligente Art und Weise so aus, dass möglichst wenige Schritte ausreichen, um einen funktionierenden build zu garantieren.
Da Build-Prozesse bei größeren Projekten einige Zeit beanspruchen können (mehrere Stunden sind hier nicht ausgeschlossen) und es beim debuggen sehr unpraktisch wäre, nach jeder Änderung einen kompletten Build-Prozess abzuwarten, macht man sich an dieser Stelle sowohl Eigenschaften von C und C++ (Object-Files, Trennung von Header und Implementation in unterschiedliche Dateien, shared und static libs, …) als auch das intelligente Vorgehen von Makefiles zu Nutze, um die entsprechenden Warte-Zeiten abzukürzen.
Makefiles bilden stets einen Pfad von Abhängigkeiten, welche schrittweise aufzulösen/abzuarbeiten sind, um zum nächsten Ziel kommen zu können.
Dabei dienen die sog. targets als die Ziele und die Definition dieser targets als die zu absolvierenden Schritte (häufig “recipes” genannt).
Hinzu kommt, dass sich die Bearbeitungszeiten der Dateien nutzen lassen, um festszustellen, ob – zum Beispiel – ein Object-File neu gebildet werden musss, oder nicht.
make
ist, wie bereits erwähnt, der Platzhirsch und der Standard unter den Build-Systemen. Allerdings können Makefile
s schnell komplex und unübersichtlich werden, weshalb man sog. “Makefile-Generatoren”, wie z.B. cmake oder gyp verwendet, um sich die entsprechenden Makefiles generieren zu lassen und diese nicht selbst schreiben und verwalten zu müssen.
Bis zu einer gewissen Größe und für eine breite Palette von Aufgaben lassen sich Makefile
s ganz hervorragend und gewinnbringend einsetzen – nicht zuletzt, da sie auf so gut wie jedem System vorhanden sind.23
Eine Einführung zu GNU make findet sich unter https://www.gnu.org/software/make/manual/make.pdf; vergleiche u.a. auch das Makefile
zum Erstellen dieser Dokumentation.
cmake24 ist ein Betriebssystem-unabhängiger Makefile-Generator für C- und C++-Projekte. Das heisst, man benutzt cmake, um ein Makefile zu generieren. cmake kann als “Industriestandard” für C- und C++-Projekte betrachtet werden.
Der Workflow mit cmake funktioniert also folgendermaßen:
cmake DIR
make
Dazu verteilt man (mindestens eine) CMakeLists.txt
-Datei (potentiell rekursiv) im Projekt, welche die Deklarationen zu Targets, Libraries, etc. enthält und teilt cmake durch die Angabe DIR
mit, wo sich diese Datei befindet. Danach wird ein Makefile
zur Verfügung stehen, welches man mit make
aufrufen kann.
cmake zu verwenden bringt mehrere Vorteile:
CMakeLists.txt
-Dateien rekursiv aufeinander aufbauen und jeweils an geeigneten Stellen im Projekt untergebracht werden, bleiben diese stets übersichtlichcmake wird vor allem deshalb gerne verwendet, weil der “klassische” autotools-Weg sehr umständlich, fehleranfällig und schlicht “altbacken” sein kann – siehe hierzu den Ablaufgraph “Autotools”, welcher den autotools-Weg nachzeichnet.
Autotools
autotools fasst wiederum den bis dahin verwendeten Weg und die dazu benötigten tools zusammen, die benutzt wurden um Makefiles generieren zu lassen. Den autotools-Weg erkennt man immer daran, dass eine Software den “klasssischen Dreischritt” ./configure && make && make install
verwendet.
Klassischerweise verwendet man bei cmake in sog. “out-of-source builds”. Einfach gesagt, heisst dies nur, dass man seinen Build nicht direkt in der source macht, sondern dafür ein eigenes Verzeichnis (meist build/
) anlegt, und fortan von dort arbeitet.
Damit vermeidet man das “Vermüllen” des source-directories mit Object-Files und dergleichen und hält so die Quellen stets sauber.
Der Workflow ist also:
~/project/foo > mkdir -p build && cd $_ # Hier liegt u.a. src/ mit den Quellen und (mindestens) einer CMakeLists.txt
~/project/foo/build > cmake .. # `..` ist übergeordnetes Verzeichnis!
~/project/foo/build > make # build-files werden out-of-source angelegt!
Die beiden witchtigsten Optionen für cmake sind:
-DCMAKE_CXX_COMPILER=/opt/local/bin/g++ # Verwende speziellen Compiler
-DCMAKE_BUILD_TYPE=Debug # Build type festlegen (andere Switches für den Compiler)
Diese gibt man cmake mit, das dann entsprechend die Makefiles generiert:
cmake -DCMAKE_CXX_COMPILER=/Users/dbruder/bin/g++ -DCMAKE_BUILD_TYPE=Debug ..
Hier ein Auszug aus den CMakeLists.txt
-Dateien des Projekts sis:
project(sis)
cmake_minimum_required(VERSION 2.8.3)
set(sis_AUTHORS "\"Daniel Bruder <bruder@cip.ifi.lmu.de>\"")
set(sis_NAME "\"SIS\"")
set(sis_SUBNAME "\"Symmetric Index Structures\"")
set(sis_FULLNAME "\"SIS -- Symmetric Index Structures\"")
# version numbers: lassen sich gut in doxygen etc verwenden
set(sis_VERSION_MAJOR 3)
set(sis_VERSION_MINOR 8)
set(sis_VERSION_PATCH 0)
set(CMAKE_INSTALL_PREFIX /usr/local)
set(FIND_LIBRARY_USE_LIB64_PATHS)
# diese Variablen können auf der Commandline mit -D gesetzt werden
set(sis_BUILD_TESTS ON)
set(sis_TEST_STATIC_ASSERTIONS 0) # 0 == 'OFF' / 0 == 'ON'
set(GIT_SUBMODULES_CHECKOUT_QUIET ON)
set(CONF_DIR ${PROJECT_SOURCE_DIR}/cmake)
include(${CONF_DIR}/config.cmake)
include(${CONF_DIR}/compilerdetect.cmake)
include(${CONF_DIR}/processorcount.cmake)
include(${CONF_DIR}/buildflags.cmake)
include(${CONF_DIR}/doxygen.cmake)
include(${CONF_DIR}/boost.cmake)
include(${CONF_DIR}/ctest.cmake)
include(${CONF_DIR}/cpack.cmake)
include(${CONF_DIR}/host.cmake)
include(${CONF_DIR}/checklocale.cmake)
include(${CONF_DIR}/submodules.cmake)
include(${CONF_DIR}/githooks.cmake)
include(${CONF_DIR}/nodewrap.cmake)
add_subdirectory(src/)
add_subdirectory(src/features)
add_subdirectory(vendor/)
add_subdirectory(web/)
Im Verzeichnis src/
findet sich folgendes cmake-file:
# Diese Verzeichnisse mit -I an den Compiler als Suchpfade mitgeben
include_directories(${CMAKE_SOURCE_DIR}/src/)
# Diese Verzeichnisse mit -isystem (oder nach OS abhängig) an den Compiler als Suchpfade mitgeben. -isystem bindet "externe" Header so ein, dass diese keine Warnings werfen.
include_directories(SYSTEM
${CMAKE_SOURCE_DIR}/vendor/
${CMAKE_SOURCE_DIR}/vendor/cautomata
${CMAKE_SOURCE_DIR}/vendor/make_unique
${CMAKE_SOURCE_DIR}/vendor/serialization/include
)
# Shared lib bauen
add_library(cppautomataadapter SHARED
adapter/CompressedAutomatonAdapter.cpp
adapter/SCDAWGAdapter.cpp
adapter/VoidSequenceAdapter.cpp
)
target_link_libraries(cppautomataadapter cautomata)
add_library(cppindexer SHARED
indexer/DocumentIndexingAutomaton.cpp
)
target_link_libraries(cppindexer cppautomataadapter cautomata)
add_executable (sis main.cpp)
target_link_libraries(sis cppautomataadapter cppindexer cautomata ${Boost_LIBRARIES})
rake
25 ist ein kleines aber feines Build-Tool, das hier als eine Alternative zu make
kurz vorgestellt werden soll. rake
ist und wird in Ruby geschrieben und ist auf den meisten Systemen, die ruby mitliefern, enthalten.
rake
ist aber nur eine von sehr vielen Alternativen zu make! Es gibt viele weitere, ähnliche Tools, doch nur eines, nämlich make
kann als echter “Standard” angesehen werden. Nicht zuletzt aber lohnt sich ein Blick auf make: unter Umständen lässt sich make durch diesen kleinen Umweg schneller und einfacher verstehen…
Rake ist – im Gegensatz zu cmake – aber kein Makefile-Generator und kann folglich (leider) keine makefiles generieren!
gyp (Generate Your Projects)26 ist ein Makefile-Generator, der von Google für den Build-Prozess von Chrome entwickelt wurde. Er wird des weiteren auch für den build von Node.js verwendet (was an anderer Stelle wichtig wird – hier werden wir gyp wieder begegnen). gyp ist also ein build-automation tool ähnlich zu cmake, gegenüber cmake aber werden die Targets in gyp-files als JSON-Struktur beschrieben – hier ein Beispiel für ein gyp-file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
|
In diesem Kapitel werden die wesentlichen Züge von Web-Applikationen vorgestellt (dazu zählen Client-Server-Konfigurationen und Kommunikation), und am Beispiel des [MEAN-Stack][Der MEAN-Stack] ein konkretes, sehr modernes Framework zum Erstellen von SPAs (Single Page Applications), bzw. Rich Client Web Applications vorgestellt. Wesentlich bei diesen ist das dynamische Laden von Seiten-Bestandteilen, statt dem kompletten Reload der gesamten Seite.
Moderne Web-Applikationen funktionieren im Allgemeinen nach wenigen, wesentlichen Prinzipien. In welcher Art und Weise diese konkret ausgestaltet werden, ist eine andere Frage – die Basics jedoch bleiben die selben.
Web Apps Basics
Die genannten Aktionen können unterschiedlicher Art sein:
Anfrage (1) kann – nach dem HTTP-Protokoll unterschiedliche Methoden haben:
Die Anfragen vom Typ (1) vom Client (B) an den Server (A) richten sich immer nach einer bestimmten Routen.
Der Server bestimmt, wie seine API gestaltet ist, und reagiert entsprechend auf Anfragen an bestimmte Routen.
Anfragen können sein:
Der Server (A) kann auf Anfrage (1) im Schritt (4) mit unterschiedlichen Typen von Ressourcen antworten (selbstverständlich wird (B) diesen “richtig” anfragen, bzw. genau diese Ergebnisse erwarten):
DER HTTP-Standard unterstützt theoretisch noch mehr Anfrage-Methoden, allerdings sind diese de facto aber kaum bis gar nicht umgesetzt (die nicht umgesetzten Methoden lassen sich gut GET und POST emulieren)
Die Schritte (3) und (4) sind optional. Der Typ der Datenbank ist auch nicht festgelegt und es gibt im wesentlichen 2 Typen von Datenbanken:
In Schritt (5) sendet der Server seine Antwort auf Anfrage (1), diese kann, wie gesehen, eine HTML-Seite sein, JSON, oder etwas anderes. An diesem Punkt ist es Aufgabe des Client, diese Antwort entsprechend zu verarbeiten.
Hier eine kleine Liste, was an welchen Stellen möglicherweise vorkommen könnte – diese Liste ist bei weitem nicht exhaustiv!
Als Server können unterschiedliche Software-Komponenten in Betracht kommen. Ein Server liefert im wesentlichen Daten aus (er “serve-d”):
Wie bereits erwähnt gibt es unterschiedliche Typen von Datenbanken und im wesentlichen SQL-orientierte und NO-SQL-orientierte:
Der MEAN-Stack ist ein Framework für die Entwicklung von modernen Webseiten – ähnlich wie das bekanntere “Ruby on Rails”. Diese neue Generation von Webseiten ist auch als “SPA” (Single Page Web-Applications) oder “Rich Client Web Applications” bekannt und unterscheidet sich von “herkömmlichen” Webseiten dadurch, dass stets nur einzelne Teilbereiche einer Webseite neu geladen werden anstatt durch eine Reihe von Seiten “vorwärts und rückwärts” zu gehen und diese jeweils komplett zu laden. Rich Client Web Applications fühlen sich wesentlich “applikationshafter” an als “herkömmliche” Webseiten und zeichnen sich nicht zuletzt dadurch aus, dass mehr Arbeit, die klassicherweise auf dem Server gemacht wurde auf den Client – also den Browser des Beutzers ausgelagert wird.
Genau genommen ist der MEAN-Stack nicht ein Framework, sondern die Kombination mehrerer, kleinerer Frameworks, Schichten und Module zu einem sogenannten stack.
Entscheidender Vorteil des MEAN-Stacks gegenüber Ruby on Rails sind u.a. folgende
Hierbei stehen die Initialen für folgende Teile, welche – ganz grob umrissen –, folgende Aufgaben übernehmen:
Wie oben angedeutet ließen sich die einzelnen Komponenten des Stack auch durch andere Komponenten austauschen – dazu im folgenden eine kleine Aufzählung, welche Alternativen (unter vielen vielen mehr) potentiell bestehen könnten – hierbei werden vielleicht auch die Aufgaben der einzelnen Komponenten deutlicher:
Node31 wird verwendet um einen eigenen Webserver selbst zu schreiben. Node ist ein unglaublich schneller Webserver und ein funktionsfähiger eigener Webserver lässt sich in Node bereits mit wenigen Zeilen schreiben.
Da am Ende Webseiten über eine normale WWW-Adresse ausgeliefert werden sollen und meistens ein Apache auf dem Server bereits läuft und auf dem HTTP-Port 80 “listened”, kann dieser Port nicht erneut belegt werden. Node macht daher seinen eigenen Port auf, z.B. 6688, was aber wiederum bedeuten würde, dass der user die seite auf diesem Port ansteuern müsste, z.B. webseite.com:6688.
Für diesen Fall wird zumeist ein sog. “Reverse Proxy” im Apache eingerichtet, der alle unterschiedlichen Services und Webserver die möglicherweise sonst noch auf dem Server laufen zusammenfasst und eine einheitliche Oberfläche nach außen bietet (d.h. keine expliziten Port-Angaben vom User verlangt, die ihn ohnehin nur verwirren würden). Bei einem Reverse-Proxy wird Apache mitgeteilt bestimmte Pfade auf bestimmte Ports einfach weiterzuleiten und nicht selbst zu bearbeiten.
Angular erlaubt es, vieles an Logik bereits in der View zu lösen, d.h. an der Stelle, an der es in einem MVC-Pattern32 häufig gebraucht wird. (Der andere, gegenteilige Ansatz nennt sich “Fat Controller”).
Angular erlaubt das ganz hervorragende “double data-binding”, welches man sich lieber live in einem Beispiel anschaut um es zu verstehen. Damit erlaubt es Angular unglaublich schnell schöne Web-Applikationen zu entwickeln.
Express ist ein Mini-Framework, das auf Node aufbaut, und es deutlich einfacher macht, einen Server mit Routes, Middleware, etc auf die Beine zu stellen. Dabei arbeitet es sehr eng mit dem Node-Webserver zusammen.
MongoDB (von “humounguous”, riesig) ist eine sogenannte “NoSQL”-, bzw. Dokumentenorientierte Datenbank.
Dabei speichert es im Gegensatz zu SQL-artigen Datenbanken JSON-Objekte (“Dokumente”) anstatt von Tabellen und ist wesentlich einfacher zu bedienen.
Auch passt es sich besser an sich schnell und häufig wechselnde Datenbank-Schemata an und ist dadurch wesentlich flexibler als eine SQL-Basierte Datenbank.
Dieses Dokument wurde erstellt mit einer elaborierten pipeline, die mehrere Tools umfasst.
Dabei wird von pandoc33 der wesentliche Teil übernommen, und dieses von gpp34, dem “general preprocessor” und ppp35, dem “pandoc preprocessor” begleitet.
Mit pandoc lassen sich eine breite Vielfalt von Dokumenten in Markdown erstellen und in verschiedene Formate exportieren, wie z.B.:
Um dieses Dokument voll setzen zu können sind mindestens notwendig:
Empfohlen werden dazu noch für das Bibliographieren:
Desweiteren sei hingewiesen auf das von David Kaumanns mit Stefan Schweter entwickelte Template zur Gestaltung von Bachelor- und Hausarbeiten, welches auf dieser pipeline aufbaut:
Der Workflow eines vollen Typeset gestaltet sich folgendermaßen:
#define
sDer Typesetting Process
Eine genauere Beschreibung und Anleitung findet sich in ppp-Documentation.pdf
im Repository von ppp49.
Für Hinweise zur Installation der dependencies bezüglich der Rechner am CIP-Pool, vergleiche die README.md
in BA Thesis Template50.
Für Hinweise zur Benutzung des Makefiles, vergleiche ebenfalls die README.md
in BA Thesis Template51.
Durch den unified deployment process in WAST werden alle Komponenten (unabhängig von ihrem Aufbau, ihren verwendeten Modulen, backends, frontends, …) abstrahiert unter einem Dach zusammengefassst, und mit nur einem Befehl verwaltet, administriert, und deployed.
Dieses Vorgehen hat den entscheidenden Vorteil, dass spätere Generationen alle Komponenten in gewohnter Weise bilden, kompilieren und deployen können – ohne sich speziell mit dem einzelnen Modul im Detail auskennen zu müssen – und, letztendlich, dabei einzelne Befehle in der richigen Reihenfolge mit den richtigen Argumenten auf diesen aufrufen zu müssen.
Der unified deployment process wird nach und nach immer stärker eingesetzt werden um die gesamte Tool- und Komponenten-Landschaft von WAST zu verwalten, d.h. – unter anderem –, Gesamt-Releases anzulegen (nach dem Semantic Versioning-Schema), den Development und den Master-Branch zu verwalten (siehe [git branching model][]), einzelne Komponenten upzudaten, neue annotierte Daten aus Bergen mit einem Befehl in alle Suchmaschinen zu verteilen und dergleichen.
Die zentrale Stelle an der alle Komponenten der WAST-Landschaft zusammenlaufen ist das repository wittfind-web.
wittfind-web stellt nach außen hin die zentrale Webseite als Einstieg für die Nutzer dar; nach innen hin ist dies der Schnittpunkt an dem alle Module zusammenlaufen52.
Das zentrale Makefile
, welches alle diese administrativen Aufgaben übernimmt (und dabei u.a. an komponentenspezifische Makefile
s weiterleitet ist hier).
+++ BEGIN DEPRECATED +++
Ab hier ist dieses Kapitel ein bisschen out-dated, vermittelt aber trotz allem den richtigen Eindruck von unserem neueren Makefile-Ansatz, bitte die folgenden Teile Updaten!
Um die Komponenten und Tools unter einem Dach zu bündeln bietet wast-infrastructure den Befehl wast
.
Unter wast
werden die verschiedenen Komponenten registriert und sind damit für den “unified deployment”-Prozess verfügbar:
> ./wast
WAST, version 0.9.5
This is the help section
Usage:
./wast <command>
Available COMMANDS:
init : initialize (all) components
build <component> : build component <component>
start <component> : start component <component>
stop <component> : stop component <component>
restart <component> : restart component <component>
Available COMPONENTS:
wast-feedback-app
wast-website
wf
sis
Die Komponenten werden unter Available COMPONENTS
gelistet und sind somit für die Befehle, die unter COMMANDS
gelistet sind, verfügbar.
Beispielsweise lässt sich somit also die Komponente wf mit folgendem Befehl bilden und starten:
> ./wast init wf && \
./wast build wf && \
./wast start wf
Selbstverständlich lässt es sich darüber nachdenken, welche weiteren Sub-Befehle für wast
noch denkbar sind (z.B. ist wast release <version>
noch nicht implementiert, und denkbar sind auch Befehle wie wast test <component>
, wast deploym <component>
um die vorhergehenden drei Befehle zusammenzufassen). Dieser Teil befindet sich bisher noch in der beta- und Evaluations-Phase, daher sollen zunächst die wichtigsten Befehle robust implementiert sein, bevor neue Funktionalität hinzugefügt wird.
Um wast
neue Funktionalität hinzuzufügen oder eine Komponente Teil des unified deployment-Prozesses zu machen ist gar nicht viel nötig, wie im nächsten Teil gezeigt wird.
wast
erwartet von einer Komponente im wesentlichen 2 Dinge:
wast
registriert seinwast
-Sub-Befehle, die unterstützt werden sollen, ein entsprechendes Skript liefern. Konkret muss wf
, wenn es die Befehle wast build
und wast start
anbieten will, in seinem Verzeichnis die Dateien wast.build
und wast.start
anbieten.In diesem Beispiel soll die Komponente sis
und ihre Implementation im Rahmen der wast-infrastructure betrachtet werden.
Zunächst also die Registrierung der Komponente in der wast infrastructure in der Datei wast
:
14 |
|
Als nächstes die entsprechenden Skripte in der Komponente selbst:
Hier also die Datei wast.build
innerhalb der Komponente “sis”:
#!/bin/sh
set -x # Show the commands that are executed
# try "Creating build directory" by
mkdir -p build
# try "Switching to build directory" by
cd build
# try "Running cmake" by
cmake ..
# try "Running make" by
make VERBOSE=1
# try "Switching to web component" by
cd ../web
# try "Installing web dependendices" by
CXX=/usr/bin/g++-4.8 npm install
# try "Installing web dependendices" by
bower install
Und hier die Datei wast.start
innerhalb der Komponente “sis”, um den Befehl ./wast start sis
zu unterstützen:
#!/bin/sh
set -x
# try "Starting feedback app" by
cd web
LD_LIBRARY_PATH=/srv/www/vhosts/wast/components/sis/build/lib \
sudo -u wastd forever start -a app.coffee
+++ END DEPRECATED +++
wastd
Alle Skripte und Komponenten, die einen Webserver starten und lange laufen, sollen unter dem “Pseudo-Superuser” genannt wastd
gestartet werden.
Dieser “Pseudo-Superuser” hat den Vorteil, dass Webserver, die von jemand anderem gestartet wurden, von einer weiteren Person – im Falle des Falles – auch wieder ge-kill
-t und gestoppt werden können. Normale Prozesse, die von einem bestimmten User gestartet wurden, erlauben es nicht, von einem anderen User ge-kill
-t zu werden.
Der User wastd
ist aber nicht im echten Sinne ein Super-User mit root
-Rechten, sondern lediglich ein entsprechend ausgestatter User, der eben diese gemeinsame Nutzung von lange laufenden Prozessen über verschiedene User hinweg erlaubt.
In diesem Kapitel werden entscheidende Techniken vorgestellt, die besonders in fortgeschrittenen C++-Programmen eine große Rolle spielen können. Die folgenden Techniken kommen sehr viel zum Einsatz in wittfind und SIS.
In manchen Situationen bietet es sich an, C struct
s nach C++ zu wrappen.
Die entscheidenden Vorteile des Wrappens von C nach C++ sind:
struct
s (dieses ist auch bekannt unter dem Namen “RAII” – zu diesem siehe auch das nächste Kapitel sowie Definitionen)Selbstverständlich lassen sich nach dem Wrapping auch alle Vorteile von C++ bzw. C++11 (und, später C++14) nutzen. Darunter fallen u.a.:
Die Smart Pointers shared_ptr
und unique_ptr
: Diese legen u.a. feste Ownership zu Grunde. Damit wird verhindert, dass man vergisst ein struct wieder zu deallokieren und die Zuständigkeit wird festgelegt: wer soll den pointer am Ende wieder deallokieren?
Templatisierung
Vererbung und Interfaces und damit klare Strukturen: Mit C++ lässt sich viel stärker auf der Problem-Ebene arbeiten denn auf der reinen Sprach-Ebene
Grundlegend funktioniert das Adapter/Wrapper-Pattern immer auf die gleiche Art und Weise: Es gibt eine Klasse die gewrapped werden soll, den Adaptee und einen Wrapper, den Adaptor bzw. Wrapper.
Adapter Pattern
Der Wrapper hält eine Verbindung zum Adaptee und bildet die API nach. Der Client wird nur noch einen Adaptor bzw. Wrapper instantiieren und der Adaptor kümmert sich darum, den entsprechenden, eigentlichen Adapee zu konstruieren.
Wenn der Client Methoden des Adaptors verwendet, so wird dieser lediglich die Parameter an den Adaptee weiterleiten und die Ergebnisse, die vom Adaptee kommen wieder an den Client zurückleiten.
Im Fall von C++ bietet es sich natürlich an, den Adaptee als Pointer im Adaptor zu halten.
Ein mögliches, elegantes, sicheres und eigentlich simples Pattern um C struct
s nach C++ zu wrappen beschreibt (R. Martinho Fernandes 2013) mit seiner “Rule of Zero”. Eine konkrete Anwendung wird im Kapitel ab Seite sowie hier im Detail beschrieben.
Um dieses Pattern besser verstehen zu können soll an dieser Stelle noch einmal ein genuerer Blick darauf geworfen werden. Als Beispiel dient in diesesm Fall die Klasse VoidSequenceAdapter
, welche das C-Struct VoidSequence
wrapped.
VoidSequence
stellt in der C-Schicht eine Art std::vector
dar, indem es eine Liste von Elementen führt, und, falls der allokierte Speicher nicht ausreicht, neuen besorgt, um weitere Elemente in seinen Members einfügen zu können. Dabei speichert es beliebige Objekte, indem es sich als void*
verwaltet – letzteres setzt natürlich voraus, dass diese pointer später wieder entsprechend “zurückgecastet” werden.
Wie auch immer das struct im Detail aussieht, das Wrapping-Pattern bleibt stets identisch. Zunächst soll ein naiver Ansatz vorgestellt werden, welcher dann kontrastiert wird von dem “Rule of Zero”-Ansatz.
1 2 3 4 5 6 7 |
|
Dieser naive Ansatz zeigt das Wrapping-Pattern in klassischer Weise in C++: es wird ein Pointer zum Adaptee
gehalten. Der Pointer wird im Konstruktor hergestellt und im Destruktor deallokiert. Problematisch wird es, wenn der “Konstruktor” des Adaptee eine exception wirft: Konstruktoren sollen keine Exceptions werfen, da es sonst schwierig wird, bereits gelaufene Allokationen rückgängig zu machen.
Ein fortgeschrittenerer Ansatz ist der “Rule of Zero”-Ansatz von (R. Martinho Fernandes 2013): In diesem Fall ist auch die Ownership zur Adaptee
-Ressource geregelt, da es sich hier um einen unique_ptr
handelt.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
struct
s einVoidSequenceAdapter
, die das C struct
VoidSequence
wrappen wirdunique_ptr
ist unique_ptr<VoidSequence>
mit einem custom-Desktruktor. Letzterer wird aufgerufen, sobald der unique_ptr destruiert wird.&::VoidSequenceFree
bedeutet folgendes:
unique_ptr
verlangt, wenn man einen custom-DTOR angeben will, als zweites Template-Argument den Typ einer Funktion. Dieses wird durch decltype
generiert.decltype
wiederum wird den Typ der Funktion VoidSequenceFree
zurückliefern&
nimmt die Adresse der Funktion (liefert also einen Pointer zurück)::VoidSequenceFree
gibt an, dass es sich hier um eine namespace-lose Funktion handelt. (Also das Gegenstück von, z.B. std::
)adaptee_handle adaptee_
legt das member adaptee_
vom Typ adaptee_handle
an. Damit ist sichergestellt, dass zu dem Zeitpunkt, an dem die Destruktion des Objekts VoidSequenceAdapter
stattfindet, alle seine member destruiert werden. Damit wiederum wird der custom-DTOR des adaptee_handle
angestoßen und dieser ist ein VoidSequenceFree
, also ist die Rückbindung an die C-Schicht gegeben.VoidSequenceAdapter
nun wird das member konkret initialisieren: Als erstes Argument wird die Konstruktionsfunktion des C-struct
s mitgegeben, als zweites Argument die Adresse der Destruktionsfunktion des C-struct
s.VoidSequenceAdapter
s braucht nichts weiter – das member adaptee_handle
wird nach den Standard-Regeln von C++ destruiert werden.Policy-based design erlaubt es, eine Klasse, die verschiedene Policies kennt, auf viele Arten und Weisen zu konfigurieren und somit ein deutlich unterschiedliches Verhalten der Klasse erzeugen zu können.
Policies decken dabei unterschiedliche, orthogonale Konzepte ab und lassen sich miteinander kombinieren um eine Vielzahl von Funktionalitäten und Verhaltensweisen zu bewirken.
Vergleiche vor allem die Implementation der ersten Generation von Smart Pointern (auto_ptr
) welche zu den heutigen Standard-Smart-Pointern (shared_ptr
und unique_ptr
) geführt haben in (Alexandrescu 2000).
Um das Beispiel aus dem vorherigen Kapitel erneut aufzugreifen, soll an dieser Stelle auf die tatsächliche Implementation des adaptee_handle
eingegangen werden:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Abegesehen von den Template Meta Programmierungs-Anteilen (std::conditional
) und den Type Traits-Anteilen (ManagedAdaptee::value
) in diesem Beispiel soll an dieser Stelle der Policy-Based Ansatz (ManagedAdaptee
) genau betrachtet werden.
Kurz zur Wiederholung: Nach der Defintion in Zeile 4, kann ein VoidSequenceAdapter
auf folgende Arten und Weisen instantiiert werden und dabei ein sehr unterschiedliches Verhalten aufweisen:
1 2 3 4 |
|
Wie im vorherigen Kapitel besprochen, wird die Klasse VoidSequenceAdapter
– im Standardfall – den Speicher des Structs adaptee_
automatisch am Ende seiner eigenen Lifetime delete
n, indem es die Funktion VoidSequenceFree
aus der C-Schicht aufrufen wird.
Nun kann es aber ganz bestimmte Situationen geben, in welchen dieses Verhalten nicht nur unerwünscht sondern fehlerhaft wäre. In diesen Fällen lässt sich ein weiteres Template-Argument, Unmanaged
mitgeben, um die Klasse derart anders zu gestalten, dass sie kein (!) automatisches delete
von VoidSequence
ausführen wird.
Dabei spielen folgende Teile zusammen:
class Managed : std::true_type {};
ist eine bereits fertige Klasse ohne weitere Implementation. Sie stellt einen “sprechenden” strong type dar, der sich als Template-Argument nutzen lässt. std::true_type
stellt einen Typ aus C++11 dar, der folgendem Typ entspricht: std::integral_constant<bool, true>
. Das heisst, es handelt sich hier um einen “strong type” vom Typ bool mit der Belegung true
, dessen einfachste Implementierung folgende sein könnte (der Standard übernimmt dieses aber):
template<class T, T v>
struct integral_constant {
static constexpr T value = v;
};
Hiermit wiederum lassen sich entscheidende Weichen (zur Compile-Zeit!) stellen, um VoidSequenceAdapter
entweder Managed
oder Unmanaged
zu machen. Um zur Compile-Zeit die Klasse VoidSequenceAdapter
anders zu konfigurieren lässt sich Managed::value
abfragen, um zu sehen, wie die Klasse konfiguriert werden soll (vergleiche hierzu das “Type Traits”- sowie das “Template Meta Programmierungs”-Kapitel).
In diesem elaborierteren Fall wird also der Typ des adaptee_handle
zur Compile-Zeit entschieden und kann – je nach Typ von ManagedAdaptee
– eines der beiden folgenden werden:
std::unique_ptr<VoidSequence, decltype(&::VoidSequenceFree)>
oder schlicht:
VoidSequence*
Mit diesem Policy-Based Ansatz ist es also möglich, eine Klasse zu schreiben, die mehreren Ansprüchen – je nach Bedarf – gerecht werden kann: Im Standardfall ist VoidSequenceAdapter
eine Adapter-Klasse, welche den RAII-Prinzipen folgt; sollte jedoch ein anderes, möglicherweise orthogonales, Verhalten benötigt werden, so lässt sich dieses ebenso umsetzen und der restliche Code, der VoidSequenceAdapter
nutzt, kann diesen auch weiterhin wie gewohnt nutzen.
Zu diesem Kapitel wird empfohlen auch die anderen Kapitel Template Meta Programmierung, SFINAE, Type traits, und Silent Degredation genau zu beachten; vor allem aber (Alexandrescu 2000), (Di Gennaro 2012) und (Abrahams and Gurtovoy 2005) genau zu studieren.
Type traits sind im wesentlichen schlicht typedef
s in einer Klasse, die man von außen abfragen kann.
Ein kleines Beispiel soll dies kurz darstellen:
Anstatt folgendes zu schreiben:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
läßt sich auf einfach der typedef
value_type
von std::vector
nutzen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
Type traits lassen sich insbesondere im Zusammenspiel von verschiedenen Klassen nutzen und ermöglichen es, unterschiedliche Klassen unabhängiger voneinander zu halten, dabei aber für mehr Typsicherheit zu sorgen.
/// @brief meta-class to handle StringType type traits (using static assertions)
template<class T>
struct ElementSize {
static constexpr bool T_is_string_type =
std::is_base_of< std::string, T>::value
|| std::is_base_of<std::wstring, T>::value ;
static_assert(T_is_string_type,
"\n\n\tYou must instantiate this class in question with std::string,"
"\n\tstd::wstring, or any class derived thereof.\n"
);
typedef T type;
typedef T string_type;
typedef typename T::value_type value_type;
typedef typename std::conditional<
std::is_same<std::string,T>::value, // if StringType is std::string
std::fstream, // then stream_type is std::fstream
std::wfstream // else it is std::wfstream
>::type stream_type;
static constexpr unsigned int value =
sizeof(typename string_type::value_type);
};
Template Meta Programmierung besteht aus mehreren Teilen die in den nächsten Kapiteln besprochen werden
Eine in “Metaprogrammierungskreisen” häufig gesehene Abkürzung ist SFINAE – wobei dieses Monstrum an Abkürzung für “Substitution failure is not an error” steht (“Substitution Failure Is Not an Error” 2014).
Bei SFINAE geht es um folgendes Prinzip:
Man macht sich die Garantie des Standards zu nutze, dass, für den Fall, in dem ein Template-Argument ungültig ist, kein Fehler ausgelöst, sondern dieser schlicht übergangen wird.
Ganz grundsätzlich geht es bei SFINAE um folgendes:
If an invalid argument or return type is formed when a function template is instantiated during overload resolution, the function template instantiation is removed from the overload resolution set and does not result in a compilation error.
Kurz gesagt:
In der Standard-Library von C++11 gibt es z.B. eine Meta-Funktion std::enable_if<>
.
Ihre Definition wird in etwa so aussehen:
template <bool Condition, typename T = void>
struct enable_if {}; // no members
template <typename T>
struct enable_if<true, T> { using type = T; }; // type member
Beispiel-Aufruf:
typename std::enable_if<std::is_floating_point<T>::value>
Wenn nun T
von einem is_floating_point
-Typ ist, so ist ein Type Trait ::type
definiert, ansonsten nicht.
Dieses Konstrukt nun lässt sich an verschiedenen Stellen einsetzen um z.B. unter bestimmten Bedingungen Funktionen ein- und auszuschalten, oder unter bestimmten Bedingungen andere Überladungen anzubieten:
std::enable_if kann als zusätzliches Argument einer Funktion verwendet werden (aber nicht bei Operatoren-Überladungen)
als Rückgabewert (daher aber nicht Konstruktoren und Destruktiren)
oder als Klassen-Template oder Funktionstemplateparameter
Als Hintergrundlektüre siehe auch:
Unter silent degredation bzw. “incomplete instantiation” versteht man folgendes Prinzip: Wenn eine Member-Methode einer Template-Klasse nie verwendet wird, so wird diese auch niemals instanziiert. (Alexandrescu 2000, S.13)
Damit lassen sich also höher-geordnete Klassen schreiben, die nach wie vor kompilierfähig bleiben, obwohl einige ihrer Member-Methoden eigentlich ill-formed wären – aber nur, falls diese benutzt werden würden!
Angenommen man hat eine Klasse DoItAll
mit folgender implementation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Angenommen, DoItAll
würde folgendermaßen genutzt werden:
1 2 3 4 |
|
Oder auch so:
1 2 3 |
|
…so wäre bis hierhin alles in Ordnung und würde sauber kompilieren – trotz dessen, dass std::string / std::string
ill-formed und kein gütliger Aufruf wäre!
Das Geheimnis dahinter ist, dass Methoden von Template Klassen erst in dem Moment konkret instantiiert werden, in dem sie auch tatsächlich genutzt werden, d.h. wenn an keiner Stelle doitall_str.div("some string")
aufgerufen wird, wird diese Methode auch nicht instantiiert und die Klasse bleibt kompilierfähig.
Dies ist das Geheimnis hinter silent degredation bzw. “incomplete instantiation” und erlaubt wiederum sehr flexible Klassen zu gestalten.
Mit C++11 gibt es eine Möglichkeit, Kompilierzeit zu sparen, indem man den Compiler mit extern template
anweisen kann, sich darauf zu verlassen, dass eine volle Instantiierung/Spezialisierung einer Klasse an anderer Stelle vorgenommen wird und an dieser Stelle nicht zu instantiieren.
Im Normalfall wird nämlich die template class X
überall dort, wo sie verwendet wird, vom Compiler (u.U. auch viele Male) instantiiert nur um am Schluss alle diese – bis auf eine – wieder zu verwerfen.
C++11 erlaubt eine einmalige Spezialisierung auf die sich andere Code-Teile stützen können mit:
extern template class std::vector<MyClass>;
Allerdings beisst sich nun dieses Konzept mit der “silent degredation” da durch dieses Vorgehen eine volle Spezialisierung und damit eine volle Instantiierung vorgenommen wird.
Beim static dispatching bzw. auch tag dispatching geht es darum, verschiedene Funktionen (oder Members) mit tags zu annotieren und, je nach Bedarf, gezielt auszuführen.
Angenommen, man hat das folgende Problem:
Es gibt ein Klassen-Template template<class T> Foo
welche eine Member-Methode bar()
hat.
Nun gibt es zwei unterschiedliche Instanziierungen für den Container Foo
: einmal als Foo<std::string>
und einmal als Foo<int>
.
Basierend auf dieser Instanziierung sollen nun zwei komplett unterschiedliche Methoden (im Sinne von unterschiedlichen Implementierungen) der Methode bar()
aufgerufen werden.
Dies ließe sich – in diesem Beispiel –, auch durch partielle Spezialisierung erreichen, dennoch greift das Beispiel in dem Sinne, als dass die tag dispatching-Technik auch in anderen Fällen eingesetzt werden kann; nicht nur in diesem Fall.
Ein member-dispatch-pattern, um dieses zu erreichen könnte z.B. so aussehen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
es gibt einen zentralen Einstiegspunkt, die Methode bar()
: die Methode bar()
ist mit dem Attribut [[dispatcher]]
markiert und zeigt damit an, dass bar()
keinerlei Logik hält, sondern lediglich weiterleiten (dispatchen) wird, zu den anderem Methoden, welche die Logik beinhalten. Das Attribut [[dispatcher]]
hat keinerlei Funktionalität, ausser, dass sie dem Anwender einen Hinweis darauf gibt, was diese Methode macht.
ebenso wie es einen [[dispatcher]]
gibt, gibt es also nun Logik-tragende Funktionen oder Methoden, zu welchen dispatched wird, diese sind mit dem Attribut [[dispatched_to]]
markiert – wiederum ist dieses Attribut lediglich ein Hinweis an den Anwender und könnte dementsprechend ausgelassen werden, es tragt keine weitere Funktionalität. Die beiden Logik-tragenden Methoden sind:
int bar(tag<int>)
undstd::string bar(tag<std::string>)
bei einem genauen Blick auf den Funktionsteil des [[dispatcher]]
sieht man, dass es die ge-tag-ten Methoden [[dispatched_to]]
aufruft, bzw. an diese weiterleitet. Diese letzteren Methoden sind implementationsabhängig, basierend auf dem type T
, mit welchem die Klasse Foo<>
instanziiert ist.
template<class T> struct tag{};
welche auch wiederum zum größten Teil nur “deskriptiven” Charakter hat, um anzudeuten, dass hier ein tag-dispatching stattfindet.auto bar() [[ dispatcher ]]
diesen Tag derart, dass es:
tag<T>
anlegt – durch uniform initialization: tag<T>{}
nun aber ist der Weg frei, je nach Instanziierung entweder int bar(tag<int>)
oder std::string bar(tag<std::string>)
aufzurufen.
Schlussendlich wird auch noch innerhalb der dispatchenden Methode der Rückgabewert offengehalten und richtet sich nach dem Rückgabewert der logik-tragenden, [[dispatched_to]]
-Methode: ->decltype(this->bar(tag<T>{}))
Offensichtlich lässt sich mit den [[Attributen]]
spielen und diese sind variabel hinsichtlich dessen, wo diese genau platziert werden.
Attribute wurden in C++11 eingeführt, um den Weg auf Sprachebene dahingehend zu öffnen und standardisieren, dass der Compiler (zu einem späteren Zeitpunkt) diese Attribute in der Kompilierung nutzen kann – bisher jedoch werden diese von den gängigen Compilern großflächig ignoriert, sind aber bereits nutzbar, wie in diesem Beispiel; machen aber lediglich für den Anwender Sinn.
Die Implementation von diesem Code-Stück findet sich hier.
Siehe hierzu das eigene Kapitel Node Wrapping.
Unter Serialisierung versteht man das Abspeichern einer konkreten, instanziierten Klasse – henau genommen der Klassentyp, seine Member und die Vererbungskette – auf der Festplatte, um diese Klasse in genau diesem Zustand zu einem späteren Zeitpunkt wiederherstellen zu können.
Für das Serialisieren und Deserialisieren in C++ gibt es mehrere Möglichkeiten:
zu verwenden, kann mehrere Nachteile haben:
ist eine weitverbreitete, d.h. gut bekannte und in vielen Projekten getestete Serialisierungsbibliothek. Boost Serialization kann u.a. auch raw pointer serialisiern, wenn solche als Klassenmember verwendet werden und Vererbungsketten bei der Deserialisierung richtig auflösen. Leider ist es z.T. etwas komplex und die Dokumentation nicht gerade leicht zu verstehen; dafür aber ist es mit eine der mächtigsten Serialisierungsbibliotheken.
cereal ist eine junge Serialisierungsbibliothek und verwendet v.a. Features aus C++11, wie z.B. Template Meta Programmierung, um weitreichende Annahmen über Datentypen machen zu können. (Dies ist vor allem darum wichtig, weil C++ keine Introspection hat bzw. haben kann).
cereal’s API ist relativ einfach zu verstehen und einzusetzen, weshalb cereal in SIS eingesetzt wird, um den C++-Teil der Automaten auf der Festplatte zu speichern und wieder zu laden.
Hinweis: in SIS werden nur die C++-Schicht der Automaten von cereal serialisiert; für die C-Schicht werden die C-eigenen Serialisierungsmethoden verwendet. Da beide Schichten und die darin befindlichen Klassen aufeinander aufbauen, ist hierbei aber tunlichst darauf zu achten, dass beide (De-)Serialisierungen ordentlich aufeinander abgestimmt sind! Siehe die entsprechenden Kapitel bei SIS für eine tiefere Dokumentation zu diesem Thema.
ist eine Idee und ein Proof of Concept von Florian Fink und Daniel Bruder, um einzelne Container gezielt durch ein- und die selbe API im Binärformat auf die Festplatte schreiben und von dort wieder laden zu können. Dies wird ermöglicht durch C++11 und Template Meta Programmierung und SFINAE. Teile hiervon werden im folgenden gleich beschrieben werden.
Die wesentlichen Formate, um Klassen, deren Members und Vererbungsketten auf Festplatte zu speichern und später wieder zu laden sind v.a. die drei folgenden:
cereal und boost serialization unterstützen jeweils alle 3 der hier genannten Formate.
Selbstverständlich ist das Byte-Format das platzsparendste unter den Formaten, dabei auch das am kompliziertesten “zu debuggende”, sollten Fehler auftreten. JSON ist sowohl platzsparend als auch bequem zu lesen. XML ist am meisten “verbose”.
Beim Serialisieren in das Byte-Format gibt es eine gängige Praxis die hier beschrieben werden soll.
Im Binärformat muss der Anwender das byte-layout genau kennen, um etwas sinnvolles damit anfangen zu können – die Grafik in ab Seite soll dies verdeutlichen.
Beispiel für ein Byte-Layout
In diesem Fall werden 4 Variablen gespeichert:
char
unsigned int
bool
std::vector<std::string>
mit der Größe 2:
std::string
eine size() == 4
std::string
eine size() == 6
Die ingesamte Größe des Binärformats beträgt dann 19 Byte.
unsigned int
beträgt 4 Byte (auf einem 64-Bit-System)bool
belegt 1 Byte: Obwohl ein bool
theoretisch nur 1 Bit statt einem ganzen Byte (8 bit) bräuchte, tritt hier das Phänomen des Padding auf und das bit wird aligned (siehe dazu http://en.wikipedia.org/wiki/Data_padding)std::vector<std::string>
ist folgendermaßen aufgebaut:
vector
enthält, also mit vector.size()
std::string
s an: 4
string2.size() == 6
char
s des zweiten string
s des vector
und die Informationen sind vollständig.Vergleiche http://www.boost.org/doc/libs/1_55_0/libs/serialization/doc/index.html
Die API von cereal ist relativ einfach – für einen tiefergehenden Einblick vergleiche das Kapitel Zweistufige Serialisierung in SIS: Symmetric Index Structures.
Im Grunde ist es lediglich notwendig eine Methode der folgenden Art in seiner Klasse zu schreiben:
Angenommen, die Klasse hat folgendes Member list_
:
#include <vector>
#include <string>
class WordList {
public:
typedef std::vector<std::string> list_t;
/* Constructors, Methods, etc... */
private: // Members
list_t list_;
};
So reicht es bereits aus, die notwendigen header von cereal und eine Methode serialize()
folgendermaßen zu schreiben:
// This method lets cereal know which data members to serialize
template<class Archive>
void serialize(Archive & archive) {
archive( list_ ); // serialize things by passing them to the archive
}
Die Klasse sieht nun also so aus:
#include <vector>
#include <string>
#include <cereal/types/string.hpp>
#include <cereal/types/vector.hpp>
class WordList {
public:
typedef std::vector<std::string> list_t;
/* Constructors, Methods, etc... */
private:
list_t list_;
public:
template<class Archive>
void serialize(Archive & archive) {
archive( list_ );
}
};
Nun kann die Klasse von aussen serialisiert und deserialisiert werden:
#include <iostream>
#include <fstream>
#include <string>
#include <cereal/archives/binary.hpp>
#include "WordList.hpp"
int main() {
WordList wl;
// Elemente zum Container hinzufügen
std::string baz{"baz"};
wl.add("foo");
wl.add(std::string{"bar"});
wl.add(baz);
std::cout << wl.size() << std::endl;
// Binär-Datei anlegen, in welche serialisert werden soll
std::ofstream os{"out.bin", std::ios::binary};
// cereal-Archiv auf die Binär-Datei anlegen
cereal::BinaryOutputArchive binary_archive_file{ os };
// Serialisierbaren Container in das cereal-Archiv geben
binary_archive_file( wl );
return 0;
}
Nach XML oder JSON zu serialiseren funktioniert genau nach dem selben Schema:
int main() {
/* Container füllen ... */
// JSON nach std::cout serialisieren:
cereal::JSONOutputArchive json_archive_cout{std::cout};
json_archive_cout( wl );
/* ... */
}
Das Einlesen ist wiederum genauso einfach:
#include <iostream>
#include <fstream>
#include <string>
#include <cereal/archives/binary.hpp>
#include "WordList.hpp"
int main() {
/* Es gibt eine Binär-Serialisierte WordList ... */
std::ifstream is{"out.bin", std::ios::binary};
// cereal Input-Archiv auf diese Datei festlegen
cereal::BinaryInputArchive binary_archive{ is };
// leeren Container anlegen
WordList wl;
// Container aus Binär-Datei laden
binary_archive(wl);
return 0;
}
generic serialize erlaubt es, beliebige Standard-Container (mit beliebiger Schachtelung) individuell mit save
zu speichern und mit load
zu laden. Es kümmert sich automatisch darum, die richtige Speichermethode für den Container und die “Tiefe” des Containers zu finden.
Die API von generic serialize ist ebenfalls sehr einfach und überschaubar. Sie funktioniert so:
namespace usb = util::serialize::binary;
/* Example: Write nested container to a binary file */
std::list<std::map<std::string, unsigned>> list = {
{{"foo", 1}, {"bar", 2}},
{{"baz", 1}, {"quux", 2}}
};
/* save */
std::ofstream os{"file.bin", std::ios::out | std::ios::binary};
usb::save(os, list);
os.close();
Das Einlesen von Binärdaten mit generic serialize ist ebenfalls sehr einfach:
namespace usb = util::serialize::binary;
/* Example: Read nested container from binary file, way #1 */
std::ifstream is{"file.bin", std::ios::in | std::ios::binary};
std::list<std::map<std::string, unsigned>> list;
usb::load(is, list);
Ein anderer Weg wäre es, den Container-Typ als template-Argument anzugeben und C++11’s auto
zu nutzen:
namespace usb = util::serialize::binary;
/* Example: Read nested container from binary file, way #2 */
std::ifstream is{"file.bin", std::ios::in | std::ios::binary};
auto list = usb::load<
std::list<std::map<std::string, unsigned>>
>(is);
Auf diese Art und Weise werden beliebige Container in einem Format wie in der Grafik ab Seite angedeutet, angelegt.
Im wesentlichen bedient sich generic serialize der Techniken der Template Meta Programmierung und den Hilfestellungen von http://flamingdangerzone.com/cxx11/2012/05/29/type-traits-galore.html.
Dabei funktioniert der Speicher-Algorithmus folgendermaßen:
Wenn ein Container vorliegt (gesteuert von den Prinzipien von SFINAE und EnableIfContainer<T>
), dann betrachte rekursiv alle Elemente ([Type Traits][]: value_type
) dieses Containers, so lange bis du auf einen fundamentalen Datentyp stößt: int
, char
, o.ä.
Sobald ein Container bis auf seine fundamentalen Datentypen zurückgeführt, ist, so kann dieser Type binär gespeichert werden:
template <typename T, EnableIfFundamental<T>...>
T
save(std::ofstream & os, T t) {
os.write(reinterpret_cast<const char*>(&t), sizeof(t));
return t;
}
Hierbei wird zuerst die Größe des Containers und danach die einzelnen fundamentalen Datentypen gespeichert, z.B.:
template <typename T, EnableIfContainer<T>...>
T
save(std::ofstream & os, T t) {
unsigned size = t.size();
save(os, size);
for (auto it = begin(t); it != end(t); ++it) {
save(os, *it);
}
return t;
}
Das Einlesen funktioniert mutatis mutandis.
Ein Container liegt vor – und aktiviert nach den Prinzipien von SFINAE – die entsprechende save
/load
Funktionalität wenn folgendes Konstrukt einen gültigen Typ zurückliefert:
template<typename C>
struct is_container<C,
typename is_of_type<
typename C::const_iterator(C::*)() const,
&C::begin,
&C::end
>::type
> : std::is_class<C> { };
D.h. es wird, um einen Container zur Compile-Zeit zu erkennen vom type C
folgendes erwartet:
std::is_class<C>
ist ein gültiger TypeC
hat eine Methode begin()
und eine Methode end()
welche einen const_iterator
zurückliefern.Für mehr Details vergleiche https://github.com/xdbr/cpp-generic-serialize.
Node-Wrapping bezeichnet das wrappen von C++-Klassen derart, dass diese innerhalb von Javascript verwendbar gemacht werden.
Im Rahmen von SIS wird dies konkret vorgeführt: die dokument-indexierenden Automaten auf der C++-Ebene werden von der Javascript-Ebene (und damit dem node-Webserver) nur ein einziges Mal deserialisiert und ab dort als vollwertige Automaten im Speicher gehalten.
Vom node-Webserver aus nun lassen sich die C++-Automaten ansprechen und benutzen.
Um dieses language-binding zu bewerkstelligen bietet node den sog. “addon”-Mechanismus, welcher es eben erlaubt, C++-Klassen mit Javascript zu verbinden.
Die Verbindung dieser beiden Sprach-Welten läuft dabei in beide Richtungen: sowohl lassen sich aus der C++-Welt Javascript-Objekte bilden und diese nach Javascript “pushen”, als auch C++-Objekte auf der Javascript-Seite konstruieren, laden und dergleichen.
Möglich wird dies durch die derzeit schnellste Javascript-Implementation, V8. V8 ist eine Javascript-Implementation in C++ und wird von Google open source bereitgestellt. (V8 ist übrigens auch im Browser Chrome für die Javascript-Ausführung zuständig.) Über diese Bridge nun können alle oben genannten Aufgaben zwischen den beiden Sprach-Welten wahrgenommen werden.
Das Kapitel Web-Ebene im Teil SIS zeigt in genauerer Weise ein Beispiel für die Verwendung der C++-Automaten auf Javascript-Ebene.
Da das Schreiben von Node-Addons keine ganz simple Sache ist, empfiehlt es sich die Dokumentation hierzu sehr genau zu studieren und sich unbedingt so nahe wie möglich an die vorgeschlagenen Patterns und Beispiele in der Dokumenetation zu halten: http://nodejs.org/api/addons.html
auf Seite zeigt die schematische Darstellung eines node-wrap.
Node Wrapping Pattern
Diese Demonstration hält sich sehr eng an das Pattern “Wrapping C++ Objects” aus der node-addon-Dokumentation (http://nodejs.org/api/addons.html#addons_wrapping_c_objects) und beschreibt noch einmal die einzelnen Schritte etwas deutlicher als es die Dokumenation selbst tut.
Angenommen, es gibt eine einfache Klasse WordList
(bzw. WL
) mit folgendem Header (und den entsprechenden Definitionen zu den Methoden in einem passenden .cpp
-File):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Diese Klasse soll nun also nach node gewrapped werden, damit sie folgendermaßen innerhalb von Javascript verwendet werden kann:
1 2 3 4 5 6 7 8 9 10 |
|
Um dieses zu erreichen sind folgende Schritte nötig:
addon.cpp
schreibenWLNodeWrap
, die von node::ObjectWrap
erbt und WL
als Member enthält schreibenbinding.gyp
, die build-Datei für node-gyp
, schreiben*.node
Datei entsprechend in Javascript einbinden und benutzenaddon.cpp
schreibenEine kleine Datei addon.cpp
wird die gesamte Struktur von C++ nach Javascript verbinden, und sieht – dem Beispiel der Dokumentation folgend –, so aus:
#ifndef BUILDING_NODE_EXTENSION
#define BUILDING_NODE_EXTENSION
#endif
#include <node.h>
#include "WLNodeWrap.hpp"
using namespace v8;
void InitAll(Handle<Object> exports) {
WLNodeWrap::Init(exports);
}
NODE_MODULE(WLNodeWrap, InitAll)
Hier wird festgelegt, dass ein Modul mit dem Namen WLNodeWrap
definiert wird, und, sobald dieses per require
(var addon = require('./build/Release/WLNodeWrap');
auf Javascript-Seite aufgerufen wird, zur Funktion InitAll
gesprungen werden soll.
Die Funktion InitAll
wiederum wird WLNodeWrap::Init
aufrufen, welches wir sogleich definieren werden.
WLNodeWrap
, die von node::ObjectWrap
erbt, schreibenDie Wrapper-Klasse WLNodeWrap
wiederum orientiert sich sehr stark an der o.g. node-Dokumentation.
Im wesentlichen erbt sie von node::ObjectWrap
und exportiert eine static-Methode Init
. (Diese static Methode ruft der vorherige Schritt auf: NODE_MODULE -> WLNodeWrap -> InitAll -> WLNodeWrap::Init
!)
Desweiteren wird die eigentliche, zu wrappende Klasse WL<std::string>
der Klasse WLNodeWrap
als Member hinzugefügt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Selbstverständlich gibt es eine passende Datei WLNodeWrap.cpp
, welche diese Deklarationen des Header definiert, auch hier kann man sich wieder sehr eng an die node-Dokumentation halten:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
|
Zeilen 1–17: Diese Zeilen sind direkt aus der node-Dokumentation
Zeilen 18–23: An dieser Stelle wird das Mapping vorgenommen, welches besagt, dass auf Javascript-Seite eine Methode add
vorhanden sein wird, welche auf WLNodeWrap::Add
zeigt. Dort wiederum wird dann auf das eigentliche add()
, nämlich WL.add()
weitergeleitet werden. (Das selbe gilt für size() [JS-Seite] -> WLNodeWrap::Add [JS-V8-C++-Bridge Ebene] -> WL::size() [echte C++-Ebene]
)
wl_
, die “eigentliche” C++-Klasse angesprochen, und auf dieser nun die Methode add(const std::string&)
aufgerufen.Jede Methode auf der V8-Ebene muss seinen Scope
schließen und dieses als Rückgabewert zurückliefern. Zeile 59 zeigt, wie man ein “void
” zurückgeben kann.
Die Zeilen 66–69 zeigen erneut, wie sich size()/Size()/size()
von Javascript bis in die C++-Ebene verknüpfen lassen.
Die Zeilen 48–51 führen den “klassischen Dreisatz” vor, wie Strings von Javascript bis in C++-Strings umgewandelt werden.
binding.gyp
, die build-Datei für node-gyp
schreibenNicht zuletzt muss noch eine Datei binding.gyp
angelegt werden, welche – ganz wie ein Makefile
–, die build-Datei für node-gyp
darstellt.
node-gyp
ist ein spezielles build-Tool (vergleiche Build Systeme) für “node-addons” und kümmert sich um das notwendige linking zu den benötigten node- und v8-libraries.
Zum vorhandenen Projekt sieht die binding.gyp
folgendermaßen aus (es geht in dieser binding.gyp
v.a. auch darum, C++11 transparent, über verschiedene Betriebssysteme hinweg verwenden zu können):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
|
Hiermit ist es also möglich, wie oben gezeigt, die C++-Klasse WL
von Javascript aus anzusprechen:
1 2 3 4 5 6 7 8 9 10 |
|
Die Vorteile von Test-geleiteter Entwicklung sind mannigfaltig. Unter anderem erlaubt eine Test-Suite folgendes:
Test Driven Development geht dabei einen zunächst ungewöhnlich erscheinenden Weg: es werden zuerst die (failenden) Tests geschrieben, und danach erst die dazu passende Implementation (also die Implementation, die den Test “pass”-en lässt).
Der Workflow im TDD ist also kurz gesagt folgender:
Interessanterweise erlaubt TDD zumeist eine schnellere Entwicklung im Projekt, eine langfristig gesehen stabilere, ausgereiftere und nachhaltigere Code-Base und macht dabei auch noch Spaß.
Obwohl es zunächst paradox erscheinen mag, so gibt es zwar einen klaren Mehraufwand, zuerst die Tests und danach die Implementation zu schreiben, durch das vorherige schreiben der Tests aber
Zumeist sind die sog. Unit-Tests die wichtigsten, bei einem größeren Projekt wie diesem jedoch ist es denkbar, volle Integration Tests, Sanity-Tests, etc. parat zu haben.
Ein Unit-Test bildet klassischerweise die Funktionalität einer Klasse in einer Isolierten Art und Weise ab.
Idealerweise bildet der Unit-Test zu einer Klasse zugleich auch ihre Spezifikation ab und prüft diese anhand der Fixtures durch.
Angenommen, es gibt eine Klasse string
, und diese Klasse soll eine Methode append(char c)
besitzen, welche einen Buchstaben an den String anhängt.
Die Spezifikation sagt also:
string
string::append(char c)
soll einen Buchstaben an den bestehenden String anfügenDie Fixture enthält vorgegebene Eingaben und die erwarteten Ausgaben/Rückgabewerte. Die stringAppendFixture
wird genau dieses verhalten abbilden:
In einer schematischen Darstellung:
class stringAppendFixture {
string teststring1_append_in = "foobar";
string teststring1_append_out = "foobarc";
};
Jede Fixture ist einem Feature zugeordnet. Das Feature ist ein kleiner Testfall, der die Funktionen der getesteten Klasse verwendet, die Eingaben der Fixture an diese füttert und die Rückgaben anhand der Fixture überprüft.
Die Klasse stringFeature
wird also die Spezifikation von string::append
prüfen (erneut in schematischer Darstellung):
class stringAppendFeature {
EXPECT(
stringAppendFixture::teststring1_append_in.append('c')
== stringAppendFixture::teststring1_append_out
);
// weitere tests der Spezifikation...
};
Diese Sorte von kleinen Tests zu schreiben ist sehr einfach und dabei aber hochgradig effektiv!
Bald nämlich wird einem auffallen, dass es sinnvoll wäre, zu testen, was passiert, wenn der string vorher leer ist:
class stringAppendFixture {
string teststring1_append_in = "foobar";
string teststring1_append_out = "foobarc";
string teststring2_append_in = "";
string teststring2_append_out = "c";
};
class stringAppendFeature {
EXPECT(
stringAppendFixture::teststring1_append_in.append('c')
== stringAppendFixture::teststring1_append_out
);
// hier gibt es schon den zweiten Testfall!
EXPECT(
stringAppendFixture::teststring2_append_in.append('c')
== stringAppendFixture::teststring2_append_out
);
// weitere tests der Spezifikation...
};
Erfüllt der Test die Spezifikation? Was wäre denn nun mit dem Fall, dass der String auf \n
endet? Soll der char c
hinter dem \n
angefügt werden, das newline möglicherweise komplett ersetzen, oder sich womöglich vor das newline einsetzen?
Ein Test wird Klärung bringen, und, en passant die Spezifikation der Klasse string
festigen! Sei die Spezifikation nun erweitert um folgende Regel(n):
string
string::append(char c)
soll einen Buchstaben an den bestehenden String anfügenDie Tests könnten nun erneut refactored werden und folgendes ergeben:
class stringAppendFixture {
std::map<string/*input*/, string /*output*/> fixture{
{ "foobar", "foobarc" },
{ "", "c" },
{ "foobar\n", "foobarc\n" },
};
};
class stringAppendFeature {
const stringAppendFixture F;
for (const auto & f: F.fixture) {
EXPECT( f.first == f.second );
}
};
Integration Tests testen in den meisten Fällen das Zusammenspiel von einzelnen Klassen oder Software-Komponenten. Wie im vorherigen Abschnitt gezeigt wurde, sind diese einzelnen Klassen bereits durch die Unit-Tests in sich getestet. Der Integration Test nun testet diese einzelnen Kompomeneten in ihrem Zusammenhang, ihrer Kooperation etc. Diese Art von Tests wird aber zumeist eher ab einer umfangreicheren Größe des Projekts benötigt.
Neben Unit- und Integration-Tests gibt es auch noch die sog. Sanity-Tests. Diese Art von Tests validiert und prüft User-Eingaben, Datentypen und ähnliches auf korrekte Werte.
E2E-Tests, bzw. End-to-end-tests sind sehr ähnlich zu Integration Tests, indem sie das Zusammenspiel der einzelnen Komponenten prüfen, haben aber eher ein konkretes Interaktions-Szenario des Benutzers im Blick: oft werden e2e Tests in Web-Frameworks und in der Entwicklung von Webseiten verwendet, um ein komplettes “Szenario” der Benutzer-Interaktion durchzuspielen und zu prüfen, ob dieses von Anfang bis Ende fehlerlos ablaufen kann.
Beispiel:
Der Bug-Tracker dient der gemeinsamen Verwaltung von Bugs und der Versionierung
Obwohl eigentlich einleuchtend und selbstverständlich haben “ordentliche” Bug-Tracking best practices eine Reihe entscheidender Vorteile:
Im folgenden eine Beschreibung der Konventionen und Vorgänge, wie bugs innerhalb von WAST gehandhabt werden sollen.
Bugs sollen ordentlich dokumentiert werden und getagged werden.
Taggen erlaubt es, die offenen bugs zu sortieren, filtern und damit besser im Überblick behalten zu können.
Ordentlich dokumentieren bedeutet, dass ein nachfolgender genau nachvollziehen kann, wie/wann/wer mit einem Bug umgegangen wurde.
Dazu gehören:
[Issued -> Analyze]
. Dies würde bedeuten, dass sich Person Foo mit dem Bug auseinandersetzt und den tag Issued
entfernt und durch den tag Analyze
ersetzt hat. Dies bedeutet für alle anderen, dass:
[p1 -> p2]
.
[+p1]
Bugs sind am besten Priorisiert von “P1” bis “P4”.
Die Prioritäten entsprechen ungefähr folgenden Ideen:
Priorität | Label | Charakterisierung | Beschreibung |
---|---|---|---|
1 | P1 | Showstopper. | Muss sofort behoben werden. Kritischer Bug. |
2 | P2 | Wichtiger Bug. | Sollte ebenfalls schnellstmöglich behoben werden. Wichtiger Bug. |
3 | P3 | Durchaus wichtiger Bug. | Durchaus wichtiger Bug, aber nicht unbedingt wesentlich solange noch P1 und P2 Bugs vorliegen. Können auch Feature Requests sein, die dann zumeist zu P4-Bugs werden. |
4 | P4 | Nicht so wichtiger Bug. | Meist “Nice To Have” Feature Requests oder wird häufig vergeben für “Next Release/Milestone”, d.h. dieses Feature wird wohl erst im nächsten Milestone umgesetzt werden. |
Der Workflow orientiert sich an folgendem Modell: Der Bug geht durch verschiedene Phasen, die unterschiedliche Bedeutung haben. Diese werden immer per tag signalisiert. So können andere ablesen, dass jemand an einem bug arbeitet. fasst dies zusammen.
Bug Tracking Workflow Status
Wenn es mehrere “related” Bugs zu einem größeren Thema gibt, so bietet es sich an einen umbrella-Bug zu definieren, der alle diese verstreuten, kleineren Bugs zusammenfasst und miteinander referenziert. Die wird am Einfachsten so gehandhabt, dass:
Wenn man in der Beschreibung des Umbrella Bugs eine kleine Beschreibung anlegt, die sagt ‘this is an umbrella bug for bugs #19, #22, #53’, so wird in den referenzierten Bugs dieses automatisch von gitlab eingetragen: ‘this bug was mentioned in bug XY’. Dies ist ziemlich praktisch.
In praktisch jedem Software-Projekt ist Versionskontrolle unerlässlich um Sicherheit zu garantieren und Übersichtlichkeit zu wahren.
Hinsichtlich der Auswahl von Versionskontrollsystemen gibt es eine Vielzahl von Produkten, die sich verwenden lassen:
Die meisten Teile der WAST-Landschaft sind mit git versioniert da dieses System die meisten Vorteile bietet.
Als beliebteste Versionskontrollsysteme sind natürlich svn und git zu erwähnen. git ist ein “version control system (vcs)” wie svn, bzw. ein source control management System (scm), mit dem entscheidenden Unterschied, dass es im Gegensatz zu svn ein verteiltes (distributed) vcs ist.
Diesen entscheidenden Unterschied zu begreifen kann durchaus einen Moment brauchen und die Umstellung, bis man sich in git wirklich “zu Hause” fühlt, kann einen Moment dauern.
Trotz allem aber hat git die Entwickler-Welt im Sturm erobert und das aus guten Gründen.
Die Kombination aus git und gitlab als online-Kollaborations-Oberfläche bietet eine Menge an überzeugenden Vorteilen für die gemeinsame Entwicklung von Software, wie im folgenden kurz dargestellt werden soll.
Das freie “git Book”53 schreibt dazu:
The Git feature that really makes it stand apart from nearly every other SCM out there is its branching model.
Git allows and encourages you to have multiple local branches that can be entirely independent of each other. The creation, merging, and deletion of those lines of development takes seconds.
This means that you can do things like:
Frictionless Context Switching. Create a branch to try out an idea, commit a few times, switch back to where you branched from, apply a patch, switch back to where you are experimenting, and merge it in.
Role-Based Codelines. Have a branch that always contains only what goes to production, another that you merge work into for testing, and several smaller ones for day to day work.
Feature Based Workflow. Create new branches for each new feature you’re working on so you can seamlessly switch back and forth between them, then delete each branch when that feature gets merged into your main line.
Disposable Experimentation. Create a branch to experiment in, realize it’s not going to work, and just delete it - abandoning the work—with nobody else ever seeing it (even if you’ve pushed other branches in the meantime).
Notably, when you push to a remote repository, you do not have to push all of your branches. You can choose to share just one of your branches, a few of them, or all of them. This tends to free people to try new ideas without worrying about having to plan how and when they are going to merge it in or share it with others.
There are ways to accomplish some of this with other systems, but the work involved is much more difficult and error-prone. Git makes this process incredibly easy and it changes the way most developers work when they learn it.54
Small and Fast Git is fast. With Git, nearly all operations are performed locally, giving it a huge speed advantage on centralized systems that constantly have to communicate with a server somewhere.
Git was built to work on the Linux kernel, meaning that it has had to effectively handle large repositories from day one. Git is written in C, reducing the overhead of runtimes associated with higher-level languages. Speed and performance has been a primary design goal of the Git from the start.55
Distributed One of the nicest features of any Distributed SCM, Git included, is that it’s distributed. This means that instead of doing a “checkout” of the current tip of the source code, you do a “clone” of the entire repository.
Multiple Backups This means that even if you’re using a centralized workflow, every user essentially has a full backup of the main server. Each of these copies could be pushed up to replace the main server in the event of a crash or corruption. In effect, there is no single point of failure with Git unless there is only a single copy of the repository.
Any Workflow Because of Git’s distributed nature and superb branching system, an almost endless number of workflows can be implemented with relative ease.56
Whereas every other version control system in the world tracks file history (when lines were added to a file, when lines were removed, when a file was renamed, added or deleted etc) Git doesn’t explicitly track this information in any way at all. It instead tracks content within a repository. By walking the repository history Git can reconstruct what happened to a particular file, but this is an extrapolation from the data rather than something explicitly encoded.
This is a radical departure that means that when you do something like cutting a method from one file and pasting it into another then the history of the method goes along with it (think svn blame). This is explained by Linus Torvalds in this post; see also this separate post on tracking renames.
First off, let’s just posit that “files” do not matter. The only thing that matters is how “content” moved in the tree. Ok? If I copy a function from one file to another, the perfect SCM will notice that, and show it as a diff that removes it from one file and adds it to another, and is still able to track authorship past the move.57
Nicht zuletzt erlaubt es git auf einfachste Weise ein eigenes vollständig-funktionierendes code-repository aufzusetzen. Bei svn wird für diesen Schritt ein server mit laufendem svn benötigt – und ist damit zumeist an einen Administrator gebunden, z.B. um ein neues repository zu erstellen.
Ein einfaches git init --bare
reicht aus, um ein vollständiges git repository anzulegen, auf welches man seine Änderungen pushen und mit anderen kollaborieren kann. Im Endeffekt ist dies bereits ein kleiner “git server” – wenn man so will:
It is incredibly easy to start managing code with Git. Unlike Subversion there is no need to set up a repository on a (possibly remote) server before starting to work. Git is even simpler than SVK, because what would normally be considered a “working copy” actually becomes a (distributed) repository itself, containing both the “working copy” and all of the history of the repository58
Um schneller mit git arbeiten zu können werden im folgenden die wesentlichen ersten Schritte detailliert erklärt und erläutert. Auf jeden Fall wird empfohlen, weitere Literatur zu diesen Themen zu konsultieren.
Im Gegensatz zu svn gibt es bei git mehr Ebenen als nur “local repository” und “upstream repository”.
auf Seite gibt ein Überblick59 über die unterschiedlichen Ebenen und die Interaktion zwischen denselben:
Git overview
Nachdem man seinen Namen und seine email-Adresse in git konfiguriert hat, kann man bereits beginnen, produktiv zu arbeiten.60
Nehmen wir als Beispiel ein Projekt genannt eos
, das Satzende-Erkennung behandeln soll.
Als erstes legen wir ein Verzeichnis an, in dem das Programm entwickelt werden soll.
> mkdir eos && cd $_
Nun legen wir eine Datei README
an, welche die wesentlichen Punkte des repositories erläutert:
> cat <<HERE >README
eos
---
an end-of-sentence recognizer
HERE
Nun machen wir aus dem reinen Verzeichnis ein repository…
> git init
In diesem Befehl wird git ein (verstecktes) Verzeichnis .git
in unserem Projekt anlegen, also in eos/.git
, in welchem es das repository verwaltet.
Als nächstes machen wir sogleich den ersten commit, um unserem repository eine “Geschichte” zu geben – ab hier beginnt nun also die Versionierung unseres Projekts:
> git commit -am 'initial commit'
Dabei stellen die (kombinierten) Kommandozeilen Switches folgendes dar:
-a
git commit -a -m
verwendet werden)
-m MESSAGE
log
eingetragen wird
Als nächstes werden wir gleich unsere erste Datei in das repository einfügen, wollen davor aber noch kurz den aktuellen status
des repository betrachten:
> git status
Hierauf wird git etwa folgendes antworten (je nach Spracheinstellung):
# On branch master
#
# Initial commit
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# README
nothing added to commit but untracked files present (use "git add" to track)
Was dies im einzelnen bedeutet:
master
(mehr zum Thema branching, s.u.)git add
.gitignore
)git add
und zählt die ihm unbekannten Dateien auf (in diesem Fall lediglich die Datei README
).Nun also zum Hinzufügen der ersten Datei:
> git add README
Mit diesem Befehl wurde die Datei aber noch nicht endgültig in das repository aufgenommen: vielmehr wurde die Datei in die sog. staging area (In auf Seite “Index” genannt) übernommen und wird beim nächsten commit endgültig in das repository aufgenommen werden.
Ein weiterer Vergleich des vorherigen Status mit dem jetzigen Status verdeutlicht dies:
> git status
…liefert uns nun folgenden Output:
# On branch master
#
# Initial commit
#
# Changes to be committed:
# (use "git rm --cached <file>..." to unstage)
#
# new file: README
#
Da dies die einzige Änderung ist, die wir in diesem Changeset ausführen wollen (iwr könnten auch noch weitere Dateien adden und erst dann commiten), machen wir einen commit:
> git commit -am 'add documentation'
Nun wird…
> git status
…folgendes melden:
# On branch master
nothing to commit (working directory clean)
d.h. wir befinden uns einem “clean-state”, d.h. alle aktuellen Änderungen sind bekannt und es sind keine weiteren Änderungen an irgendwelchen Dateien unseres repository vorgenommen worden.
Um diese letzteren Änderungen zu verfolgen betrachten wir als nächstes den Output von git log
:
commit 9112df662257116774d946cb5482116263cbc511
Author: AUTORENNAME <EMAILADRESSE></emailadresse>
Date: DATUM
add documentation
Anders als svn hat git keine “klaren” Revisions-Nummern wie r1
etc., sondern statt dessen 40-stellige SHA-Summen für jeden commit.
Dies hat mehrere Vorteile (vergleiche Daten-Sicherheit[^siehe oben, und hier [[link einfügen]]]), aber auch einen entscheidenden Nachteil: sie sind menschlich schlecht lesbar.
Es gibt mehrere Möglichkeiten, sich nicht diese 40-stelligen Nummern merken zu müssen, um, z.B. um mit jemand anderen über eine bestimmte Revision reden zu können.
Hierzu gibt es verschiedene Strategien, die git anbietet, z.B.: Abkürzen der SHA-Summe auf wenige Stellen (solange der damit gemeinte commit eindeutig bleibt). In der Regel einigt man sich auf die ersten 6 Stellen wenn man über einen konkreten commit sprechen will, in diesem Fall also:
9112df
Nebenbemerkung für Interessierte: Es gibt einen sogenannten git plumbing command (sozusagen ein Kommando um mit git selbst zu arbeiten, das aber gewöhnlich nicht im Alltag gebraucht wird (dies wären die porcelain commands)), das anzeigt, was git aus einer solchen abgekürzten Revisions-Nummer herausliest:
> git rev-parse 9112df
9112df662257116774d946cb5482116263cbc511
Eine weitere Strategie, Revisionsnummern menschlich zugänglicher zu machen, besteht darin, Aliases zu verwenden.
Dies kann geschehen durch:
HEAD
, HEAD^
, HEAD^^
um jeweils auf den letzten, vorletzten, vor-vor-letzten commit zu referenzierenHEAD@{1}
, HEAD@{2}
um auf die selben commits zu referenzierengit tag
. So könnte man also einen ganz bestimmten 40-stelligen commit als v1.2.3
taggen und somit genau das gleiche meinen.Als nächstes wollen wir in der Entwicklung unseres Satzende-Erkenners fortfahren und dabei eines von git
’s wichtigsten features nutzen, das branching, d.h. wir zweigen von der Hauptentwicklungslinie weg, um eine neue Funktionalität zu entwickeln und werden diese, sobald sie fertig ist, wieder in die Hauptentwicklungslinie zurückführen (mergen). git ist im Vergleich zu svn sehr sparsam beim Anlegen von neuen branches und fördert das Anlegen und mergen von branches sehr.
Als erstes wollen wir einmal gucken, ob es denn vielleicht zufällig schon branches gibt:
> git branch
* master
Offensichtlich gibt es derzeit nur einen branch, und zwar den branch master
.
Vorsicht! Git zeigt in diesem Fall nur unsere lokalen branches an, nicht aber potentielle branches die auf servern liegen! Bisher haben wir noch keine sog. remote
s (also entfernte repositories, wie zum Beispiel ein “Zentral-”Repository) eingetragen, aber um potentiell “entfernte” branches anzeigen zu lassen, brauchen wir folgenden Switch:
> git help branch
[...]
-a, --all
List both remote-tracking branches and local branches.
[...]
Da aber keine remotes existieren wird das Ergebnis von git branch -a
also das selbe ergeben wie git branch
(nur lokale branches anzeigen):
> git branch -a
* master
Nun also soll endlich ein branch für die Entwicklung mit dem Namen dev
angelegt werden:
> git branch dev
Ein erneuter check, welche branches existieren, zeigt:
> git branch
dev
* master
…was bedeutet, dass es einen branch namens dev
gibt, der aktuelle branch aber noch auf master
steht. Das wollen wir ändern:
> git checkout dev
Switched to branch 'dev'
Wie erwartet, haben wir in den branch dev
gewechselt und können dies auch überprüfen:
> git branch
* dev
master
Hier also können wir neue Funktionalität entwickeln, ohne die Hauptentwicklungslinie zu beeinträchtigen.
Legen wir also als nächstes unsere ersten Dateien des Satzendeerkenners an, mit folgenden Inhalten:
> cat eos.pl
#!/usr/bin/env perl -0 -p
s/
(
(
(
(Mr|[[:upper:]]|\d+)\. # Ausnahmen: Abkürzungen etc.
|
.+? # alles andere (non-greedy,
# d.h. 1. nicht [.!?] gefolgt von SPACE + Großbuchstabe)
# 2. keine Abkürzung
)+? # eine Folge von Abkürzungen oder "alles"
[.!?](?=\s+[[:upper:]]) # EOS
)
)
/$1<EOS>/gx;
EOS
> cat eos.t
Ein Satz? Hier auch! Am 19. August 1972 schläft
Mr. G. W. Spencer. Hier beginnt ein Satz. In der
Oettingenstr. 67 stehen Computer.
Perl arbeitet hier zeilenweise, Sätze dürfen nicht über Zeilen gehen.
TXT
Als nächstes machen wir das Programm ausführbar:
> chmod u+x eos.pl
Und legen noch ein Verzeichnis für Ergebnisse von unterschiedlichen Testläufen an – diese Ergebnisse und das gesamte zugehörige Verzeichnis werden wir aber nicht in unseren commits haben wollen und ignorieren:
> mkdir var
> cat eos.t | ./eos.pl > var/res.1
Die Ausgabe von git status
ist mittlerweile also schon vertraut:
> git status
# On branch dev
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# eos.pl
# eos.t
# var/
git teilt uns also mit, dass es zwei Dateien und ein Verzeichnis in unserem Projekt gibt, dass es noch nicht kennt, die wir aber per git add
zu unserer Versionskontrolle hinzufügen können.
Da wir das Verzeichnis für Zwischenergebnisse, aber nicht tracken und lieber ignorieren wollen, tun wir folgendes: wir legen eine Datei .gitignore
an, die alle Dateien und Verzeichnisse listet, die git ignorieren soll und fügen diese in das repository hinzu.
echo var/ >> .gitignore
Nun zeigt git status
foldenes interessantes Ergebnis:
git status
# On branch dev
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# .gitignore
# eos.pl
# eos.t
git hat schon die Datei .gitignore
ausgewertet und zeigt das Verzeichnis var
gar nicht mehr als potentiell-hinzuzufügende Datei an!
Um schöne kleine commits zu haben (die man im Notfall später leichter “zurückspielen” kann), werden wir die fehlenden Dateien nur Stück für Stück zum repository adden:
> git add .gitignore
> git commit -am 'add gitignore'
[dev 1f986bd] add gitignore
1 file changed, 1 insertion(+)
create mode 100644 .gitignore
> git status
# On branch dev
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# eos.pl
# eos.t
nothing added to commit but untracked files present (use "git add" to track)
> git add eos.*
> git commit -am 'add first version of eos + test data'
[dev 46b34ab] add first version of eos + test data
2 files changed, 19 insertions(+)
create mode 100755 eos.pl
create mode 100644 eos.t
> git status
# On branch dev
nothing to commit (working directory clean)
Nun können wir bereits die kurze History unseres repository anschauen, z.B. in diesem Format:
> git log --oneline --decorate --graph
* 46b34ab (HEAD, dev) add first version of eos + test data
* 1f986bd add gitignore
* 9112df6 (master) add documentation
…und sehen hierbei die Bezeichnungen für die jeweiligen branches auf denen die Änderungen gemacht wurden, und den aktuellen HEAD.
Interessant wird nun folgendes Experiment: Wir lassen uns ganz gewöhnlich von der shell den Inhalt des Verzeichnisses anzeigen, wechseln dann in den master-branch und schauen uns den Verzeichnisinhalt erneut an und vergleichen beide Ergebnisse.
> git branch
* dev
master
> ls -1
README
eos.pl
eos.t
var
> git checkout master
Switched to branch 'master'
> ls -1
README
var
> git checkout dev
Switched to branch 'dev'
> ls -1
README
eos.pl
eos.t
var
Das Ergebnis ist durchaus aus verblüffend zu bezeichnen: git hat tatsächlich auf File-System-Ebene (!) die Dateien “ausgetauscht” und das Verzeichnis komplett verändert.
Als nächstes nun wollen wir die Entwicklung unseres eos
hier beenden und den dev
-branch nun in den master
-branch zurück-merge
n. Dafür werden wir folgendes tun:
Auf den master
branch wechseln
> git checkout master
Switched to branch 'master'
> git status
# On branch master
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# var/
nothing added to commit but untracked files present (use "git add" to track)
den dev
-branch merge
n
> git merge dev
Updating 9112df6..46b34ab
Fast-forward
.gitignore | 1 +
eos.pl | 15 +++++++++++++++
eos.t | 4 ++++
3 files changed, 20 insertions(+)
create mode 100644 .gitignore
create mode 100755 eos.pl
create mode 100644 eos.t
Die vorhandenen Dateien prüfen
> ls -1
README
eos.pl
eos.t
var
Den log
prüfen
> git --no-pager log --graph
* commit 46b34ab61c7f23ed6d4fbb2baf7dca468bbfb07d
| Author: NAME <EMAIL>
| Date: DATE
|
| add first version of eos + test data
|
* commit 1f986bd9d98e04aa5ea73a68ea9cbf83a736da7a
| Author: NAME <EMAIL>
| Date: DATE
|
| add gitignore
|
* commit 9112df662257116774d946cb5482116263cbc511
Author: NAME <EMAIL>
Date: DATE
add documentation
den dev
-branch löschen (sonst entsteht bald Chaos!)
> git branch -d dev
Deleted branch dev (was 46b34ab).
Zu guter letzt wollen wir die Ergebnisse auf ein remote-repository pushen.
Dafür checken wir erstmal ob remote
s eingetragen sind:
> git remote -v
[kein output]
Daher tragen wir unsere erste remote-Adresse ein und nennen sie origin
:
> git remote add origin name@machine:path/to/bare/repository.git
> git remote -v
origin name@machine:path/to/bare/repository.git (fetch)
origin name@machine:path/to/bare/repository.git (push)
…dabei funktionieren die Angaben für remote-repositories genau wie beim ssh
-Protokoll:
name@machine
für den Zugang:
path/to/bare/repository.git
repository.git
an, dass dies ein sog. git “bare” repository ist, dazu an anderer Stelle mehr).Wenn das Verzeichnis existiert und die Pfadangabe stimmt, lässt sich nun auf dieses repository pushen:
> git push origin master
Hierbei pushen wir unseren branch master
auf das remote-repository mit dem Namen origin
.
Äquivalent dazu ist der pull, d.h. Änderungen (die womöglich von anderen) in das remote-repository ge-push
-t worden sind, abzuholen:
> git pull origin master
git-extras (visionmedia 2014) geben viele nützliche (neue) Kommandos zu den Standard git-Kommandos hinzu.
Dadurch kürzen sie einige Wege ab, oder erleichtern andere, routine-mäßige Aufgaben.
Da sie aber mit Hilfe der von git selbst zur Verfügung gestellten Kommandos implementiert sind, könnte man selbstverständlich alles auch ohne git-extras machen.
Dennoch können die git-extras als eine sinnvolle Empfehlung betrachtet werden.
Um die git-extras auf den CIP-Rechnern zu installieren kann man folgendes Verfahren nutzen:
git clone https://github.com/visionmedia/git-extras
cd git-extras
make DESTDIR=$HOME PREFIX="" install
Ich empfehle sehr, die git-extras61 zu verwenden und schlage folgendes Branching-Model vor:
Mit git-extras ist das wirklich einfach: Anstatt selbst einen Branch anzulegen, auf diesen zu wechseln, dort zu arbeiten, dann diesen Branch in den master-Branch zu mergen, bietet sich u.a. folgender Workflow an:
pltk@master$ git feature lemmatizer
pltk@feature/lemmatizer$ [hack hack (möglichst kleine Commits bitte!)]
pltk@feature/lemmatizer$ git commit -am 'last commit'
pltk@feature/lemmatizer$ git checkout master
pltk@master$ git feature finish lemmatizer
Bei Bug-Fixes schlage ich folgendes Modell vor:
# [In gitlab ist ein issue im bugtracker eröffnet]
pltk@master$ git bug issue-66
pltk@bug/issue-66$ [ hack hack ...]
pltk@bug/issue-66$ [ letzter commit ]
pltk@bug/issue-66$ git checkout master
pltk@master$ git feature finish issue-66
Die Versionierung (in Zusammenarbeit mit der gemeinsamen Bug Verwaltung) garantiert Referenzierbarkeit hinsichtlich der Zitierbarkeit für die Philosophie (und darüber hinaus).
Beispiel:
Das Versionsschema von WAST orientiert sich nach außen am Versions-Schema von Ubuntu. Dabei wrid die Versionsnummer zusammengesetzt aus der Jahreszahl und dem Monat des Releases.
Im oben gegebenen Beispiel hat sich X mit v14.02 auf die Version von WAST zum Zeitpunkt Februar 2014 bezogen. Die Version von November 2015 wird also 15.11 sein. Dabei ist ein monatlicher Release-Cycle gegeben.
Wie gesagt, dies ist die Versionierung nach außen, d.h. im Zusammenspiel aller Komponenten. In der Versionierung nach innen sollte sich jede der Komponenten selbst jedoch am Besten am Schema des Semantic Versioning orientieren.
Dementsprechend kann ein Release und die Version desselben genauer unterteilt werden:
Beispiel:
Im wewsentlichen versteht man unter semantic versioning eine Versionsangabe mit folgendem Format: Major.Minor.Patch.
Dabei stehen Major-Versionen für (z.T.) untereinander nicht-kompatible APIs oder große Veränderungen/Verbesserungen am Code, bzw. Verweisen auf wesentliche Entwicklungsschritte im Code.
Die Zahl unter Minor bezeichnet den aktuellen Entwicklungsstand des Codes. In den meisten Fällen werden als Minor ungerade Zahlen verwendet um anzudeuten, dass dies eine Entwickler-Version ist, und, nachdem diese Entwicklung eines bestimmten Features (oder ähnlichem) abgeschlossen ist, diese dann um eins hochgezählt zu einer geraden Zahl.
Nicht zuletzt wird Patch bei bugfixes hochgezählt.
Vergleiche z.B.
Mehr Informationen zu Sematic Versioning finden sich bei http://semver.org/(Tom Preston-Werner 2013)
Bei allen Entwicklungen – sowohl im kommerziellen wie auch in der “Public Domain” –, ist die Frage nach Lizenzen als sehr wichtig einzustufen.
Die Geschichte der “Open Source”- und “Free/Libre Software”-Lizenzen ist sehr interessant und in erster Linie hier nachzulesen [Lessig, Lessig, Stallman, Moody].
Kurz gesagt mussten sich die “Hacker” auch in Lizenzmodelle einarbeiten und diese “hacken” um ihre Software zu schützen, daher die vielen verschiedenen, z.T. sich widersprechenden und komplizierten Lizenzmodelle.
Im folgenden ein Kurzüberblick über die meistverwendeten Lizenzen und deren Charaktersitika – dies aber vollständig “ohne Gewähr”, um bei Entwicklungen innerhalb von WAST die richtige Lizenz wählen zu können.
[Lizenz-Wizard auch verlinken]
wast-list
– die allgemeine Mailingliste für WAST
wast-kurs
– Mailingliste für den WAST-Kurs von Max und Daniel (SoSe 2014)
Die Mailinglisten laufen über die IFI: https://lists.ifi.lmu.de/
wer ist drauf?
link von stefan
link zu mailing-list-best-practices bzw import von dort
Grundlegendes zu den Übungen:
Warum:
generieren sie ssh-Keys (mit oder ohne Passphrase), und legen Sie ihren Public(!)-Key an der dafür vorgesehenen Stelle in gitlab ab (im Notfall lesen Sie Dokumentation! Einmal: ‘generate ssh keys’ und die gitlab-Dokumentation: ssh keys adden.)
Installieren Sie die git-extras auf Ihrem und/oder den cip-Rechnern (dann wie [hier] beschrieben). Dieser Schritt ist empfohlen aber nicht zwingend: alles was die git-extras machen, lässt sich mit git alleine auch machen.
Schreiben Sie eine Datei im Markdown-Format in ihrem Ordner, in dokumentieren Sie in dieser ihre bisher geleisteten Schritte – so dass andere von Ihnen lernen könnten und klar ist, was Sie getan haben – es steht Ihnen dabei selbstverständlich frei, hierfür pandoc zu verwenden und damit zu spielen
Bieten Sie einen pull-request zum upstream-Repository an (das Repository, dass Sie am Anfang ge-clone-d hatten) und bitten Sie um Übernahme ihres “Patches”
~/.ssh
hinzu, entweder per Hand, oder mit ssh-copy-id
(Ihre Distribution liefert dieses möglicherweise schon mit, oder Sie können es sich bequem installieren wenn Sie möchten)ssh cip
statt ssh nutzer@cip.ifi.lmu.de
sagen zu können, um sich erfolgreich an den cip-Rechnern einzuloggenbyobu
(als Frontend für eines der vorgestellten vertraut) und ermöglichen Sie es sich dadurch, Terminal-Sessions auf den cip-rechnern laufen lassen zu können und zu diesen später zurückkehren zu können.App::assh
und versuchen Sie, eine permanente Verbindung zu den cip-Rechnern halten zu können (schalten Sie dafür Ihr Internet ab- und wieder-an, und gucken Sie, ob die Verbindung automatisch wieder hergestellt wird)Schreiben Sie eine C++-Klasse, die die volle Funkionalität des folgenden C-struct wrapped und abbildet (inklusive der Unterscheidung private/public Methode), mit vollen Konstruktoren und Destruktoren, die das C-Struct automatisch allokieren und de-allokieren
Das C-struct halt folgende Spezifikation:
typedef struct tWordList {
std::vector<std::string> words_;
} WordList;
// CTOR / DTOR
extern WordList * WordListInit();
extern void WordListFree(WordList*);
// "public methods"
extern void WordListAdd(WordList*, const char*);
extern int WordListSize(WordList*);
// "private methods"
static std::string WordListUpgradeWord(WordList*, const char*);
Schreiben Sie zunächst eine C++-Klasse, die:
public
und private
Methoden des C-Structs unterstützt und dabeiVerwenden Sie ihre Klasse aus der vorhergehenden Woche und schreiben Sie Makefiles
im cmake-Format diese.
Das cmake-file CMakeLists.txt
soll dabei folgendes unterstützen:
cstructlib
und cppstructlib
, wobei diese jeweils die beiden Klassen der letzten Woche enthaltenmain.cpp
kann gebaut werdenmain.cpp
hinzugelinktcmake .. && make cppstructlib
Verwenden Sie Ihre C++-Klasse und den cmake build-Prozess aus der letzten Woche und schreiben Sie Unit-Tests. Auch wenn TDD eigentlich anders herum funktioniert (Failing Test first – implementation to make the test pass later), sollten Sie dennoch ein gutes Gefühl für Unit-Testing und TDD bekommen. Nicht zuletzt ist es zum Einstieg dann doch leichter für eine bereits existierenden (hoffentlich auch funktionierende!) Klasse, Tests zu schreiben.
Die Tests sollen folgendes erfüllen:
(dokumentieren Sie bitte, wie die Tests zu starten sind)
Machen Sie sich schon einmal mit der zu lesenden Literatur für nächste Woche und, wenn möglich, bereits mit der Theorie für nächste Woche grundlegend vertraut
Makefile
oder ein Rakefile
, das per default
-Task die tests frisch kompiliert und laufen lässt [finden sie den link zu guten Beschreibungen von Rakefiles im entsprechenden Kapitel des Skripts]Verwenden Sie ihre C++-Klasse aus der vorangegangenen Woche und schreiben Sie eine Binär-Serialisierung für diese.
Zum Serialisieren können sie folgendes verwenden:
Erweitern Sie Ihre C++-Klasse aus der letzten Woche um folgende Methoden:
save(const std::string & filename)
load(const std::string & filename)
…wobei die beiden Methoden (möglicherweise als static
-Methoden!) die Klasse jeweils auf Festplatte speichern und wieder laden.
Wie immer: schreiben Sie Tests, die Ihre Ergebnisse verifizieren und mit einem Befehl ausführbar sind. Dokumentieren Sie Ihre Vorgehensweise.
const boost::filesystem::path & path
statt const std::string & filename
verwenden[Spezifikation]
Verwenden Sie Ihre C++-Klasse mit den Tests aus der vorangegangenen Woche
Nun schreiben Sie ein Node-Addon, das folgendes kann:
var addon = require('myWrappedCppClass')
(oder ähnlich) aufrufenaddon.add(element);
aufrufen und das Element wird in der Member-Variable der C++-Klasse abgespeichertconsole.log(addon.size());
die Anzahl von Elementen in der Member-Variable der C++-Klasse abfragenaddon.elements()
alle Elemente aus der C++-Klasse in ein Javascript-Array holen.Hinweise:
In dieser Übung werden Sie in die Template Meta Programmierung eingeführt. Dabei erlernen Sie, wie man Entscheidungen und – möglicherweise – sogar Berechnungen in die Compile-Zeit überführen kann.
In letzterem Falle bedeutet dies tatsächlich, dass zur Laufzeit bereits alle Berechnungen erfolgreich erledigt wurden und keine Zeit mehr benötigen!
Schreiben Sie eine templatisierte C++-Klasse template<class T> class MyClass
, die:
std::string
oder einem std::wstring
instanziiert wurde, dann sollen diese in einem std::vector<>
abgespeichert werdenint
, unsigned int
, unsigned long long int
, short
, float
, double
, … instanziiert wurde, sollen diese in einem std::set<>
gespeichert werdenstatic_assert
und einer entsprechenden Fehlermeldung abgelehnt werden.make && make test
) – zugegeben, manche Dinge lassen sich hier nur schlecht (bis kaum testen)Schreiben Sie ein type-trait, das für eine Klasse template<class T> Foo
zur Compile-Zeit (!)
std::string
, std::wstring
oder einer aus diesen abgeleiteten Klassen bestehtbool is_wide_string
Also:
namespace trait {
template<class T> mytrait {
static constexpr bool is_wide_string = /*...*/;
}
}
temlpate<class StringType>
class MyClass {
using is_wide_string = mytrait<StringType>::is_wide_string;
};
auto main() -> int { // C++11 only!
MyClass<std:: string> stringclass;
MyClass<std::wstring> wstringclass;
std::cout << std::boolalpha
<< stringclass::is_wide_string // false
<< std::endl
<< wstringclass::is_wide_string // true
<< std::endl;
return 0;
}
Schreiben Sie eine Komponente für LaaS, die “linguistics as a service”-Webapplikation.
Gehen Sie dazu wie folgt vor:
server.js
und in lib/controllers/api.js
an, und versuchen Sie zu verstehen, was hier passiert – letztendlich werden nur Daten vom Frontend (also von der Client-Seite entgegengenommen und ein Shell-Befehl ausgeführt und die dadurch gewonnen Daten an die Client-Seite zurückgegeben)app/scripts/controllers/tokenize.coffee
app/views/partials/tokenize.jade
uppercase
, lowercase
, camelCase
, CamelCase to snake_case
, strip
, Spracherkennung, reverse
, wordcount
, …Diese Dokumentation ist ein Work-in-Porgress-Produkt und daher:
Da die Dokumentation auch in einer kollaborativen Team-Arbeit erstellt und gepflegt werden soll, bitte bei Fehlern diese unbedingt korrigieren und per gitlab als Änderungen anbieten, danke!
Component: wf
Description:
Maintainer: flo@cis.lmu.de
Link external: wittfind.cis.lmu.de
Repository: gitlab.cis.lmu.de/wast/wf
Dependencies:
* `gcc > 4.7`
* boost
* ... (?)
* cmake
* (doxygen)
Component: wittfind-web
Description:
Maintainer: maximilian@cis.lmu.de et al.
Link external: wittfind.cis.lmu.de
Repository: gitlab.cis.lmu.de/wast/wittfind-web
Dependencies:
* git
* git-submodules
* wf
* PHP
* Apache
* E2E-dependencies:
* [^node] / [^npm]
* [^casperjs]
* [^phantomjs]
Component: SIS (frontend)
Description:
Maintainer: bruder.cis.lmu.de
Link external: sis.cis.lmu.de
Repository: gitlab.cis.lmu.de/wast/sis3/tree/master/web
Dependencies:
* MEAN-Stack
* [^node] / [^npm]
* [^expressjs]
* [^angularjs]
* [^bower]
* client-seitige dependencies
werden durch [^bower]
installiert
* [^npm]
* server-seitige dependencies
werden durch [^npm]
installiert
* MongoDB
Component: SIS (backend)
Description:
Maintainer: bruder@cis.lmu.de
Link external: sis.cis.lmu.de
Repository: gitlab.cis.lmu.de/wast/sis3/tree/master/src
Dependencies:
* `gcc > 4.7`
* cmake
* (doxygen)
* git
* git-submodules
* lest
* boost
* ... (?)
Component: "Alternativen-Suche"
Description: Ausformulierung von Alternativen
Maintainer: seebauerp@cip.ifi.lmu.de / lindinger@cip.ifi.lmu.de / TBD
Link external: wittfind.cis.lmu.de
Repository: gitlab.cis.lmu.de/wast/wittfind-web
Dependencies:
* perl-module (?)
* XML-Twig (?)
* ... (?)
* ... (?)
Component: Helppage
Description: Beispiel-Anfragen und Hilfe für wittfind
Maintainer: kreya@cip.ifi.lmu.de
Link external: wittfind.cis.lmu.de/help.php
Repository: gitlab.cis.lmu.de/wast/wittfind-web
Dependencies:
* PHP (Version?)
* ... (?)
* ... (?)
Component: Feedback
Description: Bug Reports für Aussenstehende
Maintainer: bruder@cis.lmu.de, TBD
Link external: wastfeedback.cis.lmu.de
Repository: gitlab.cis.lmu.de/wast/wast-feedback
Dependencies:
* curl
* gitlab user token / "FeedbackBot"-User
* MEAN-Stack
* [^node] / [^npm]
* [^expressjs]
* [^angularjs]
* [^bower]
* client-seitige dependencies
werden durch [^bower]
installiert
* [^npm]
* server-seitige dependencies
werden durch [^npm]
installiert
* MongoDB
Component: E2E
Description: End-to-end tests für wittfind-web: komplette User-Szenarien durchspielen
Maintainer: bruder@cis.lmu.de, TBD
Link external: ---
Repository: gitlab.cis.lmu.de/wast/wittfind-web/tree/development/tests
Dependencies:
* [^node] / [^npm]
* [^casperjs]
* [^phantomjs]
Component: Reader
Description: Reader-Applikation für Facsimile
Maintainer: lindinger@cip.ifi.lmu.de (et al.?)
Link external: ---
Repository: gitlab.cis.lmu.de/wast/wittfind-web/tree/development/tests
Dependencies:
* PHP (Version?)
* [^turn.js]
* ... (?)
Component: wast-doc
Description: Dokumentation für WAST
Maintainer: bruder@cis.lmu.de / TBD
Link external: www.cip.ifi.lmu.de/~bruder/wast/
Repository: https://gitlab.cis.lmu.de/wast/wast-doc
Dependencies:
* [make](https://www.gnu.org/software/make/manual/make.html)
* [pandoc](johnmacfarlane.net/pandoc/)
* [gpp](http://files.nothingisreal.com/software/gpp/gpp.html)
* [/github.com/typesetters/p5-App-pandoc-preprocess)
* [dot](http://www.graphviz.org/Documentation.php)/[neato](http://www.graphviz.org/pdf/neatoguide.pdf)
* [rdfdot](RDF::Trine::Exporter::GraphViz)
* [ditaa](http://ditaa.sourceforge.net/)
* [Image::Magick](http://www.imagemagick.org/) (image manipulation)
* [rsvg-convert](http://live.gnome.org/LibRsvg)
* [yuml](https://github.com/wandernauta/yuml) (install as python module using `pip install https://github.com/wandernauta/yuml/zipball/master` or `easy_install https://github.com/wandernauta/yuml/zipball/master`)
* [plantuml](http://plantuml.sourceforge.net/)
[^wf]: <https://gitlab.cis.lmu.de/wast/wf>
[^SIS-backend]: <https://gitlab.cis.lmu.de/wast/sis3/tree/master/src>
[^SIS-frontend]: <https://gitlab.cis.lmu.de/wast/sis3/tree/master/web>
[^wittfind-web]: <https://gitlab.cis.lmu.de/wast/wittfind-web>
[^feedback]: <https://gitlab.cis.lmu.de/wast/wast-feedback>
[^feedback-link]: <http://wastfeedback.cis.uni-muenchen.de/>
[^e2e]: <https://gitlab.cis.lmu.de/wast/wittfind-web/tree/development/tests>
[^reader]: <https://gitlab.cis.lmu.de/wast/wittfind-web/>
[^cmake]: <http://www.cmake.org/>
[^lest]: <https://github.com/martinmoene/lest>
[^node]: <http://nodejs.org/>
[^npm]: <http://nodejs.org/>
[^expressjs]: <http://expressjs.com/>
[^angularjs]: <https://angularjs.org/>
[^bower]: <http://bower.io/>
[^phantomjs]: <http://phantomjs.org/>
[^casperjs]: <http://casperjs.org/>
[^turn.js]: <http://www.turnjs.com/>
[^doxygen]: <http://doxygen.org/>
[^sis-link]: <http://sis.cis.lmu.de/>
[^wf-link]: <http://wittfind.cis.lmu.de/>
[^wittfind-link]: <http://wittfind.cis.lmu.de/>
[^helppage]: <https://gitlab.cis.lmu.de/wast/wittfind-web>
WAST Komponenten und Dependencies
Komponente | Teil | Personen | Von | Bis |
---|---|---|---|---|
SIS | alle 4 Schichten | Daniel Bruder | 01/2012 | dato |
In der Entwicklung der WAST-Landschaft arbeiten wir mit dem sehr bekannten und verbreiteten git branching model von dieser Seite69 — auf Seite fasst den git branching model workflow an dieser Stelle kurz zusammen.
git branching model
auf Seite verdeutlicht den Workflow einer CI, für mehr Informationen siehe auch das Kapitel Continuous Integration.
Continuous Integration
auf Seite gibt einen Überblick, wie die WAST-pipeline (in zunehmender Kombination mit dem Unified Deployment-Prozess) aussieht:
WAST Data Workflow
Graphik auf Seite fasst noch einmal als Überblick den Bug Tracking Workflow zusammen – für eine insgesamte Beschreibung siehe auch Kapitel [Bug Tracker][].
Bug Tracking Workflow
Es ist nicht nur schön anzusehen, sondern oft auch aufschlussreich, sich die Visualisierung der history per gource [webhinweis] anzusehen, um zu verstehen wie ein Projekt gewachsen ist
WAST
auf *AST
: Öffnung und Aufbereitung für andere Editionsprojekte.UML Legende: Class
UML Legende: Simple Association
UML Legende: Cardinality
UML Legende: Directional Association
UML Legende: Aggregation
UML Legende: Composition
UML Legende: Inheritance
UML Legende: Interface Inheritance
UML Legende: Dependencies
UML Legende: Interface
UML Legende: Class with Details
<#startdeprecated>
+++ END DEPRECATED +++
Defintion von Deploy
Wittgenstein Advanced Search Tools
laden immer nur bestimmte Teile der Seite neu und es gibt keinen kompletten Refresh der Seiten
siehe auch AngularJS
Pattern für Applikationen: Model View Controller
favorisiert Fat Client-Architektur
im Gegensatz zu DVCS
im Gegensatz zu VCS
Constructor
Destructor
Substitution Failure Is Not An Error
Resource Acquisition Is Initialization
Sitzung | Datum | Themen | Hausaufgabe | Vorbereitung für nächste Sitzung |
---|---|---|---|---|
1 | 11.04.2014 | Einführung / WAST: Projekt-Landschaft / Seminarplan / Hinweise zur Dokumentation | Übung 1: git | Techniken-Kapitel zum Thema Wrapping von C nach C++ / C-Wrapping Beschreibung in SIS |
N/A | 18.04.2014 | Stunde findet nicht statt | ||
2 | 25.04.2014 | C-Wrapping | Übung 2: C-Wrapping | cmake, TDD |
3 | 02.05.2014 | Build-Systeme / Test Driven Development | Übung 3: cmake + Übung 4: cmake | Einschlägige Kapitel zu Serialisierung |
4 | 09.05.2014 | Serialisierung: Serialisierung in C++ / Verschiedene Libraries zur Serialisierung / Verwendung von “cereal zur Serialisierung / Zweistufige Serialisierung in SIS | Übung 5: Serialisierung | Web-Apps / MEAN-Stack / Techniken: Node-Wrapping |
5 | (16.05.2014) | –Betriebsausflug– | ||
6 | 23.05.2014 | |||
7 | (30.05.2014) | |||
8 | 06.06.2014 | |||
9 | 13.06.2014 | |||
10 | (20.06.2014) | |||
11 | 27.06.2014 | |||
12 | 04.07.2014 | |||
13 | 11.07.2014 |
Abrahams, David, and David Gurtovoy. 2005. C++ Template Metaprogramming: concepts, Tools, and Techniques from Boost and Beyond. Boston: Addison-Wesley.
Alexandrescu, Andrei. 2000. Modern C++ Design: applied Generic Programming and Design Patterns. Boston, MA; London: Addison-Wesley.
Arseny Kapoulkine. 2014a. “pugixml.” pugixml. http://pugixml.org/.
———. 2014b. “zeux/Pugixml.” GitHub. https://github.com/zeux/pugixml.
Blumer, A., J. Blumer, D. Haussler, R. McConnell, and A. Ehrenfeucht. 1987. “Complete Inverted Files for Efficient Text Retrieval and Analysis.” J. ACM 34 (3) (July): 578–595. doi:10.1145/28869.28873. http://doi.acm.org/10.1145/28869.28873.
boost.org. 2014a. “Boost Test Library.” http://www.boost.org/doc/libs/1_55_0/libs/test/doc/html/index.html.
———. 2014b. “Serialization.” http://www.boost.org/doc/libs/1_55_0/libs/serialization/doc/index.html.
C++1y Standard Committee. 2014. “xdbr/Cpp-Make-Unique.” GitHub. https://github.com/xdbr/cpp-make-unique.
Daniel Bruder. 2012. “SIS – Symmetric Index Structures.” PhD thesis, LMU München. www.cip.ifi.lmu.de/~bruder/ma/MA/sis/MA-SIS-DB.pdf.
———. 2014. “xdbr/Cmake-Module-Submodule.Cmake.” GitHub. https://github.com/xdbr/cmake-module-submodule.cmake.
Daniel Bruder, Florian Fink. 2014. “xdbr/Cpp-Generic-Serialize / GitHub.” https://github.com/xdbr/cpp-generic-serialize.
“Day 1 Keynote - Bjarne Stroustrup: C++11 Style.” 2012. http://www.youtube.com/watch?v=0iWb_qi2-uI&feature=youtube_gdata_player.
Di Gennaro, Davide. 2012. Advanced C++ Metaprogramming. S.l.: s.n.].
Emscripten Authors. 2013. “Home · Kripken/Emscripten Wiki · GitHub.” https://github.com/kripken/emscripten/wiki.
expressjs. 2014. “Express - Node.Js Web Application Framework.” http://expressjs.com/.
Fink, Florian. 2013. “Programming of the Rule-Based Wf.”
Gaston, Gross. 1991. “La Form d’un Dictionnaire Electronique.” LADL-Report. Laboratoire d’Automatique Documentaire Et Linguistique.
Gerdjikov, Stefan, Stoyan Mihov, Petar Mitankin, and Klaus U. Schulz. 2013. “Good Parts First - a New Algorithm for Approximate Search in Lexica and String Databases.” arXiv:1301.0722 [Cs] (January). http://arxiv.org/abs/1301.0722.
“Git - Submodules.” 2014. http://git-scm.com/book/en/Git-Tools-Submodules.
Gitlab cis group. 2014. “Gitlab Cis Group / Gitlab.” Gitlab Cis Group / Gitlab.
Google. 2014. “AngularJS — Superheroic JavaScript MVW Framework.” http://angularjs.org/.
Gotscharek, Annette, Ulrich Reffle, Christoph Ringlstetter, Klaus U. Schulz, and Andreas Neumann. 2011. “Towards Information Retrieval on Historical Document Collections: the Role of Matching Procedures and Special Lexica.” International Journal on Document Analysis and Recognition (IJDAR) 14 (2): 159–171. doi:10.1007/s10032-010-0132-6. http://dx.doi.org/10.1007/s10032-010-0132-6.
Grassi, Marco, Christian Morbidoni, Michele Nucci, Simone Fonda, and Francesco Piazza. 2013. “Pundit: Augmenting WEB Contents With Semantics.” Literary and Linguistic Computing. Special Issue “Digital Humanities 2012: Digital Diversity: Cultures, Languages and Methods”. Edited by Paul Spence, Susan Brown and Jan Christoph Meister 28 (4) (December).
Guenthner, Franz, and Petra Maier. 1994. “Das CISLEX Wörterbuchsystem.” CIS-Bericht-94-76.
Inenaga, Shunsuke, Hiromasa Hoshino, Ayumi Shinohara, Masayuki Takeda, and Setsuo Arikawa. 2001. Construction of the CDAWG for a Trie.
Krey, Angela. 2013. “Semantische Annotation von Adjektiven Im Big Typescript von Ludwig Wittgenstein.”
Langer, Stefan, and Daniel Schnorbusch (Hrsg.). 2005. Semantik Im Lexikon. Tübingen: Narr.
Lindinger, Matthias. 2013. “Highlighting von Treffern Des Tools WiTTFind Im Zugehörigen Faksimile.”
Marco Rogers. 2013. “Marco Rogers: Creating Node Addons, C/C++ Extensions for Fun and Profit.” http://www.youtube.com/watch?v=q1ri36UI5GA&feature=youtube_gdata_player.
Martin Moene. 2013. “martinmoene/Lest · GitHub.” https://github.com/martinmoene/lest.
Maurice, Gross. 1997. “The Construction of Local Grammars.” Finite-State LanguageProcessing: 329–354.
Meyers, Scott. 2005. Effective C 55 Specific Ways to Improve Your Programs and Designs. - Includes Index. Upper Saddle River NJ: Addison-Wesley.
MongoDB. 2014. “MongoDB.” http://www.mongodb.org/.
node. 2014. “node.Js.” http://nodejs.org/.
Node API. 2014. “Addons Node.Js V0.10.25 Manual & Documentation.” http://nodejs.org/api/addons.html#addons_wrapping_c_objects.
Pichler, Alois. 2010. “„Towards the New Bergen Electronic Edition“.” Wittgenstein After His Nachlass. Edited by Nuno Venturinha: 157–172.
Pichler, Alois, H.W. Krüger, D.C.P. Smith, T.M. Bruvik, A. Lindebjerg, and V. Olstad. 2009. “Wittgenstein Source Bergen Facsimile Edition (BTE).” Wittgenstein Source. Bergen: WAB, Wittgenstein Source.
R. Martinho Fernandes. 2013. “Rule of Zero @ Flaming Dangerzone.” http://flamingdangerzone.com/cxx11/2012/08/15/rule-of-zero.html.
Rothhaupt, Josef G. F. 2006. “Zur Dringend Notwendigen Revision Des „Standard View“ Der Genese Der ‘Philosophischen Untersuchungen’.” Gasser, Georg / Kanzian, Christian / Runggaldier, Edmund (Hg.): Cultures: Conflict - Analysis - Dialogue. Papers of the 29th International Wittgenstein Symposium 2006, Kirchberg 2006: S. 278–280.
Rothhaupt, Josef G. F. 1996. “Farbthemen in Wittgensteins Gesamtnachlaß. Philologisch-Philosophische, Untersuchungen Im Längsschnitt Und in Querschnitten.” PhD thesis, Weinheim.
Schiller, Anne, Christine Thielen, and Simone Teufel. 1999. “Stuttgart Tübinger Tagset (STTS).” http://www.ims.uni-stuttgart.de/forschung/ressourcen/lexika/TagSets/stts-table.html.
Schmid, Helmut. 1994. “Probabilistic Part-of-Speech Tagging Using Decision Trees.” Proceedings of International Conference on New Methods in Language Processing, Manchester, UK.
Seebauer, Patrick. 2012. “Verbesserung Der Suche Im Wittgenstein-Nachlass. Suche in Alternierenden Texten.”
Strutynska, Olga. 2012. “Nachlass von Ludwig Wittgenstein: Optimierung Eines Digitalen Lexikons Und Semantische Kodierung Der Nomen. Evaluation Des Lexikons Mit Hilfe von Konkordanzanalysen.”
“Substitution Failure Is Not an Error.” 2014. Wikipedia, the Free Encyclopedia. http://en.wikipedia.org/w/index.php?title=Substitution_failure_is_not_an_error&oldid=581401656.
TEI Consortium. 2009. “Guidelines for Electronic Text Encoding and Interchange.” TEI Consortium.
Tom Preston-Werner. 2013. “Semantic Versioning 2.0.0.” http://semver.org/.
uscilab. 2014. “cereal Docs - Main.” http://uscilab.github.io/cereal/.
visionmedia. 2014. “git-Extras.” https://github.com/visionmedia/git-extras.
Volos, Lyudmyla. 2013. “Disambiguierung von Partikelverb – Konstruktionen Und Verbpräpositional – Konstruktionen Im Big Typescript von Ludwig Wittgenstein.”
At the time being, there are only two formats supported; one format for the web-service, and another format for the terminal.↩
Intel 2.90GHz Quadcore↩
On the web the limit is set to 25 for unregistered users.↩
I could show you the exact colour of the wallpaper if there was anything around that has this colour.↩
Shortcomings of the denomination of universality made by Frege and Russell.↩
I only mean, what I say.↩
zu finden in vendor/cautomata
↩
zu finden in src/
↩
zu finden in web/
↩
link: späteres Kapitel (in fussnote)↩
das Kürzel für MongoDB+Express+Angular+Node, verlgeiche hierzu auch das nächste Kapitel und das Kapitel [Teilbesprechungen][] und seine Unterkapitel [Der MEAN-Stack][] und [Web-Applikationen, Basics][]↩
gyp steht für “Generate Your Project” und wurde von Google für den Build-Prozess von Chrome entwickelt: um die Komplexität dessen zu ermessen, vergleiche: “Life Of A Chromium Developer” auf http://dev.chromium.org/Home↩
https://gitlab.cis.uni-muenchen.de/wast/wittgenstein-issues/issues↩
http://www.martinfowler.com/articles/continuousIntegration.html↩
unbedingt https verwenden, sonst kann es sein, dass man von http nicht weitergeleitet wird auf https!↩
Dieses Skript zum Beispiel wird ebenfalls durch ein Makefile gesteuert: dazu gehören neben dem Typesetten auch das Hochladen auf den webspace und andere Aufgaben.↩
MongoDB (2014)↩
expressjs (2014)↩
Google (2014)↩
node (2014)↩
node (2014)↩
vergleiche hierzu auch das Kapitel MVC im Abschnitt Design Patterns↩
https://github.com/typesetters/p5-App-pandoc-preprocess bzw. https://metacpan.org/release/App-pandoc-preprocess↩
https://github.com/typesetters/p5-App-pandoc-preprocess bzw. https://metacpan.org/release/App-pandoc-preprocess↩
https://gitlab.cis.uni-muenchen.de/David/stub-template-bathesis↩
https://gitlab.cis.uni-muenchen.de/David/stub-template-bathesis↩
https://gitlab.cis.uni-muenchen.de/David/stub-template-bathesis↩
siehe hierzu auch das Kapitel Integration Tests und E2E Tests. Inegration Tests prüfen das korrekte Zusammenspiel von Komponenten welche, einzeln für sich bereits alle ihre eigenen [Unit Tests][] bestehen. Mit E2E Tests beschreibt man komplette User-Szenarien, wie diese sich auf der Webseite bewegen und mit dieser interagieren. Hierbei wird auch gleichzeitig die Verfügbarkeit der Webserver und der auf diesen vorhandenen routes, etc. geprüft)↩
diese Graphik ist inspiriert von http://www.ndpsoftware.com/git-cheatsheet.html und dort interkativ mit Erklärungen zu den jeweiligen Ebenen und Interaktionen verfügbar↩
Zu diesem Punkt ist u.a. diese Hilfe sehr gut geeignet: https://help.github.com/articles/set-up-git#set-up-git↩
Martin Moene (2013)↩
boost.org (2014a)↩
Daniel Bruder, Florian Fink (2014)↩
uscilab (2014)↩
boost.org (2014b)↩
Node API (2014)↩
Marco Rogers (2013)↩