代码错误或我的误解?

时间:2014-08-07 17:58:38

标签: f#

我看到了我无法从F#编译器解释的行为(Visual F#3.1.1.0) - 表面上出现的只是具有命名本地和传递临时之间的区别实际上产生了行为差异

我不了解F#行为,或者这是代码生成错误? (我知道,后者更有可能。)

Repro - 我发现在不使用Reactive Extensions的情况下很难重现,所以这就像我得到它一样简单。请注意,try1try2几乎相同。

open System
open System.Reactive.Linq
open System.Threading

let interval = TimeSpan.FromSeconds(0.5)
let testDuration = TimeSpan.FromSeconds(2.0)

let mkHandler () = // creates a function that closes over state
    let count  = ref 0
    fun _ -> count := !count + 1
             printfn "State is now %d" !count

let try1 () =
    printfn "try1"
    let handler = mkHandler ()
    use subscription = Observable.Interval(interval).Subscribe(handler)
    Thread.Sleep(testDuration)

let try2 () =
    printfn "try2"
    // creates handler inline:
    use subscription = Observable.Interval(interval).Subscribe(mkHandler ())
    Thread.Sleep(testDuration)

[<EntryPoint>]
let main argv = 
    try1 ()
    try2 ()
    0

输出 - try1try2功能分别说明了所需和不良行为。该计划的输出是:

try1
State is now 1
State is now 2
State is now 3
try2
State is now 1
State is now 1
State is now 1

根据我的理解try2应该与try1的行为相同。如果没有,请说明这种微小差异应该如何起作用。


通过检查反编译器的输出,我确定了以下内容:

mkHandler运作正常;它创建了一个关闭唯一状态的函数。当多次调用时,它会改变该状态。

Subscribetry1同时调用try2的相同重载:public static IDisposable Subscribe<T>(this IObservable<T> source, Action<T> onNext)

try1生成的幕后帮助程序代码将关闭处理程序函数并正确调用它:

[CompilationMapping(SourceConstructFlags.Closure)]  
[Serializable]  
// subscription@16  
internal sealed class subscriptionu004016
{
    public FSharpFunc<long, Unit> handler;

    public subscriptionu004016(FSharpFunc<long, Unit> handler)
    {
    }

    internal void Invoke(long obj)
    {
        this.handler.Invoke(obj);
    }
}

try2的幕后帮助程序代码不会关闭处理程序函数,而是在每次调用时调用mkHandler工厂函数;这解释了输出,但不是理想的行为:

[CompilationMapping(SourceConstructFlags.Closure)]
[Serializable]
// subscription@22-1
internal sealed class subscriptionu004022u002d1
{
    public subscriptionu004022u002d1()
    {
    }

    internal void Invoke(long obj)
    {
        Program.mkHandler<long>().Invoke(obj);
    }
}

重申我的问题:为什么这两个功能表现不一样?这是代码生成错误吗?以上都不是吗?

1 个答案:

答案 0 :(得分:5)

据我所知,您的代码没有任何问题 - 您所做的事情是有道理的。这似乎是F#编译器中的一个微妙的错误。

我怀疑编译器如何解析Subscribe方法有问题。您的代码正在创建F#函数值,但编译器会自动将其包装到Action<int64>委托中,并使用Rx版本的Subscribe。但是,它通常不会自动将部分应用的函数转换为代理 - 它似乎只发生在这种情况下。

最简单的解决方法似乎是将mkHandler函数更改为显式创建委托,然后一切按预期工作:

let mkHandler () = // creates a function that closes over state
    let count  = ref 0
    Action<int64>(fun _ -> 
      count := !count + 1
      printfn "State is now %d" !count)

编辑:经过一番调查后,我会说这是一个特别使用Subscribe IObservable<T>方法的错误。由于F#会自动将事件视为IObservable<T>值,因此它会对它们进行一些特殊处理,并添加Subscribe方法。如果在其他地方声明了Subscribe扩展名,则会发生冲突并且事情会中断。

我能找到的最简单的repro是使用:

创建一个C#项目
public static class Extensions {
  public static void Subscribe(this IObservable<int> c1, Action<int> f) { 
    f(1);
    f(2);
  }
}

然后完成你所做的事情:

let partial() = 
  printfn "called"
  fun n -> ()

let a = new Event<int>()
let o = a.Publish.Subscribe(partial())

这两次打印“被叫”,而它应该只被调用一次。我创建了一个bug for this issue on the F# bug tracker