6

如果可能的话,我正在寻找一种更惯用的方式来编写以下 clojure 代码:

(import '(System.Net HttpWebRequest NetworkCredential)
        '(System.IO StreamReader)) 

(defn downloadWebPage
  "Downloads the webpage at the given url and returns its contents."
  [^String url ^String user ^String password]
  (def req (HttpWebRequest/Create url))
  (.set_Credentials req (NetworkCredential. user password ""))
  (.set_UserAgent req ".NET")
  (def res (.GetResponse req))
  (def responsestr (.GetResponseStream res))
  (def rdr (StreamReader. responsestr))
  (def content (.ReadToEnd rdr))
  (.Close rdr)
  (.Close responsestr)
  (.Close res)
  content
  )

这是在 ClojureCLR 上并且有效。(它是 CLR 变体的事实并不重要)

我想摆脱defs(用let替换?它们可以互相引用吗?)

如何更好地进入流 - 请记住..链接将不起作用,因为我需要稍后关闭流。

编辑:回答后,我在 .NET 中找到了一种更简单的方法来使用 WebClient 类下载网页。我仍然使用了许多 Michal 推荐的方法——只是想记录下我现在认为是最好的答案:

(defn download-web-page
    "Downloads the webpage at the given url and returns its contents."
    [^String url ^String user ^String password]
    (with-open [client  (doto (WebClient.)
                        (.set_Credentials (NetworkCredential. user password "")))]
      (.DownloadString client url)))
4

1 回答 1

6

问题中的代码可以像这样非常惯用地重写(以任何拼写错误为模——希望没有错别字):

(defn download-web-page
  "Downloads the webpage at the given url and returns its contents."
  [^String url ^String user ^String password]
  (let [req (doto (HttpWebRequest/Create url)
              (.set_Credentials (NetworkCredential. user password ""))
              (.set_UserAgent ".NET"))
        response        (.GetResponse req)
        response-stream (.GetResponseStream res)
        rdr             (StreamReader. response-stream)
        content (.ReadToEnd rdr)]
    (.Close rdr)
    (.Close response-stream)
    (.Close response)
    content))

Assuming the .NET version of with-open calls .Close at the bound objects (as I expect it might, but won't be able to check -- no .NET REPL at hand) and that .readToEnd eagerly consumes the whole stream, this could be further simplified to

Update: Just checked that ClojureCLR's with-open calls .Dispose on the bound objects. If that is ok in place of .Close, great; if .Close is required, you can write your own version of with-open to use .Close instead (possibly copying most of the original):

(defn download-web-page
  "Downloads the webpage at the given url and returns its contents."
  [^String url ^String user ^String password]
  (let [req (doto (HttpWebRequest/Create url)
              (.set_Credentials (NetworkCredential. user password ""))
              (.set_UserAgent ".NET"))]
    (with-open [response        (.GetResponse req)
                response-stream (.GetResponseStream res)
                rdr             (StreamReader. response-stream)]
      (.ReadToEnd rdr))))

Some comments:

  1. Don't use def, defn etc. anywhere except at top level unless you really know you need to do that. (Actually using them immediately inside a top-level let is occasionally useful if you need the object being created to close over the let-bound locals... Anything more funky than that should receive very careful scrutiny!)

    def & Co. create top level Vars or reset their root bindings; doing so in the course of a programme's regular operation is completely contrary to the functional spirit of Clojure. Perhaps more importantly from a practical POV, any function which relies on "owning" a bunch of Vars can only be executed by one thread at a time; there's no reason why download-web-page should be thus limited.

  2. let-introduced bindings may not be mutually recursive; later bindings may refer to earlier bindings, but not the other way around. Mutually recursive local functions may be introduced with letfn; other types of mutually recursive objects may be somewhat less convenient to create outside of top level (though by no means impossible). The code from the question doesn't rely on mutually recursive values, so let works fine.

于 2010-09-01T23:00:32.017 回答