Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

Schorsch

Supermoderator

  • »Schorsch« ist der Autor dieses Themas

Beiträge: 5 145

Wohnort: Wickede

Beruf: Softwareentwickler

  • Private Nachricht senden

1

02.12.2011, 14:35

Haskel, Problem mit Parsern

Hey,
ich "muss" in der Uni in einem Kurs mit Haskell arbeiten. Nun, funktional ist glaube ich nicht ganz meine Welt, wobei ich mich mittlerweile immer besser in die Denkensweise einarbeiten kann. hauptsächlich lerne ich mit dem Buch "Programming in Haskell" von Graham Hutton, welches auf dem Gebiet ja zu den Standardwerken zählt. Ich versuche mich grad in Parser einzuarbeiten, da dass das nächste Thema ist, was ansteht. Dazu wird im Buch erst folgendes erstellt:

Quellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Parser a = String -> [(a,String)]

return :: a -> Parser a
return v = \inp -> [(v,inp)]

failure :: Parser afailure = \inp -> []
item :: Parser Char
item = \inp -> case inp of
    [] -> []
    (x:xs) -> [(x,xs)]

parse :: Parser a -> String -> [(a, String)]
parse p inp = p inp

(>>=) :: Parser a -> (a -> Parser b) -> Parser b
p >>= f = \inp -> case parse p inp of
    [] -> []
    [(v,out)] -> parse (f v) out



Es wird also ein Typ für Parser a festgelegt, welcher eine Liste mit Tupeln von a und Strings ausgibt. Gedacht ist es so, dass wenn ein Parser fehlschlägt, die leere Liste zurück gegeben wird. Im Normalfall, enthält die Liste ein Element, und für den Fall, dass der String auf verschiedene Möglichkeiten verarbeitet werden kann, kann man nun die verschiedenen Varianten in der Liste ausgeben. Soweit so gut.
Es werden dann ein paar Standardparser erstellt.
return schlägt nie fehl, und gibt den Parameter als result zurück. Der übergebene String (Siehe Lambda) wird im Tupel an 2ter Stelle zurückgegeben. Diese 2te Stelle ist dafür gedacht, einen Reststring zurück zu geben, falls nicht der ganze String vom Parser verarbeitet werden kann.
failure schlägt immer fehl und gibt die leere Liste zurück.
item schlägt mit leeren Strings fehl, gibt ansonsten das erste Zeichen als result zurück.
Die Funktion parse ist einfach nur als Hilfsfunktion gedacht, damit man die einzelnen Parser "einfacher" testen kann. Quasi sowas wie parse (return 1) "abc" was dann [(1, "abc")] zurück geben sollte.

Bis hier hin ist alles soweit klar, wobei er sich bei mir beschwert, wenn ich return aufrufen will, da return ja schon definiert ist. Finde ich schon merkwürdig dass es dann so in das Buch aufgenommen wird, aber zum testen habe ich die Funktion dann einfach in preturn umbenannt, für parser-return. Dann klappts auch soweit.

>>= ist als Operator für sequencing gedacht, damit man die einzelnen Parser hintereinander schalten kann. Hier wird wirds jetzt komisch.
Es wird ein Testparser erstellt:

Quellcode

1
2
3
4
5
p :: Parser (Char,Char)
p = do x <- item
       item
       y <- item
       return (x,y)


Das Prinzip dahinter ist mir soweit klar. Trotzdem finde ich es merkwürdig, dass ein Operator für sequencing erstellt wird und dann nicht genutzt wird. Wäre ja soweit kein Problem, doch er meckert.

Die Fehlermeldung:
No instance for (Monad ((-)) String))
arising from a use of 'return'
Als mögliche Korrektur wird mir vorgeschlagen, eine Deklaration für (Monat ((-)) String)) anzugeben.
Von Monaden stand bis jetzt noch nichts im Buch. Deshalb gehe ich davon aus, dass man hier für keine Monaden braucht. Sehe ich das richtig? Monaden werden weiter hinten im Buch zwar behandelt, aber es macht ja keinen Sinn, da in den Codestücken keine Monaden definiert wurden.

