2010-02-21 3 views
19

Ich lerne Haskell nach Jahren der OOP.Wie man eine "Netzspinne" mit Staat in Haskell entwirft?

Ich schreibe eine dumme Webspinne mit wenigen Funktionen und Zustand.
Ich bin mir nicht sicher, wie es in der FP-Welt richtig geht.

In OOP Welt könnte diese Spinne wie diese gestaltet werden (durch Nutzung):

Browser b = new Browser() 
b.goto(“http://www.google.com/”) 

String firstLink = b.getLinks()[0] 

b.goto(firstLink) 
print(b.getHtml()) 

Dieser Code lädt http://www.google.com/, dann „Klicks“ der erste Link, lädt Inhalt der zweiten Seite und druckt dann den Inhalt.

class Browser { 
    goto(url: String) : void // loads HTML from given URL, blocking 
    getUrl() : String // returns current URL 
    getHtml() : String // returns current HTML 
    getLinks(): [String] // parses current HTML and returns a list of available links (URLs) 

    private _currentUrl:String 
    private _currentHtml:String 
} 

Es ist possbile 2 zu haben oder „Browser“ auf einmal, mit einem eigenen separaten Staat:

Browser b1 = new Browser() 
Browser b2 = new Browser() 

b1.goto(“http://www.google.com/”) 
b2.goto(“http://www.stackoverflow.com/”) 

print(b1.getHtml()) 
print(b2.getHtml()) 

FRAGE: zeigen, wie würden Sie eine Sache von scracth in Haskell so entwerfen (Browser -ähnliche API mit der Möglichkeit, mehrere unabhängige Instanzen zu haben)? Bitte geben Sie ein Code-Snippet.

HINWEIS: Aus Gründen der Einfachheit überspringen Sie die Details der Funktion getLinks() (es ist trivial und nicht interessant).
Auch nehmen wir an, es gibt eine API-Funktion

getUrlContents :: String -> IO String 

, die HTTP-Verbindung öffnet sich und gibt einen HTML-Code für bestimmte URL.


UPDATE: Warum Zustand haben (oder auch nicht)?

Die API kann mehr Funktionen haben, nicht nur einzelne "Lade- und Parsergebnisse".
Ich habe sie nicht hinzugefügt, um Komplexität zu vermeiden.

Auch könnte es sich um HTTP Referer Header und Cookies kümmern, indem Sie sie mit jeder Anfrage senden, um echtes Browserverhalten zu emulieren.

sich das folgende Szenario:

  1. öffnen http://www.google.com/
  2. Type "Haskell" in die erste Eingabebereich
  3. Klicken Sie auf Schaltfläche "Google-Suche"
  4. Click link "2"
  5. Click Link "3"
  6. HTML der aktuellen Seite drucken (Google-Ergebnisseite 3 für "Haskell")

ein Szenario wie dieses auf den Händen zu haben, die ich als Entwickler möchte es übertragen so nah wie möglich zu kodieren:

Browser b = new Browser() 
b.goto("http://www.google.com/") 
b.typeIntoInput(0, "haskell") 
b.clickButton("Google Search") // b.goto(b.finButton("Google Search")) 
b.clickLink("2") // b.goto(b.findLink("2")) 
b.clickLink("3") 
print(b.getHtml()) 

Das Ziel dieses Szenario HTML-Code der letzten Seite zu bekommen, ist nach eine Reihe von Operationen. Ein weiteres weniger sichtbares Ziel ist es, Code kompakt zu halten.

Wenn Browser einen Status hat, kann er HTTP Referer Header und Cookies senden, während er alle Mechaniken in sich versteckt und eine nette API gibt.

Wenn Browser keinen Status hat, wird der Entwickler wahrscheinlich alle aktuellen URL/HTML/Cookies weitergeben - und das fügt dem Szenario-Code Rauschen hinzu.

HINWEIS: Ich denke, es gibt Bibliotheken außerhalb für die Verschrottung von HTML in Haskell, aber meine Absicht war nicht, HTML zu verwerfen, sondern zu lernen, wie diese "black-boxed" Dinge in Haskell richtig gestaltet werden können.

Antwort

12

Wie Sie das Problem beschreiben, gibt es überhaupt nicht nötig Zustand:

data Browser = Browser { getUrl :: String, getHtml :: String, getLinks :: [String]} 

getLinksFromHtml :: String -> [String] -- use Text.HTML.TagSoup, it should be lazy 

goto :: String -> IO Browser 
goto url = do 
      -- assume getUrlContents is lazy, like hGetContents 
      html <- getUrlContents url 
      let links = getLinksFromHtml html 
      return (Browser url html links) 

Es ist possbile auf einmal 2 oder „Browser“ zu haben, mit einem eigenen separaten Staat:

Sie können natürlich so viele haben, wie Sie wollen, und sie können sich nicht gegenseitig stören.

Jetzt das Äquivalent Ihrer Schnipsel. Erstens:

htmlFromGooglesFirstLink = do 
           b <- goto "http://www.google.com" 
           let firstLink = head (links b) 
           b2 <- goto firstLink -- note that a new browser is returned 
           putStr (getHtml b2) 

Und zweitens:

twoBrowsers = do 
       b1 <- goto "http://www.google.com" 
       b2 <- goto "http://www.stackoverflow.com/" 
       putStr (getHtml b1) 
       putStr (getHtml b2) 

UPDATE (Antwort auf Ihr Update):

Wenn Browser einen Zustand hat, kann es Referrer-Header und Cookies senden, während alle versteckt Mechanik in sich selbst und geben nette API.

Keine Notwendigkeit für Zustand noch, goto kann nur ein Browser-Argument nehmen. Zuerst müssen wir die Art erweitern:

data Browser = Browser { getUrl :: String, getHtml :: String, getLinks :: [String], 
         getCookies :: Map String String } -- keys are URLs, values are cookie strings 

getUrlContents :: String -> String -> String -> IO String 
getUrlContents url referrer cookies = ... 

goto :: String -> Browser -> IO Browser 
goto url browser = let 
        referrer = getUrl browser 
        cookies = getCookies browser ! url 
        in 
        do 
        html <- getUrlContents url referrer cookies 
        let links = getLinksFromHtml html 
        return (Browser url html links) 

newBrowser :: Browser 
newBrowser = Browser "" "" [] empty 

Wenn Browser keinen Staat hat, ist der Entwickler wahrscheinlich um alle aktuelle URL/HTML/Cookies zu übergeben - und das fügt Rauschen Szenario Code.

Nein, Sie übergeben nur Werte vom Typ Browser herum. Für Ihr Beispiel

useGoogle :: IO() 
useGoogle = do 
       b <- goto "http://www.google.com/" newBrowser 
       let b2 = typeIntoInput 0 "haskell" b 
       b3 <- clickButton "Google Search" b2 
       ... 

Oder Sie können diese Variablen loszuwerden:

(>>~) = flip mapM -- use for binding pure functions 

useGoogle = goto "http://www.google.com/" newBrowser >>~ 
      typeIntoInput 0 "haskell" >>= 
      clickButton "Google Search" >>= 
      clickLink "2" >>= 
      clickLink "3" >>~ 
      getHtml >>= 
      putStr 

Ist das genug gut aussehen? Beachten Sie, dass der Browser immer noch unveränderbar ist.

+0

Brilliant. .... – oshyshko

+1

Beachten Sie, dass die BrowserAction-Monade bereits existiert: http://hackage.haskell.org/packages/archive/HTTP/4000.0.8/doc/html/Network-Browser.html – jrockway

+1

Beachten Sie auch, dass 'flip mapM' heißt 'forM'. – BMeph

3

Versuchen Sie nicht, zu vielen Objektorientierungen zu replizieren.

definieren, nur einen einfachen Browser Typen, der die aktuelle URL (pro IORef aus Gründen der Veränderlichkeit) und einig IO Funktionen bereitzustellen Zugriffs- und Änderungsfunktionalität enthält.

Eine Probe Programm würde wie folgt aussehen:

import Control.Monad 

do 
    b1 <- makeBrowser "google.com" 
    b2 <- makeBrowser "stackoverflow.com" 

    links <- getLinks b1 

    b1 `navigateTo` (head links) 

    print =<< getHtml b1 
    print =<< getHtml b2 

Beachten Sie, dass, wenn Sie eine Hilfsfunktion wie o # f = f o definieren, werden Sie eine Objekt-ähnliche Syntax haben (z b1#getLinks).

komplette Typdefinitionen:

data Browser = Browser { currentUrl :: IORef String } 

makeBrowser :: String -> IO Browser 

navigateTo :: Browser -> String -> IO() 
getUrl  :: Browser -> IO String 
getHtml  :: Browser -> IO String 
getLinks  :: Browser -> IO [String] 
+3

Warum versuchen Sie, Browser "Objekte" zu machen und das objektorientierte Design/Interface/Syntax nachzuahmen? Wäre nicht ein einfaches zusätzliches 'getLinks :: String -> String -> [String]' alles was benötigt wird? – sth

+1

IMHO, auch Sie versuchen, OOP zu viel zu replizieren.Für diese Aufgabe ist der einzige aus der Ferne mögliche Vorteil für die Wandlungsfähigkeit das Zwischenspeichern von HTML- und Link-Listen, was Ihre Antwort nicht tut. Und selbst dort wird es nicht benötigt. –

3

Die getUrlContents Funktion bereits tut, was goto() und getHtml() tun würden, das einzige, was fehlt, ist eine Funktion, die Verbindungen aus der heruntergeladenen Seite extrahiert. Es könnte eine Zeichenfolge (der HTML-Code einer Seite) nehmen und eine URL (in relative Links zu lösen) und extrahiert alle Links von dieser Seite:

getLinks :: String -> String -> [String] 

Aus diesen beiden Funktionen können Sie ganz einfach andere Funktionen erstellen, die die Spidern tun . Zum Beispiel könnten die „erhalten die ersten gelinkten Seite“ Beispiel wie folgt aussehen:

getFirstLinked :: String -> IO String 
getFirstLinked url = 
    do page <- getUrlContents url 
     getUrlContents (head (getLinks page url)) 

Eine einfache Funktion alles aus einer URL verknüpft herunterladen könnte:

allPages :: String -> IO [String] 
allPages url = 
    do page <- getUrlContent url 
     otherpages <- mapM getUrlContent (getLinks page url) 
     return (page : otherpages) 

(Beachten Sie, dass dies zum Beispiel wird folgen Zyklen in den Links endlos - eine Funktion für den realen Einsatz sollte sich um solche Fälle kümmern)

Dort nur "Zustand", der von diesen Funktionen verwendet wird, ist die URL und es wird nur die relevanten Funktionen als Parameter gegeben.

Wenn es wäre mehr Informationen, dass alle Browser-Funktionen benötigen Sie eine neue Art zu gruppieren sie alle zusammen schaffen könnten:

data BrowseInfo = BrowseInfo 
    { getUrl  :: String 
    , getProxy :: ProxyInfo 
    , getMaxSize :: Int 
    } 

Funktionen, die diese Informationen benutzen, könnten dann einfach einen Parameter dieser Art nehmen und Benutze die enthaltenen Informationen. Es gibt kein Problem, viele Instanzen dieser Objekte zu haben und sie gleichzeitig zu verwenden, jede Funktion wird nur das Objekt verwenden, das als ein Parameter angegeben ist.

2

zeigen, wie würden Sie so etwas in Haskell von scrakth (Browser-ähnliche API mit der Möglichkeit, mehrere unabhängige Instanzen haben) zu entwerfen? Bitte geben Sie ein Code-Snippet.

Ich würde an jedem Punkt ein (Haskell) Gewinde verwendet, haben alle Fäden im Staat monadisch mit einem Satzart laufen, was auch immer Ressourcen, die sie brauchen, und haben die Ergebnisse mitgeteilt zurück zu dem Haupt-Thread über einen Kanal.

Mehr Concurrency hinzufügen! Das ist der FP-Weg.

Wenn ich mich richtig erinnere, gibt es einen Entwurf, der hier für Banden von Verbindungsfäden Überprüfung Kanäle der Kommunikation über:

Auch stellen Sie sicher, nicht Strings zu verwenden, aber Text oder ByteStrings - - Sie werden viel schneller sein.

Verwandte Themen