Как создать "веб-паук" с состоянием в 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
}
Возможно наличие двух или "браузеров" одновременно со своим отдельным состоянием:
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 может иметь больше функций, а не только отдельные "результаты загрузки и анализа".
Я не добавлял их, чтобы избежать сложности.
Также он может заботиться о заголовке HTTP-заголовка и файлах cookie, отправив их с каждым запросом, чтобы эмулировать реальное поведение браузера.
Рассмотрим следующий сценарий:
- Откройте http://www.google.com/
- Введите "haskell" в первую область ввода
- Нажмите кнопку "Поиск Google"
- Нажмите ссылку "2"
- Нажмите ссылку "3"
- Печать 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-заголовка и файлы cookie, скрывая всю механику внутри себя и предоставляя хороший API.
Если в браузере нет состояния, разработчик, скорее всего, передаст все текущие URL/HTML/Cookies - и это добавит шум в код сценария.
ПРИМЕЧАНИЕ. Я предполагаю, что существуют библиотеки для утилизации HTML в Haskell, но я не собирался отказываться от HTML, но узнал, как эти "черные ящики" могут быть правильно разработаны в Haskell.
Ответы
Ответ 1
Как вы описали проблему, вообще нет необходимости в состоянии:
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)
Возможно наличие двух или "браузеров" одновременно со своим отдельным состоянием:
Вы, очевидно, можете иметь столько, сколько хотите, и они не могут мешать друг другу.
Теперь это эквивалент ваших фрагментов. Во-первыхи:
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-заголовка и файлы cookie, скрывая всю механику внутри себя и предоставляя хороший 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
Хорошо ли это выглядит? Обратите внимание, что браузер по-прежнему неизменен.
Ответ 2
Не пытайтесь реплицировать многие объекты.
Просто определите простой тип Browser
, который содержит текущий URL (за 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]
Ответ 3
Функция getUrlContents
уже выполняет функции goto()
и getHtml()
, единственное, что отсутствует, это функция, которая извлекает ссылки с загруженной страницы. Он может взять строку (HTML страницы) и URL (для разрешения относительных ссылок) и извлечь все ссылки с этой страницы:
getLinks :: String -> String -> [String]
Из этих двух функций вы можете легко создавать другие функции, которые делают пауки. Например, пример "получить первую связанную страницу" может выглядеть так:
getFirstLinked :: String -> IO String
getFirstLinked url =
do page <- getUrlContents url
getUrlContents (head (getLinks page url))
Простая функция для загрузки всего, связанного с URL-адресом, может быть:
allPages :: String -> IO [String]
allPages url =
do page <- getUrlContent url
otherpages <- mapM getUrlContent (getLinks page url)
return (page : otherpages)
(Обратите внимание, что это, например, будет бесконечно следовать циклам в ссылках - функция для реального использования должна заботиться о таких случаях)
Там только "состояние", используемое этими функциями, является URL-адресом, и оно просто присваивается соответствующим функциям в качестве параметра.
Если будет больше информации о необходимости всех функций просмотра, вы можете создать новый тип, чтобы сгруппировать все вместе:
data BrowseInfo = BrowseInfo
{ getUrl :: String
, getProxy :: ProxyInfo
, getMaxSize :: Int
}
Функции, которые используют эту информацию, могут просто взять параметр этого типа и использовать содержащуюся информацию. Нет проблем в том, что у многих экземпляров этих объектов и их одновременное использование, каждая функция будет просто использовать объект, который он задает в качестве параметра.
Ответ 4
показать, как вы могли бы создать такую вещь в Haskell из scracth (браузерный API с возможностью иметь несколько независимых экземпляров)? Пожалуйста, дайте фрагмент кода.
Я бы использовал один (Haskell) поток в каждой точке, все потоки, запущенные в государственной монаде, с типом записи любых ресурсов, в которых они нуждаются, и результаты передаются обратно в основной поток по каналу.
Добавить еще concurrency! Это путь FP.
Если я правильно помню, здесь есть проект для банд потоков ссылок, связывающих каналы:
Кроме того, не используйте Strings, а Text или ByteStrings - они будут намного быстрее.