Nun geht das überhaupt ohne Monaden? Soweit ich weiß sind diese ja dafür da Funktionen nacheinander aufzurufen, von daher wäre das hier ja eigentlich ein Einsatzgebiet dafür. In der Vorlesung sind Parser jedoch auch vor Monaden geplant, von daher denke ich, dass es auch ohne gehen müsste. Kennt jemand eine Lösung für das Problem, oder geht das wirklich nur mit Monaden?
„Es ist doch so. Zwei und zwei macht irgendwas, und vier und vier macht irgendwas. Leider nicht dasselbe, dann wär's leicht.
Das ist aber auch schon höhere Mathematik.“

Beiträge: 721

Wohnort: /dev/null

Beruf: Software-Entwickler/Nerd

  • Private Nachricht senden

2

02.12.2011, 17:43

Ja, für die korrekte Ausführung deines Programmes müsstest du dir eine Monade definieren, aber das kannst du ja noch nicht(anscheinend?). Deine einzige Chance dein Programm korrekt zu implementieren ist ein zweischneidiges Schwert:
* Du musst damit dein Programm überhaupt erst kompiliert, die Prelude exkludieren, damit deine Funktionen "return" und "(>>=)" nicht mit der Prelude kollidieren. Der Compilerflag "XNoImplicitPrelude" sollte dir dabei helfen.
* Du musst den Syntactic Sugar "do" durch Nutzung von Bind ersetzen. "Do" ersetzt die Newlines nur durch ">>" und ">>=". Mehr dazu einfach mal ergooglen.("Haskell do notation").

Der Bind Operator ist ein typischer Monaden-Operator, genauso wie die Do-Syntax nur Syntactic-Surgar ist und return ebenfalls monadisch. Wenn man sich mal die typische Definition einer Monade anguckt, dann sieht man, dass die vorgegebene Struktur leicht in eine Monade übersetzt werden kann:

C-/C++-Quelltext

1
2
3
4
5
class Monad m where
    (>>=)  :: m a -> ( a -> m b ) -> m b
    (>>) :: m a -> m b -> m b
    return :: a -> m a
    fail :: String -> m a

"return" ist demnach ein monadischer Operator, der folglich auch einen monadischen Typ besitzt( "m a" ). Bind macht nichts anderes, als, stumpf gesagt, zwei Funktionen miteinander zu kombinieren. Das kann man sich relativ gut mit dem Typ-System von Haskell erschließen:

(>>=) :: m a -> ( a -> m b ) -> m b

Bind nimmt das Ergebnis der ersten Funktion und nutzt es für die Ausführung der nächsten Funktion. Wichtig ist, dass die Funktionen auch gar nicht ausgeführt werden müssen, das liegt alles im Ermessen des Bind-Operators. Der Bind-Operator ist also der Schlüssel zu Monaden, die in Kombination mit dem Typsystem den großen Reiz an Haskell ausmachen. Monaden bieten dir die Möglichkeit, dein Programm extrem von der Plattform und IO zu abstrahieren, sodass letztendlich eine Kern-Logik übrig bleibt. Wichtig für das Verständnis von Monaden sind Typklassen, weil Monaden auch nichts anderes als Typklassen sind. Desweiteren sollte dir bewusst sein, dass Monaden niemals sequentiell oder imperativ sind, das hängt alles von der Implementation der Monade ab. Gerade für Parsing eignen sich Monaden wunderbar, da du eine Reine von Definitionen deiner Zielsprache einfach hintereinander schreiben kannst und die Monade automatisch den Output der Parser-Funktion A mit dem Input der Parser-Funktion B verbindet. Desweiteren wird zum Beispiel IO über eine Monade geregelt.

Ich empfehle dir einfach mal mehr mit dem ghci zu arbeiten und so Dinge wie ":t DeinTyp" einzutippen. Der ghci ist essentiell für das schnelle Verständnis von Haskell. Du solltest dir außerdem mal ein anderes Buch aussuchen. "Real World Haskell" kann man Online frei lesen oder bestellen, das Buch ist wirklich gut und hat auch den richtigen Umfang.

Schorsch

Supermoderator

  • »Schorsch« ist der Autor dieses Themas

Beiträge: 5 145

Wohnort: Wickede

Beruf: Softwareentwickler

  • Private Nachricht senden

3

02.12.2011, 20:07

