奇怪的行为定义意味着F#中的运算符

时间:2013-07-25 21:47:58

标签: f#

我尝试在F#中定义我自己的逻辑隐含运算符,如下所示

let (-->) p q = (not p) || q

但是当我真正试用它时,似乎没有实现短路OR

> false --> ((2/0)=1);;
System.DivideByZeroException: Attempted to divide by zero.at <StartupCode$FSI_0015>.$FSI_0015.main@()
Stopped due to error
> 

它不应该评估结果,但它是

有人能看到这里有什么问题吗?我在VS 2012中运行F#

4 个答案:

答案 0 :(得分:10)

当您编写a --> b时,真正发生的事情是调用名为-->的函数,其中包含2个参数ab运算符语法只是一些语法糖。

在调用函数之前,运行时必须评估该函数的所有参数。因此,在调用-->之前,它首先评估false,然后评估(2/0)=1。在评估最后一个表达式时,它会抛出异常。您的暗示函数永远不会被调用!

在其他一些语言中,例如Haskell,你有懒惰的评价。也就是说,只有在函数内部实际访问函数的参数时,才会评估它们的参数。您可以通过不传递值来模拟它,但是传递一个计算结果为该值的函数,或者传递一个thunk的函数。

请注意,要在F#(thunks)中实现此类功能,必须稍微修改该函数:它必须调用 thunk来获取其值,就像给出的示例一样约翰帕尔默:

let --> p q = (not p) || q()
let thunk = (fun _ -> ((2/0)=1))
false --> thunk

注意定义中的函数调用q()暗示运算符。如果你没有在第二个参数上给它一个thunk,它将不再起作用了!

答案 1 :(得分:6)

您还可以使用lazy按需评估第二个参数:

let (-->) p (q: Lazy<bool>) = (not p) || q.Force()

你可以称之为:

false --> (lazy (2/0=1))

答案 2 :(得分:3)

RHS的评估必须在传递给函数之前进行,因此除法很早就会发生,所以这样的警卫不会起作用。

您可以改为传递类似的功能

let --> p q = (not p) || q()

false --> (fun _ -> ((2/0)=1))

答案 3 :(得分:2)

F#有严格的评估策略。这意味着在传递给函数之前会对参数进行求值。因此,2/0=1在传递给-->函数之前会进行评估,因此||的短路不会影响2/0=1的评估,因为在||之前评估。

您需要转换您的函数(-->运算符)以按名称而不是按值获取其参数。实际上,这意味着要使其() -> 'TLazy<'T>

let (-->) p q = (not <| p()) || q()

> (fun () -> false) --> (fun () -> 2/0=1);;
true

或者,使用内置的lazy构造。通过这种方式,您可以使用更简洁的语法实现相同的结果(假设您的计算不依赖于副作用):

let (-->) (p : Lazy<_>) (q : Lazy<_>) = (not <| p.Force()) || q.Force()

> lazy false --> lazy (2 / 0 = 1)
true

它有效,但看起来不太方便。不幸的是,我认为没有简单的方法来摆脱fun () -> ...,但我认为你可以通过定义一些常量来减少样板:

let True, False = (fun () -> true), (fun () -> false)

为了进一步减少为每个参数创建lambda的样板,您可以尝试使用代码引用(以及像Unquote这样的库来评估它们):

let (-->) p q = (not (eval p)) || (eval q)

<@ false @> --> <@ 2 / 0 = 1 @>