【问题标题】:Temporal Recursion in F# InteractiveF# Interactive 中的时间递归
【发布时间】:2017-01-16 15:40:01
【问题描述】:

背景: 我一直在使用 Extempore 和 Opusmodus 在现场环境中进行计算机辅助作曲(在观众面前编写古典音乐)。由于我是一名专业的 .Net 开发人员,因此我开始编写自己的 .Net 软件(F# 和 C# 的组合),深受 Extempore 和 Opusmodus 的影响。我现在要实现时间递归,因为它可以在 Extempore 中工作,并且无法在 .Net 平台上找到方法。一些方向和灵感会很有帮助。

定义: 时间递归最简单地定义为任何代码块(函数、方法等),它安排自己在未来某个精确的时间点被回调。

Scheme 中的示例: 理论上,标准递归函数是一个时间递归函数,它会立即回调自身——即没有任何时间延迟。例如(在方案中):

;; A standard recursive function

(define my-func
  (lambda (i)
    (println 'i: i)
    (if (< i 5) 
        (my-func (+ i 1)))))

相同的功能,但使用时间递归将是这样的:

;; A temporally recursive function with 0 delay
;; (callback (now) my-func (+ i 1)) ~= (my-func (+ i 1))
;; (now) here means immediately - straight away

(define my-func
  (lambda (i)
    (println 'i: i)
    (if (< i 5)
      (callback (now) my-func (+ i 1)))))

在前面的示例中 (callback (now) my-func (+ i 1)) 提供与 (my-func (+ i 1)) 类似的功能 - 两者都负责立即回调 my-func,传递i 的递增值。然而,这两个递归调用的操作方式却大不相同。由递归调用 (callback (now) my-func (+ i 1)) 形成的时间递归被实现为与当前控制状态不同的事件。换句话说,虽然调用 (my-func (+ i 1)) 维护控制流,并且可能(假设没有尾部优化)调用堆栈,但 (callback (now) my-func (+ i 1)) 调度my-func 然后将控制流返回给实时调度器。

我的问题 鉴于我已经在 C# 中启动并运行了调度程序部分并且运行良好。我还可以安排将由 C# 调度程序调用的 F# 函数。但是如何完成一个预定的函数调用,其中函数本身可以使用 F# Interactive 实时更改。

所以我希望能够在 F# 交互中做的事情是这样的:

let playMeAgain time<ms>
    instrument.Play "c4 e4 g4"
    callback (time<ms> playMeAgain)

playMeAgain 1000<ms>

然后,只要我在 F# Interactive 中将此函数更改为以下内容,之前的 playMeAgain 就不会再次被调用,而是会调用新版本的函数(与 playMeAgain 的新绑定):

 let playMeAgain time<ms>
    instrument.Play "d4 f4 a4"
    callback (500<ms> playMeAgain)

在 .NET 下这可能吗?如何在 F# 下完成这种“热插拔”代码技术,因为这不是递归,因为 F# 定义了递归(因此需要 let rec playMeAgain 语法才能正确编译)。

有关时间递归的详细信息,请参阅this link

优秀的example of why Temporal Recursion is a crucial technique in this domain

添加代码以方便讨论带参数的时间递归 模块 TemporalRecursion = 打开 LcmMidi.MidiDotNet 打开 LCM.Instrument 打开 LCM.Sound.Sounds 打开 LcmMidi.MidiDotNet

    let mutable private temporalRecursives : Map<string, obj -> obj> = Map.empty

    let private call name args =
        let fn = temporalRecursives |> Map.find name
        fn args

    let private againHandler (args:System.EventArgs) =
        let newArgs = args :?> LcmMidi.TemporalRecursionEventArgs 
        let name = newArgs.FunctionName
        call name ()
        |> ignore

    let defOstinato name (f: 'a -> 'b) =
        if (temporalRecursives.ContainsKey name) then
            temporalRecursives <- Map.remove name temporalRecursives
        temporalRecursives <- Map.add name (fun arg -> box <| f (unbox arg)) temporalRecursives
        fun (a: 'a) -> call name (box a) |> unbox<'b>

    let repOstinato (name:string) (time:float) =
        let message = new LcmMidi.TemporalRecursionMessage(name, float32 time)
        message.Again.Add againHandler
        LcmMidi.MidiDotNet.TimedScheduler.Instance.Schedule(message)

带参数的时间递归 我希望能够做的是:

let pp (notation:string) = 
    defOstinato "prepPiano" (fun (notation) -> 
        piano.Play notation
        let nextNotation = markovChainOfChords notation
        repOstinato ("prepPiano", 8. , nextNotation) 
    )

pp("c4e4g4)

【问题讨论】:

    标签: recursion f#-interactive temporal


    【解决方案1】:

    以下是我写的原始答案,它提供了一个“通用”解决方案,即 la Lisp。但后来我想也许你不需要“通用”,也许你只有这样的一两个功能(即“和弦”和“打击乐”,就是这样吗?)。如果是这种情况,您可能会对将函数引用保留为可变变量感到满意:

    let mutable f : int -> int -> int = Unchecked.defaultof<_>
    f <- fun a b -> a + b
    f 5 6   // it = 11
    f <- fun a b -> a - b
    f 5 6   // it = -1
    f <- fun a b -> if a % 2 = 0 then a + b else f b (a-1) // recursion
    f 5 6   // it = 10
    
    let mutable temporal : unit -> unit
    temporal <- fun () -> doStuff; callback (time + 10) (fun () -> temporal())
    

    这带来了一些不便:

    1. 您不能同时声明、初始化和递归变量:可变值不能递归。所以你必须使用Unchecked.defaultOt&lt;_&gt;作为初始化值的尴尬技巧,然后单独分配它。
    2. 由于无法内联初始化值,因此必须明确指定其类型。
    3. let f &lt;- fun x -&gt; ... 的语法很尴尬。看起来fun x 在尖括号中。
    4. 当将这样的函数作为参数传递给callback 时,您需要始终将其包装在 lambda 表达式中,就像在我的示例中:(fun() -&gt; temporal()),否则callback 将获得对该函数的引用,因为它在致电callback 的时间,而不是预定时间。

    原答案

    要实现 Lisp 所实现的,你需要做 Lisp 所做的——即,以 Lisp 定义函数的方式定义函数,而不是作为程序执行之外的静态结构,而是作为将名称映射到代码的字典,即 Map&lt;string, obj -&gt; obj&gt; .

    显然,要做到这一点,您必须放弃let 构造并设计自己的构造。我们称之为defun。它将名称和代码作为参数,将它们放入字典中(字典必须是可变的,当然 - 是的,这就是它在 Lisp 中的实际工作方式),并返回各种“引用” - 一个包装器本身就是一个具有相同签名的函数,但是当被调用时,它会首先从字典中获取实际代码并执行。

    因为你的函数可能都是不同的类型,所以整个事情都需要擦除类型(因此obj -&gt; obj)。这有点令人不安,但是,嘿,它并不比 Lisp 差! :-)

    let mutable definitions : Map<string, obj -> obj> = Map.empty
    
    let call name arg = 
        let fn = Map.find name definitions // NOTE: will throw when name is not defined
        fn arg 
    
    let defun name (f: 'a -> 'b) = 
        definitions <- Map.add name (fun arg -> box <| f (unbox arg)) definitions
        fun (a: 'a) -> call name (box a) |> unbox<'b>
    

    用法:

    let x = defun "x" (fun a b -> a + b)
    x 5 6  // it = 11
    
    defun "x" (fun a b -> a - b)
    x 5 6  // it = -1
    

    请注意,从技术上讲,您可以通过犯错来打破这一点:

    defun "x" (fun a -> "Hello " + a)
    x 5 6  // InvalidCastException
    

    但是,嘿,这也是 Lisp 会做的事情!

    但我们还没有走出困境:仍然无法进行递归。

     let x = defun "x" (fun a b -> a + (x b a))  // `x` is not defined
    

    嗯...从技术上讲,您可以只说let rec x,它会起作用:

     let rec x = defun "x" (fun a b -> if a % 2 = 0 then a + b else x b (a-1))
     x 5 6   // it = 10
    

    但这会产生一个编译器警告,因为从技术上讲,根据x 的主体,编译器不能保证整个定义不会导致无限递归。在这种特殊情况下它不会,因为x 是在一个不会立即调用的 lambda 表达式中使用的。但编译器不知道这一点,因此发出警告。

    所以从技术上讲,您可以在这里停下来。但是这整个不健全让我有点恼火,所以我将提供另一种递归方式:向函数传递一个额外的参数,这将是对自身的引用。然后函数可以调用它或者传递给callback

    let mutable definitions : Map<string, obj -> obj> = Map.empty
    
    let call name arg = 
        let fn = Map.find name definitions
        fn arg // NOTE: will throw when name is not defined
    
    let defun name (f: ('a -> 'b) -> 'a -> 'b) = 
        let self = fun (a: 'a) -> call name (box a) |> unbox<'b>
        definitions <- Map.add name (fun arg -> box <| f self (unbox arg)) definitions
        self
    
    // Note: no `rec` keyword
    let x = defun "x" (fun recur a b -> if a % 2 = 0 then a + b else recur b (a-1))
    x 5 6  // it = 10
    
    defun "x" (fun recur a b -> a - b)
    x 5 6  // it = -1
    
    defun "x" (fun recur a -> "Hello " + a)
    x 5 6  // InvalidCastException
    

    【讨论】:

    • 非常感谢。我完全被卡住了,但你让我进入了正确的方向,它现在正在工作:-) 关于递归性,因为它是调度程序再次调用该函数,所以我能够保持代码非常干净。再次感谢您的帮助!!!!
    • 这里有一个简短的视频演示当前的时间递归实现。 youtube.com/watch?v=bsi6b7SXAbA&lc(defun 变成了 defOstinato 并且计划下一个回调是使用 repOstinato 函数完成的)。这是一个没有参数的实现。还不能让它工作。我猜参数也必须存储在字典中并由repOstinato 更新。在这方面提供一些帮助会很棒。
    • 我真的不能说任何确定的东西,因为我没有看到你对 defOstinatorepOstinato 的定义,但马上,我看到你试图重复你的按名称而不是按引用函数。可能不是这个原因,但是没有看到你的代码我就不能再说什么了。
    • 另外,你在评论中提到了参数,但我没有看到你的函数有任何参数。
    • Fyoder,我应该添加当前实现的代码。对此感到抱歉。我现在在原始帖子的末尾添加了 TemporalRecursion 模块的源代码。希望能促进讨论。您将看到一条消息被传递给调度程序。该消息包含要调用的函数的名称以及函数的调用时间(以节拍为单位)。
    猜你喜欢
    • 1970-01-01
    • 2010-10-28
    • 1970-01-01
    • 1970-01-01
    • 2016-05-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多