Как спроектировать «веб-паука» с состоянием в Haskell? - PullRequest
19 голосов
/ 21 февраля 2010

Я изучаю 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
}

Возможно иметь 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 может иметь больше функций, а не просто «результаты загрузки и анализа».
Я не добавил их, чтобы избежать сложности.

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

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

  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-файла и файлы cookie Referer, скрывая при этом все механизмы и предоставляя хороший API.

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

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

Ответы [ 4 ]

12 голосов
/ 21 февраля 2010

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

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)

Возможно иметь 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)

ОБНОВЛЕНИЕ (ответ на ваше обновление):

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

Нет необходимости указывать состояние, goto может принимать аргумент Browser. Сначала нам нужно расширить тип:

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 / файлы cookie - и это добавляет шум в код сценария.

Нет, вы просто передаете значения типа 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

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

3 голосов
/ 21 февраля 2010

Функция 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
     }

Функции, которые используют эту информацию, могли бы тогда просто взять параметр этого типа и использовать содержащуюся информацию. Нет проблем в том, чтобы иметь много экземпляров этих объектов и использовать их одновременно, каждая функция будет просто использовать объект, который указан в качестве параметра.

3 голосов
/ 21 февраля 2010

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

Просто определите простой тип 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]
2 голосов
/ 21 февраля 2010

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...