2010-02-21 26 views
19

Estoy aprendiendo Haskell después de años de OOP.¿Cómo diseñar una "araña web" con estado en Haskell?

Estoy escribiendo una araña web muda con pocas funciones y estado.
No estoy seguro de cómo hacerlo bien en el mundo de FP.

En el mundo POO esta araña se podría diseñar como esto (por el uso):

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

String firstLink = b.getLinks()[0] 

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

Este código carga http://www.google.com/, a continuación, haga “clic” el primer eslabón, se carga el contenido de la segunda página y luego imprime el contenido.

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 possbile tener 2 o “navegadores” a la vez, con su propio estado independiente:

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()) 

PREGUNTA: mostrar cómo diseñaría una cosa así en Haskell de scracth (Navegador -like API con posibilidad de tener varias instancias independientes)? Por favor, da un fragmento de código.

NOTA: Para simplificar, omita los detalles de la función getLinks() (es trivial y no es interesante).
También vamos a suponer que hay una función API

getUrlContents :: String -> IO String 

que abre la conexión HTTP y devuelve un código HTML para determinada URL.


ACTUALIZACIÓN: ¿por qué tener el estado (o puede ser que no)?

La API puede tener más funciones, no solo "resultados de carga y análisis".
No los agregué para evitar la complejidad.

También podría importarle el encabezado HTTP Referer y las cookies enviándolas con cada solicitud para emular el comportamiento real del navegador.

cuenta la situación siguiente:

  1. abierto http://www.google.com/
  2. Tipo "Haskell" en la primera área de entrada
  3. botón Haga clic en "Búsqueda de Google"
  4. Haga clic en enlace de "2"
  5. Haga clic en enlace "3"
  6. Imprimir el HTML de la página actual (página de resultados de google 3 para "haskell")

Tener un escenario como este en las manos, yo como desarrollador gustaría transferirlo a codificar lo más cerca posible:

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()) 

El objetivo de este escenario es conseguir HTML de la última página después de un conjunto de operaciones. Otro objetivo menos visible es mantener el código compacto.

Si el navegador tiene un estado, puede enviar el encabezado HTTP Referer y las cookies mientras oculta todas las mecánicas dentro de sí mismo y proporciona una buena API.

Si el navegador no tiene estado, es probable que el desarrollador pase todas las URL/HTML/Cookies actuales, y esto agrega ruido al código del escenario.

NOTA: Supongo que hay bibliotecas fuera para eliminar HTML en Haskell, pero mi intención no era eliminar HTML, pero aprenda cómo estas cosas en "caja negra" se pueden diseñar correctamente en Haskell.

Respuesta

12

Al describir el problema, no hay necesidad de que el estado del todo:

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 possbile tener 2 o “navegadores” a la vez, con su propio estado independiente:

Obviamente, puede tener todos los que desee y no pueden interferir entre sí.

Ahora el equivalente a sus fragmentos. Primero:

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) 

Y en segundo lugar:

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

ACTUALIZACIÓN (respuesta a la actualización):

si el navegador tiene un estado, puede enviar la cabecera HTTP Referer y galletas mientras se esconde todo mecánica dentro de sí misma y dando una buena API.

Sin necesidad de estado todavía, goto puede simplemente tomar un argumento del navegador. En primer lugar, tendremos que ampliar el tipo:

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 

si el navegador no tiene ningún estado, el desarrollador es probable que pase alrededor de todas las URL/html/Cookies actuales - y esto añade ruido al código escenario.

No, acaba de pasar los valores del tipo Navegador. Por su ejemplo,

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

O usted puede deshacerse de esas variables:

(>>~) = 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 

se ve este lo suficientemente bueno? Tenga en cuenta que el navegador todavía es inmutable.

+0

Brillante. .... – oshyshko

+1

Tenga en cuenta que la mónada BrowserAction ya existe: http://hackage.haskell.org/packages/archive/HTTP/4000.0.8/doc/html/Network-Browser.html – jrockway

+1

También tenga en cuenta que 'flip mapM' se llama 'forM'. – BMeph

3

No intente replicar en muchas orientaciones de objetos.

apenas define un tipo simple Browser que contiene la dirección URL actual (por IORef por el bien de la mutabilidad) y algunos IO funciones para proporcionar la funcionalidad de acceso y modificación.

Un programm muestra se vería así:

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 

Tenga en cuenta que si se define una función de ayuda al igual que o # f = f o, tendrá una sintaxis más objetual (por ejemplo b1#getLinks).

completas definiciones de tipos:

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

¿Por qué intentas crear "objetos" de navegador e imitar el diseño/interfaz/sintaxis orientados a objetos? ¿No sería un simple adicional 'getLinks :: String -> String -> [String]' todo lo que se necesita? – sth

+1

en mi humilde opinión, incluso usted está tratando de replicar OOP demasiado.Para esta tarea, el único beneficio remotamente posible para la mutabilidad es el almacenamiento en caché de HTML y la lista de enlaces, que su respuesta no hace. E incluso allí no es necesario. –

3

La función getUrlContents ya hace lo goto()getHtml() y lo haría, lo único que falta es una función que extrae los enlaces de la página descargada. Podría tomar una cadena (el código HTML de una página) y una URL (para resolver los enlaces relativos) y extraer todos los enlaces de esa página:

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

A partir de estas dos funciones se puede construir fácilmente otras funciones que hacen el rastreo . Por ejemplo, el "conseguir la primera página del enlace" ejemplo podría tener este aspecto:

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

una función simple para descargar todo lo vinculado desde una URL podría ser:

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

(Tenga en cuenta que esto, por ejemplo, se siga ciclos en los enlaces sin fin - una función para uso real debería ocuparse de tales casos)

Solo el "estado" que utilizan estas funciones es la URL y se asigna a las funciones relevantes como parámetro.

Si no habría más información que todas las funciones de navegación necesitan podría crear un nuevo tipo de grupo que todos juntos:

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

funciones que utilizan esta información podría entonces simplemente tomar un parámetro de este tipo y usa la información contenida. No hay problema en tener muchas instancias de estos objetos y usarlos simultáneamente, cada función simplemente usará el objeto que se le da como parámetro.

2

muestra cómo se diseña tal cosa en Haskell desde scracth (API similar a un navegador con posibilidad de tener varias instancias independientes)? Por favor, da un fragmento de código.

me gustaría utilizar un hilo (Haskell) en cada punto, tienen todos los subprocesos que se ejecutan en la mónada Estado con un tipo de registro de todos los recursos que necesitan, y han comunicado los resultados de vuelta al hilo principal través de un canal.

¡Agregue más concurrencia! Esa es la manera FP.

Si no recuerdo mal, hay un diseño aquí por bandas de acoplamiento de comprobación hilos comunican a través de canales:

Además, asegúrese de no utilizar cuerdas, pero el texto o ByteString - - Serán mucho más rápidos.

Cuestiones relacionadas