Как создать" веб-паука " с состоянием в Haskell?

Я изучаю Haskell после многих лет ООП.

Я пишу тупой веб-паук с несколькими функциями и состоянием.
Я не уверен, как это сделать правильно в мире FP.

в мире ООП этот паук может быть спроектирован следующим образом (по использованию):

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

String firstLink = b.getLinks()[0]

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

этот код загружается http://www.google.com/, затем "щелкает" первую ссылку, загружает содержимое второй страницы, а затем печатает содержимое.

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
}

это possbile иметь 2 или "браузеры" сразу, со своим отдельным состоянием:

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

вопрос: покажите, как бы вы спроектировали такую вещь в Haskell из scracth (браузерный API с возможностью иметь несколько независимых экземпляров)? Пожалуйста, дайте фрагмент кода.

Примечание: для простоты пропустите детали функции getLinks () (ее тривиальная и не интересная).
также предположим, что есть функция API

getUrlContents :: String -> IO String

это открывает HTTP подключение и возвращает HTML для данного URL-адреса.


обновление: Почему иметь состояние (или не может быть)?

API может иметь больше функций, а не только один "load-and-parse results".
Я добавил их не для того, чтобы избежать сложностей.

также он может заботиться о заголовке HTTP Referer и cookies, отправляя их с каждым запросом, чтобы эмулировать реальное поведение браузера.

рассмотрим следующий сценарий:

  1. открыть http://www.google.com/
  2. введите "haskell" в первую область ввода
  3. Нажмите кнопку "Поиск Google"
  4. нажмите ссылку "2"
  5. нажмите ссылку "3"
  6. печать HTML текущей страницы (страница результатов google 3 для "haskell")

имея такой сценарий на руках, я как разработчик хотел бы передать его в код как можно ближе возможно:

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

цель этого сценария-получить HTML последней страницы после набора операций. Другой менее заметной целью является сохранение компактности кода.

если браузер имеет состояние, он может отправлять HTTP Referer заголовок и куки, скрывая все механики внутри себя и давая хороший API.

если браузер не имеет состояния, разработчик, скорее всего, передаст все текущие URL/HTML/Cookies-и это добавляет шум в код сценария.

ПРИМЕЧАНИЕ: I думаю, есть библиотеки снаружи для слом HTML в Haskell, но мое намерение не было ломать HTML, но узнать, как эти "черные ящики" могут быть разработаны должным образом в Haskell.

4 ответов


как вы описываете проблему, нет необходимости в состоянии вообще:

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)

это possbile иметь 2 или "браузеры" сразу, со своим собственным отдельным состоянием:

вы, очевидно, можете иметь столько, сколько хотите, и они не могут мешать друг другу.

теперь эквивалент ваших фрагментов. Первый:

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)

и второе:

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

UPDATE (ответ на обновление):

Если Браузер имеет состояние, он может отправлять HTTP Referer заголовок и куки, скрывая все механики внутри себя и давая хороший API.

нет необходимости в состоянии еще,goto можно просто взять аргумент браузера. Во-первых, нам нужно расширить тип:

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

если браузер не имеет состояния, разработчик, скорее всего, передаст все текущие URL/HTML/Cookies-и это добавляет шум в код сценария.

нет, вы просто передаете значения типа Browser вокруг. Например,

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

или вы можете избавиться от этих переменных:

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

это выглядит достаточно хорошо? Обратите внимание, что браузер по-прежнему неизменен.


Не пытайтесь реплицироваться на многие объектные ориентации.

просто определите простой Browser введите текущий URL-адрес (per IORef ради изменчивости) и некоторые IO функции для того чтобы обеспечить функциональность доступа и изменения.

пример программы будет выглядеть следующим образом:

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

обратите внимание, что если вы определяете вспомогательную функцию, такую как o # f = f o, у вас будет более объектный синтаксис (например,b1#getLinks).

полный тип определения:

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]

на


показать, как бы вы разработали такую вещь в Haskell из scracth (браузерный API с возможностью иметь несколько независимых экземпляров)? Пожалуйста, дайте фрагмент кода.

Я бы использовал один поток (Haskell) в каждой точке, имел бы все потоки, работающие в монаде состояния с типом записи любых ресурсов, которые им нужны, и имел бы результаты, передаваемые обратно в основной поток по каналу.

добавить больше параллелизма! Это FP путь.

Если я правильно помню, здесь есть дизайн для групп потоков проверки ссылок, сообщающихся по каналам:

кроме того, убедитесь, что не использовать строки, но текст или ByteStrings-они будут намного быстрее.