【问题标题】:Recursive list function in HaskellHaskell中的递归列表函数
【发布时间】:2020-01-26 19:00:00
【问题描述】:

我目前正在 Haskell 开发这个程序,我在其中分析网站并尝试找到属于该网站的所有链接 (href)。我已经能够提取主站点的所有链接,但我正在为递归而苦苦挣扎,因为我想遵循我已经找到的链接并再次执行相同的过程。

这是我已经拥有的:

parseHtml = fmap LB.unpack . simpleHttp
filterFunc x y = -- damn long line with a lot of filters

main :: IO()
main = do
    let site = "https://stackoverflow.com/"
    url <- parseHtml site
    let links = filterFunc site url
    mapM_ print $ take 5 $ links

这是我目前的输出:

"https://stackoverflow.com/company/about"
"https://stackoverflow.com/company/work-here"
"https://stackoverflow.com/help"
"https://stackoverflow.com/jobs/directory/developer-jobs"
"https://stackoverflow.com/questions/44691577/stream-versus-iterators-in-set"

我只需要关于如何进一步进行以及如何再次访问已经找到的链接的提示。我应该使用折叠吗?

【问题讨论】:

    标签: haskell recursion


    【解决方案1】:

    链接查找本质上是一个图遍历问题,由于功能的纯洁性,这在 Haskell 中可能会很棘手:很难通过使用外部历史表来明确地将节点(链接)标记为已访问或未访问。

    您的典型遍历算法可能如下所示:

    function traverse(current_node) {
        if (current_node.is_visited) {
            return some_data;
        } else {
            current_node.is_visisted = true;          // Hard in Haskell!
            accumulated_data = ...;
            for (child in current_node.children()) {
                accumulated_data += traverse(child);  // Recursion happens here.
            }
            return accumulated_data;
        }
    }
    

    因为没有简单直接的方法来标记节点是否已访问,我们可以尝试其他解决方案。例如,我们可能会考虑这样的事情:

    traverse :: ([URL], Data) -> URL -> ([URL], Data)
    traverse (history, datum) current = let ... in ([new_history], accumulated_data)
    

    这里的想法如下:我们保留一个我们访问过的URLs 的明确列表。如果当前节点 (URL) 出现在我们的历史列表中,这允许我们快速从当前节点返回(也许是 Set 用于优化?:))。在这种情况下,使用traverse 对子节点的每个后续调用都将获得new_history 列表,从而有效地跟踪已访问和未访问的URLs 列表。

    一种可能的实现方式是使用折叠函数,例如foldl

    foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b
    

    这里类型t a可能是[URL],表示当前链接的子节点,我们的traverse函数很方便地具有类型签名(b -&gt; a -&gt; b),其中type b = ([URL], Data)type a = URL

    你能从这里找出如何组合traversefoldl吗?

    【讨论】:

    • 您可能希望使用Set URL 而不是[URL]。既是为了提高效率,也是因为这就是您保留该列表时的意思。
    • 我在我的 filterfunc Set.toList . Set.fromList 中有这个代码 sn-p 以消除我得到的所有重复项。我是否应该将其保留为一组以便更轻松地使用它?
    • 将其保留为Set 可能是最简单的选择,正如@gallais 所指出的那样。需要注意的另一件事是具有不同名称的链接可能指向同一个页面,因此为它们获取规范表示可能是明智之举。
    • 如果出于某种原因您需要以List 格式保留唯一表示,一种选择是使用List.nubhackage.haskell.org/package/base-4.9.1.0/docs/…
    【解决方案2】:

    只需将您的链接访问逻辑移动到一个单独的函数中,该函数将链接作为参数,然后按照您的直觉递归链接。

    根据您最终想要对链接执行的操作,您可以例如简单地使用您的函数折叠链接。

    例如,稍微修改你的代码:

    parseHtml = fmap LB.unpack . simpleHttp
    filterFunc x y = -- damn long line with a lot of filters
    
    visitLink :: String -> IO ()
    visitLink site = do
        url <- parseHtml site
        let links = filterFunc site url
        mapM_ print $ take 5 $ links -- or whatever you want to do on your links
        mapM_ visitLink links -- the recursive call
    
    
    main :: IO()
    main = visitLinks "https://stackoverflow.com/"
    

    如果您不想随时打印链接,而是希望返回它们,请调整visitLink 函数的返回类型(例如String -&gt; IO [String] 并适当更改visitLink 中的最后一行(例如实例fmap join $ mapM visitLinks links)。

    正如另一个答案中提到的,请记住,使用如此简单的代码,您可能会无限次访问同一个链接。考虑将您访问的链接存储在您将传递给visitLink 的合适数据结构(例如集合)中。

    【讨论】:

    • 我只是打印它们以确保一切正常并查看结果是什么,所以我知道如何使用它。最后一步是将我得到的所有链接保存到一个文件中。
    • 我忘了说我在我的 filterfunc (Set.toList . Set.fromList) 中使用了一个 Set 来摆脱所有的重复项
    • @SarahK。当然,我的意思是我提出的解决方案适用于适合IO () 的所有内容(包括写入文件),但您可能必须对其进行调整以适应其他用途。关于我的 Set 评论,请记住,后面的页面可以引用以前的页面(例如链接到网站主页的页面),您需要考虑到这一点。
    猜你喜欢
    • 1970-01-01
    • 2017-06-22
    • 1970-01-01
    • 2011-08-21
    • 2011-07-16
    • 1970-01-01
    • 2016-08-03
    • 2016-05-06
    • 2023-04-09
    相关资源
    最近更新 更多