Hey,
vielen Dank für den langen Beitrag. Hat mir schon mal weitergeholfen. Muss mich aber auch entschuldigen;) Hab in dem Kapitel mal weiter gelesen. Am Ende kommt der Verweis, dass mit Monaden gearbeitet werden muss. Komisch, dass er da Themen vorweg greift, und dann nicht mal die korrekte Implementation zeigt aber gut;) Die Logik der Parser habe ich soweit erst mal verstanden. Werde mir die Themen jetzt einfach weiter vornehmen. Parallel arbeite ich "Real World Haskell" durch. Hatte es vorher schon im Netz gefunden und bin wirklich glücklich damit. Vieles wird unglaublich viel besser erklärt als bei Graham Hutton. Kann man wirklich super mit arbeiten. Hinzu kommt, dass mir die Einteilung der einzelnen Kapitel eher zusagt. Die gewählten Beispiele machen dann natürlich auch einiges her, da sie weniger abstrakt sind. Sind halt nicht nur "sinnfreie" Aufgaben, sondern Sachen die man auch mal Anwenden kann.
Werde mir deinen geposteten Code noch mal genauer anschauen wenn ich zu Hause bin. So auf die Schnelle konnte ich noch nicht alles 100% nachvollziehen aber das krieg ich schon hin:)
Also nochmals vielen Dank.
„Es ist doch so. Zwei und zwei macht irgendwas, und vier und vier macht irgendwas. Leider nicht dasselbe, dann wär's leicht.
Das ist aber auch schon höhere Mathematik.“

Schorsch

Supermoderator

  • »Schorsch« ist der Autor dieses Themas

Beiträge: 5 145

Wohnort: Wickede

Beruf: Softwareentwickler

  • Private Nachricht senden

4

03.12.2011, 22:53

Habe jetzt heute im Zug ein bisschen weiter im Buch gelesen. Mittlerweile gefällt mir Graham Hutton immer weniger. In Real World Haskell wird einfach unglaublich viel besser auf Einzelheiten eingegangen. Was mir aufgefallen ist, ist dass die Implementierungen oft ineffizient sind. Oft werden Funktionen benutzt, die das Problem zwar lösen, aber viel mehr tun als sie müssten. Das ist mir jedoch auch bei Implementierungen verschiedener Algorithmen in Haskell im Netz aufgefallen. Als Beispiel wird ein Element aus einer Liste gesucht. Dabei wird oft die Liste komplett gefiltert. Dabei wird dann jedoch die ganze Liste durchlaufen. Persönlich würde ich mir eine Funktion schreiben, die abbricht, sobald das Element gefunden wurde. Jetzt hat ja jede Sprache ihren eigenen Stil. Haskell ist ja jetzt auch nicht unbedingt die Sprache, die versucht etwas besonders effizient zu lösen. Ist es einfach so, dass man mit Haskell versucht eine Lösung zu finden, obwohl es vielleicht effizienter geht? Klar sollte man versuchen, sein Problem möglichst gut zu lösen, aber ist es vielleicht in der Haskell-Gemeinschaft einfach so, dass man auf bereits implementierte Funktionen zurück greift und so sein Problem löst, oder ist es schon so, dass man bestimmte Funktionen neu schreibt, um es etwas schneller zu lösen?
Ich finde es schwierig das Problem grad besser zu beschreiben. Dabei geht es mir eher um die Philosophie von Haskell. Würde dazu gerne etwas mehr erfahren. Ich muss sagen, dass mir die Sprache und das Prinzip hinter Funktionaler Programmierung immer besser gefällt, da es schon einige Vorteile hat. Aber mir fehlt es halt immer noch stark an Erfahrung.
„Es ist doch so. Zwei und zwei macht irgendwas, und vier und vier macht irgendwas. Leider nicht dasselbe, dann wär's leicht.
Das ist aber auch schon höhere Mathematik.“

drakon

Supermoderator

Beiträge: 6 513

Wohnort: Schweiz

Beruf: Entrepreneur

  • Private Nachricht senden

5

03.12.2011, 23:23

Ich finde es schwierig das Problem grad besser zu beschreiben. Dabei geht es mir eher um die Philosophie von Haskell.

Das wirklich tolle an funktionalen Sprachen ist, dass die Korrektheit der Programme, die man schreibt beweisbar ist. Bei C, Java etc. geht das nicht oder bei weitem nicht so einfach.

Über Effizienz muss/kann/soll man sich da keine Gedanken machen. Das ist die Sache vom Compiler. Die grundlegende Definition durch Rekursion und fehlenden Nebeneffekten ist ja an sich extrem ineffizient und muss daher für ein sinnvoll schnell laufendes Programm sowieso umgewandelt werden.

Werbeanzeige