OCaml中退出循环的惯用例外

时间:2015-06-19 11:25:01

标签: loops ocaml idiomatic imperative-programming

在OCaml中,可以通过引发异常提前退出命令式循环。

虽然在OCaml中使用命令式循环不是惯用的本身,但我想知道在早期退出时模拟命令式循环的最惯用方法是什么(考虑到方面如表演,如果可能的话。)

例如,an old OCaml FAQ提及异常Exit

  

Exit:用于跳出循环或函数。

它还是最新的吗? standard library只是将其视为一般用途例外:

  

任何库函数都不会引发Exit异常。它可以在您的程序中使用。

相关地,this answer提到另一个问题,提到使用预先计算的let exit = Exit异常来避免循环内的分配。还需要吗?

此外,有时候人们想要从具有特定值的循环中退出,例如raise (Leave 42)。是否有惯用的例外或命名约定来执行此操作?在这种情况下我应该使用引用(例如let res = ref -1 in ... <loop body> ... res := 42; raise Exit)吗?

最后,在嵌套循环中使用Exit可以防止某些人想要退出几个循环,例如Java中的break <label>。这将需要定义具有不同名称的异常,或者至少使用整数来指示应退出多少范围(例如Leave 2以指示应退出2个级别)。同样,这里有一个惯用的方法/异常命名吗?

4 个答案:

答案 0 :(得分:5)

正如最初在评论中发布的那样,在OCaml中提前退出的惯用方法是使用continuation。在您希望提前返回的位置,您创建一个延续,并将其传递给可能提前返回的代码。这比循环标签更通用,因为你可以退出任何有权访问延续的东西。

此外,正如在评论中发布的那样,请注意raise_notrace对于您从不希望运行时生成的跟踪的异常的使用。

A&#34;天真&#34;第一次尝试:

module Continuation :
sig
  (* This is the flaw with this approach: there is no good choice for
     the result type. *)
  type 'a cont = 'a -> unit

  (* with_early_exit f passes a function "k" to f. If f calls k,
     execution resumes as if with_early_exit completed
     immediately. *)
  val with_early_exit : ('a cont -> 'a) -> 'a
end =

