ocaml中的评估监视器

evaluation monitor in ocaml

我要实现的目标类似于日志记录工具,但用于监视和传输来自 运行ning 模拟的任意数据。这是简化的情况:

module Sim (V:VEC) = struct
  module V = V
  module M = struct type data = V.t end
  let loop n init_data = 
    let running_data = ref init_data in
    for _i = 1 to n do 
      (*?*) (* monitor here: data => outside world *)
      rdata := process_data !rdata
    done
end

虽然模拟循环,但在 ? 我可能想要 'tap' 数据并累积它。其他时候,我只想让它 运行 并以最小的开销禁用数据流——? 处于紧密循环中。所以我希望流式传输能够以很少的成本进行配置。

我现在拥有的是:

module Sim (V:VEC) = struct
  module V = V
  module M = struct type data = V.t end
  let data_monitor : (M.data -> unit) ref = ref (fun d -> ())
  let loop n init_data = 
    let running_data = ref init_data in
    for _i = 1 to n do 
      !data_monitor !rdata; (* monitor here *)
      rdata := process_data !rdata
    done
end

即。我在那里放了一个存根监控功能参考。在实际的应用程序脚本中,我可以分配一个函数,例如将数据值累积到列表或类似列表中。有效。

所以问题是:这是实现我想要的 best/lowest overhead/nicest 方法吗?

这种方法似乎有点hackish,我宁愿使用模块系统而不是函数指针。但是,要流式传输的数据类型仅在仿函数 Sim 内定义。因此,在 Sim 之外的另一个模块 Sampler 中创建监视功能并以此参数化 Sim 似乎不方便 and/or 需要重复代码或递归模块。我试过了,但我无法使所有类型都相等。

编辑:这是它在没有函数引用的情况下的大致尝试:

module Sampler (V:VEC) : sig
  module V : VEC
  type data = V.t
  val monitor_data : data -> unit
end 
with type data = V.t = struct
  module V = V
  type data = V.t
  let monitor_data data = store_away_the data
end

module Sim (V:VEC) (Sampler:??) : sig
  ...
end with type M.data = V.t

?? 我不确定如何指定 Sampler 的输出签名,因为输入签名 VEC 仍然是免费的;我也不确定如何使类型相等工作。也许我在这里做错了。

正如评论中所讨论的那样,您可以使用高阶函数(而不是必须求助于高阶函子)来做这样的事情:

module type VEC = sig type t end
module Vec = struct type t = unit end

module Sim (V : VEC) =
struct
  module M = struct type data = V.t list end

  let process x = x

  let rec loop ?(monitor : M.data -> unit = ignore) n data =
    if n <= 0 then data
    else
      (monitor [];
      process data |> loop ~monitor (n - 1))
end

module MySim = Sim (Vec)

let monitor _ = print_endline "foo"

let () =
  MySim.loop ~monitor 5 ()
上面的

loop 将一个可选函数作为参数,您可以使用语法 ~monitor:my_fun~monitor:(fun data -> ...) 传递它。如果您已经在范围内有一个名为 monitor 的值,您可以简单地执行 ~monitor 来传递它。如果您不传递任何内容,则默认值为 ignore(即 fun _ -> ())。

我还以递归方式重写了 loop。上面的代码打印了 5 次 foo。请注意,您的 monitor 函数仍然可以来自 Sampler 模块,只是在实例化 Sim.

时无需传入整个模块

编辑:如果你仍然想声明一个高阶仿函数,你可以这样做 (...)

编辑 2:更改了给出附加信息的示例,高阶仿函数的原因是有多个监视函数要调用。请注意,在这种情况下,除了高阶函子之外还有其他解决方案:您可以将函数分组到一个记录中,然后将记录传递给 loop。与此类似,您可以传递一个 first-class 模块。或者,您可以创建一个采用变体类型的函数,其案例指示在哪个阶段调用监视函数,并携带与每个阶段关联的数据。您也可以为此使用 classes,但我不推荐它。但是,如果您致力于在 Sim.

中声明 M,则函子方法确实有一个优势

我在草图中省略了签名VEC,因为我的印象是提问者知道在哪里添加它,并且没有问题:)

module type SAMPLER =
sig
  type data
  val monitor : data -> unit
  val monitor' : data list -> unit
end

(* These are created inside Sim. *)
module type DATA =
sig
  type data
  val show : data -> string
end

(* Note that I am using destructive substitution (:=) to avoid the need
   to have a type data declared in the body of MySampler below. If you
   use a regular type equality constraint, you need to add a field
   "type data = Data.data" to the body. *)
module type SAMPLER_FN =
  functor (Data : DATA) -> SAMPLER with type data := Data.data

(* This is the higher-order functor (it takes another functor as an
   argument). *)
