如何在Haskell中设计一个带有状态的"网络蜘蛛"?

osh*_*hko 19 oop io state haskell

经过多年的OOP,我正在学习Haskell.

我正在写一个功能和状态很少的愚蠢的网络蜘蛛.
我不知道如何在FP世界中做到这一点.

在OOP世界中,这个蜘蛛可以像这样设计(按用法):

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

String firstLink = b.getLinks()[0]

b.goto(firstLink)
print(b.getHtml())
Run Code Online (Sandbox Code Playgroud)

此代码加载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
}
Run Code Online (Sandbox Code Playgroud)

它可能同时拥有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())
Run Code Online (Sandbox Code Playgroud)

问题:展示如何从scracth(类似浏览器的API,可能有多个独立实例)在Haskell中设计这样的东西?请给出一个代码段.

注意:为简单起见,请跳过getLinks()函数的详细信息(其简单而无趣).
我们还假设有一个API函数

getUrlContents :: String -> IO String
Run Code Online (Sandbox Code Playgroud)

打开HTTP连接并返回给定URL的HTML.


更新:为什么要有州(或可能不是)?

API可以具有更多功能,而不仅仅是单个"加载和解析结果".
我没有添加它们以避免复杂性.

此外,它可以通过发送每个请求来关注HTTP Referer标头和cookie,以模拟真实的浏览器行为.

请考虑以下情形:

  1. 打开http://www.google.com/
  2. 在第一个输入区输入"haskell"
  3. 点击"Google搜索"按钮
  4. 点击链接"2"
  5. 点击链接"3"
  6. 打印当前页面的HTML(谷歌结果页面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())
Run Code Online (Sandbox Code Playgroud)

此方案的目标是在一组操作之后获取最后一页的HTML.另一个不太明显的目标是保持代码紧凑.

如果Browser有一个状态,它可以发送HTTP Referer头和cookie,同时隐藏自身内部的所有机制并提供漂亮的API.

如果浏览器没有状态,开发人员可能会传递所有当前的URL/HTML/Cookies - 这会给场景代码增加噪音.

注意:我猜有外面的库可以在Haskell中删除HTML,但我的目的不是要废弃HTML,而是要了解如何在Haskell中正确设计这些"黑盒子"的东西.

Ale*_*nov 12

在描述问题时,根本不需要状态:

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)
Run Code Online (Sandbox Code Playgroud)

它可能同时拥有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)
Run Code Online (Sandbox Code Playgroud)

第二个:

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

更新(回复您的更新):

如果Browser有一个状态,它可以发送HTTP Referer头和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
Run Code Online (Sandbox Code Playgroud)

如果浏览器没有状态,开发人员可能会传递所有当前的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
              ...
Run Code Online (Sandbox Code Playgroud)

或者你可以摆脱这些变量:

(>>~) = 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
Run Code Online (Sandbox Code Playgroud)

这看起来不错吗?请注意,浏览器仍然是不可变的.