struct
  type 'a cont = 'a -> unit

  (* Early return is implemented by throwing an exception. The ref
     cell is used to store the value with which the continuation is
     called - this is a way to avoid having to generate an exception
     type that can store 'a for each 'a this module is used with. The
     integer is supposed to be a unique identifier for distinguishing
     returns to different nested contexts. *)
  type 'a context = 'a option ref * int64
  exception Unwind of int64

  let make_cont ((cell, id) : 'a context) =
    fun result -> cell := Some result; raise_notrace (Unwind id)

  let generate_id =
    let last_id = ref 0L in
    fun () -> last_id := Int64.add !last_id 1L; !last_id

  let with_early_exit f =
    let id = generate_id () in
    let cell = ref None in
    let cont : 'a cont = make_cont (cell, id) in
    try
      f cont
    with Unwind i when i = id ->
      match !cell with
      | Some result -> result
        (* This should never happen... *)
      | None        -> failwith "with_early_exit"
end



let _ =
  let nested_function i k = k 15; i in

  Continuation.with_early_exit (nested_function 42)
  |> string_of_int
  |> print_endline

正如您所看到的,上面通过隐藏异常来实现提前退出。 continuation实际上是一个部分应用的函数,它知道创建它的上下文的唯一id,并且有一个引用单元来存储结果值,同时将异常抛出到该上下文。上面的代码打印15.您可以根据需要传递延续k。您还可以在将函数传递给f时立即定义函数with_early_exit,从而产生类似于在循环上具有标签的效果。我经常使用它。

以上问题是'a cont的结果类型,我将其任意设置为unit。实际上,类型'a cont的函数永远不会返回,因此我们希望它的行为类似于raise - 在任何类型都可以使用的情况下都可以使用。但是,这并不能立即奏效。如果您执行type ('a, 'b) cont = 'a -> 'b之类的操作,并将其传递给嵌套函数,则类型检查器将在一个上下文中推断'b的类型,然后强制您仅在具有相同内容的上下文中调用continuation类型,即你不能做像

这样的事情
(if ... then 3 else k 15)
...
(if ... then "s" else k 16)

因为第一个表达式强制'bint,但第二个表达式要求'bstring

为了解决这个问题,我们需要提供一个类似于raise的函数来提前返回,即

(if ... then 3 else throw k 15)
...
(if ... then "s" else throw k 16)

这意味着摒弃纯粹的延续。我们必须在上面部分应用make_cont(并将其重命名为throw),然后传递裸上下文:

module BetterContinuation :
sig
  type 'a context

  val throw : 'a context -> 'a -> _
  val with_early_exit : ('a context -> 'a) -> 'a
end =

struct
  type 'a context = 'a option ref * int64
  exception Unwind of int64

  let throw ((cell, id) : 'a context) =
    fun result -> cell := Some result; raise_notrace (Unwind id)

  let generate_id = (* Same *)

  let with_early_exit f =
    let id = generate_id () in
    let cell = ref None in
    let context = (cell, id) in
    try
      f context
    with Unwind i when i = id ->
      match !cell with
      | Some result -> result
      | None        -> failwith "with_early_exit"
end



let _ =
  let nested_function i k = ignore (BetterContinuation.throw k 15); i in

  BetterContinuation.with_early_exit (nested_function 42)
  |> string_of_int
  |> print_endline

表达式throw k v可以在需要不同类型的上下文中使用。

我在我工作的一些大型应用程序中普遍使用这种方法。我甚至更喜欢常规例外。我有一个更复杂的变体,其中with_early_exit的签名大致如下:

val with_early_exit : ('a context -> 'b) -> ('a -> 'b) -> 'b

其中第一个函数表示尝试执行某些操作,第二个函数表示可能导致类型'a的错误的处理程序。与变体和多态变体一起,这给出了更明确类型的异常处理。它对多态变体特别有用,因为编译器可以推断出错误变量集。

Jane Street方法实际上与此处描述的方法相同,事实上我之前有一个实现,它使用一流模块生成异常类型。我不确定为什么我最终选择了这个 - 可能会有微妙的差异:)

答案 1 :(得分:3)

只是为了回答我在其他答案中没有提到的问题的特定部分:

  

...使用预先计算的let exit = Exit异常来避免循环内的分配。还需要吗?

我在Core_bench上使用4.02.1+fp做了一些微基准测试,结果表明没有显着差异:比较两个相同的循环,一个包含在循环之前声明的本地exit和另一个循环一个没有它,时差很小。

此示例中raise Exitraise_notrace Exit之间的差异也很小,在某些运行中约为2%,在其他运行中高达7%,但它可能在此类错误范围内简短的实验。

总的来说,我无法衡量任何明显的差异,所以除非有人会有Exit/exit显着影响性能的例子,否则我会更喜欢前者,因为它更清晰,避免创建一个无用的变量。

最后,我还比较了两个习语之间的区别:在退出循环之前使用对值的引用,或者创建包含返回值的特定异常类型。

参考结果值+ Exit

 let res = ref 0 in
 let r =
   try
     for i = 0 to n-1 do
       if a.(i) = v then
        (res := v; raise_notrace Exit)
     done;
     assert false
   with Exit -> !res
 in ...

使用特定的例外类型:

 exception Res of int
 let r =
   try
     for i = 0 to n-1 do
       if a.(i) = v then
         raise_notrace (Res v)
     done;
     assert false
   with Res v -> v
 in ...

再次,差异很小,并且在运行之间变化很大。总体而言,第一个版本(参考+ Exit)似乎有一点点优势(速度提高0%到10%),但差异不足以推荐一个版本而不是另一个版本。

由于前者需要定义初始值(可能不存在)或使用选项类型来初始化引用,后者需要为循环返回的每种类型的值定义新的异常,因此这里没有理想的解决方案

答案 2 :(得分:1)

Exit没问题(我不确定我是否可以说它是惯用的)。但是,如果您使用的是最新的编译器(从4.02开始),请确保您正在使用raise_notrace

更好的解决方案是使用OCaml Core library中的with_return。它不会对范围有任何问题,因为它将为每个嵌套创建一个全新的异常类型。

当然,您可以获得相同的结果,或者只是获取Core实现的源代码。

更加惯用,不是使用异常来缩短迭代次数,而是考虑使用现有算法(findfind_mapexists等)或者只是写一个递归函数,如果没有算法适合你。

答案 3 :(得分:1)

关于这一点

  

使用预先计算的let exit = Exit异常来避免分配   在循环内。还需要吗?

答案是 no ,其中包含足够新版本的OCaml。以下是OCaml 4.02.0的Changelog的相关摘录。

  
      
  • PR#6203:不再分配常量异常构造函数(Alain Frisch)
  •   

这是PR6203:http://caml.inria.fr/mantis/view.php?id=6203