Lwt_stream的使用和设计问题

时间:2015-05-15 22:56:13

标签: ocaml

我正在编写一个基于lwt的Twitter API库,我想使用Lwt_stream库实现Twitter API的cursor功能。

我决定使用Lwt_stream.from并为此函数提供参数f

这是我目前的工作

let get_follower ~access_token ~screen_name ?(count=5000) ?(wait=true) () =
  let base_uri = "https://api.twitter.com/1.1/followers/ids.json" in
  let cursor = ref (-1) in
  let f () =
    Client.do_get_request
      ~uri_parameters:
        ["screen_name", screen_name;
         "cursor", (string_of_int (!cursor));
         "count", (string_of_int count)]
      ~uri:(Uri.of_string base_uri)
      ~access_token:access_token
    () >>= fun res ->
    match res with
    | Ok (header, str) -> begin
      match (Yojson.Safe.from_string str |> user_ids_of_yojson) with
      | `Ok uids -> 
        cursor := uids.next_cursor;
        return (Some (Ok uids))
      | `Error msg -> failwith msg 
      end
    | Error e -> return (Some (Error (process_error_exn e))) in
  Lwt_stream.from f

我不确定是否应该使用ref我使用ref的原因是f的行为取决于它之前返回的值。具体而言,将使用cursor的值下次取决于当前next_cursor,如果cursor为零,f知道它到达终点并返回None

在这里使用ref被认为是一个很好的设计选择吗?有没有更好的方法来实现此功能?

2 个答案:

答案 0 :(得分:2)

Antron对我的观点给出了很好的答案,但我想分享一些实用的建议。如果我是你,我不会使用一个名为f的大函数和ref,好吧,主要是因为它闻起来,而且它是不可读的并且不能扩展。我将使用Lwt_stream.create创建一个流,并使用递归函数来处理它,它执行主循环,所有逻辑在辅助函数中分开。主循环函数可以具有重复的状态,因此我们不需要丑陋的引用或显式可变性。

因此,下面是一个示例,说明如何重构代码。我也没有找到零游标的显式检查,应该用来停止流(正如你在文中提到的那样),所以我添加了这个。否则代码应该像你的一样工作。

所以,我把它分成三个函数:make_request负责发出请求。 parse_response是一种纯粹的转变,它解构了答案。最后,loop函数执行主循环,发出请求,解析它们并在零上停止。

注意:看起来您正在使用Or_error monad并将其与异常混合使用。这是一个坏主意,因为它打破了前提条件,返回Or_error.t的格式良好的函数永远不会抛出异常。

let get_follower ~access_token ~screen_name ?(count=5000) ?(wait=true) () =
  let base_uri = "https://api.twitter.com/1.1/followers/ids.json" in
  let make_request cursor = 
    Client.do_get_request
      ~uri:(Uri.of_string base_uri) ~access_token    
      ~uri_parameters:[
        "screen_name", screen_name;
        "cursor", string_of_int cursor;
        "count", string_of_int count
      ] () in 
  let parse_response = function
    | Error e -> Error (process_error_exn e)
    | Ok (header, str) -> 
      match Yojson.Safe.from_string str |> user_ids_of_yojson with
      | `Ok uids -> Ok uids
      | `Error msg -> failwith msg in
  let stream,push = Lwt_stream.create () in
  let rec loop cursor = 
    make_request cursor >|= parse_response >>= function
    | Ok {next_cursor=0} -> push None; return_unit
    | Ok {next_cursor=n} as r -> push (Some r); loop n
    | error -> push (Some error); loop cursor in
  async (fun () -> loop (-1));
  stream

更新

但是,当然这个实现将急切地从服务器中提取数据,并将其推向下游。它就像一个泵和管道系统。一旦调用了功能,泵就会打开,它会不断地将水倒入系统。你可以使用有限推动来点击,可以使用Lwt_stream.create_bounded获得。但是你仍然会有一些预取(或者回到我们的管道系统类比,你会有一些扩展坦克)。通常,这并不错,因为它消除了一些延迟,但有时这不是你想要的。在这种情况下,剩下的唯一选择是使用显式引用单元来控制循环(并使用Lwt_stream.from构建流)。其他方法是改变界面,也许你试图将很多东西打包成一个抽象。也许返回follower list Lwt.t甚至follower Lwt.t list会更好。或者您甚至可以创建一个隐藏线程并返回follower的抽象类型follower list,并将其访问者提升到_ Lwt.t,例如

 module Follower : sig
   type t
   val name : t -> string Lwt.t
   ...
 end 

在这种情况下,此接口可以与Lwt_list模块完全兼容。

答案 1 :(得分:1)

由于f需要unit并且每次产生不同的结果,正如您所说,它必须依赖于某些状态,正如我认为您所知道的那样。它已经通过依赖I / O的结果来做到这一点,我认为这就是使ref非平凡的问题得到回答的问题。否则,答案是肯定的,这是必要的(见下面的第(2)点)。

我认为消除句法ref有两种主要的可能性,但它们都没有意义。

  1. 在I / O f内部以某种方式隐藏这些状态位,即在API上的对等端上。这看起来似乎不太可能,事实上它似乎必须在客户端上才能使API工作。
  2. 在客户端的其他位置隐藏这些状态位。但是在某种程度上仍然会在客户的某个地方以某种方式存在与ref相同的精神。您可以编写或使用某​​种类型的包装器来解决这个问题ref,但除非该包装器在许多地方都有用,否则只会使得在此处存在额外的客户端状态变得不那么明显。我宁愿将州保持为本地,尽可能接近其使用。由于f已经必然会做“脏”的事情,f是正确的地方。
  3. 因此,总而言之,我会说保持ref不变。无论如何,流通常是有状态的。

    上述讨论假定您必须使用Lwt_stream。如果没有,您可以提供一个替代接口,其类型看起来像get_follower : cursor:int -> ... -> (results * cursor) Lwt.t,如果必须,让调用者担心状态。这可能是我首先尝试的。

    修改

    当然有一个缺点,即编写调用代码以提供错误的光标的可能性。您可以通过返回部分应用的函数来隐藏光标,但可能会调用调用代码来调用错误的函数。如果你担心这些可能性,没有线性类型,流方法会更安全,因此它有其优点。