module Sim (Sampler_fn : SAMPLER_FN) =
struct
  (* Corresponds to module "Sim.M" in the question. *)
  module Data =
  struct
    type data = string
    let show s = s
  end

  (* Note that without additional type constraints or rearrangements,
     the type data is abstract to Sampler (more precisely, Sampler_fn
     is parametric over Data). This means that Sampler_fn can't
     analyze values of type data, which is why we need to provide
     functions such as Data.show to Sampler_fn for instances of
     Sampler_fn to be "useful". If you are trying to avoid this and
     are having trouble with these specific constraints, let me
     know. The ability to pass types and related values (functions
     in this case) to Sampler_fn is the main argument in favor of
     using a higher-order functor. *)
  module Sampler = Sampler_fn (Data)

  let simulate x =
    (* Call one monitoring function. *)
    Sampler.monitor "hi!";
    (* Do some computation and call another monitoring function. *)
    Sampler.monitor' ["hello"; "world"]
end

用法:

module MySampler (Data : DATA) =
struct
  let monitor data = data |> Data.show |> print_endline
  let monitor' data =
    data
    |> List.map Data.show
    |> String.concat " "
    |> print_endline
end

module MySim = Sim (MySampler)

let () = MySim.simulate ()

这会打印

hi!
hello world

完整性:

建立在 的仿函数部分之上,这就是我目前正在使用的。它仍然有点复杂,也许可以做得更简洁,但它有一些不错的优点。即:可以在集中位置(类型 SAMPLER 的模块)打开和关闭各个方面的监视,并且可以导出任意类型,即使它们仅在模拟器模块内部的某个地方定义。

我这样定义监控(=采样)模块和模块类型:

module type STYPE = sig type t end

module type SSAMPLER = sig
  type t
  val ev : t React.event
  val mon : t -> unit
end

module type SAMPLER_FN = functor (Data : STYPE) -> SSAMPLER
  with type t := Data.t

(* stub sampler function for a single one *)
module Never : SAMPLER_FN = functor (Data : STYPE) -> struct
  let ev = React.E.never
  let mon = ignore
end

(* event primitive generating sampling function *)
module Event : SAMPLER_FN = functor (Data : STYPE) -> struct
  let (ev : Data.t React.event), mon' = React.E.create ()
  let mon = mon' ?step:None
end

在这里,我使用 React 库生成输出数据流。 React.E.never 事件什么都不做,对应于采样被关闭。然后像这样指定完整的采样配置:

(* the full sampling config *)
module type SAMPLER = sig
  val sampler_pos : (module SAMPLER_FN)
  val sampler_step : (module SAMPLER_FN)
  (* and several more... *)
end

module NoSampling : SAMPLER = struct
  let sampler_pos = (module Never: SAMPLER_FN)
  let sampler_step = (module Never: SAMPLER_FN)
  (* ... *)
end

(* default sampling config *)
module DefaultSampling : SAMPLER = struct
  include NoSampling
  (* this is only possible when using first class modules *)
  let sampler_pos = (module Event : SAMPLER_FN)
end

可以避免第一个 class 模块,但是 DefaultSampling 中方便的包含和覆盖将不被允许。

在模拟库代码中是这样使用的:

module type VEC = sig
  type t
  val zeropos : t
  val wiggle : t -> t
end

module Sim (V:VEC) (Sampler:SAMPLER) = struct
  module V = V                  

  module M = struct
    type t = { mutable pos : V.t }
    val create () = { pos=V.zeropos }
    module Sampler_pos = (val Sampler.sampler_pos) (struct type nonrec t = t end)
    let update f m = m.pos <- f m.pos
  end

  module Sampler_b = (val Sampler.sampler_b) (struct type t = int end)

  let loop n (running_data:M.t) = 
    for i = 1 to n do 
      (* monitor step number: *)
      Sampler_b.mon i;
      (* monitor current pos: *)
      Sampler_pos.mon running_data;
      M.update V.wiggle running_data
    done 

end

此处,采样仿函数应用于模拟循环中的适当位置。 (val ...) 再次是必要的,只是因为第一个 class 模块包装。

最后,在应用程序脚本中,可以这样做:

module Simulator = Sim (V) (DefaultSampling);;

let trace = Simulator.M.Sampler_pos.ev
          |> React.E.fold (fun l h -> h :: l) []
          |> React.S.hold [];;

let init_m = Simulator.M.create () in
Simulator.loop 100 init_m;;

React.S.value trace;;

最后一行包含循环期间出现的 Simulator.M.t 类型值的累积列表。计步器的监控(一个愚蠢的例子)被关闭。通过制作另一个 SAMPLER 类型的采样函子并以此参数化 Sim,如果需要,可以进一步自定义监控。