F# Interactive 中的时间递归

Temporal Recursion in F# Interactive

上下文: 我一直在使用 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 然后 returns 控制流到实时调度程序。

我的问题 鉴于我已经在 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# 下完成此 "hot swappable" 代码技术,因为这不是递归,因为 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)

下面是我写的原始答案,它提供了一个 "general" 解决方案,一个 la Lisp。但后来我想也许你不需要 "general",也许你只是有这样的 one-two 函数(即 "chords" 和 "percussion",就这样?)。如果是这种情况,您可能只满足于将函数引用保留为可变变量:

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<_>作为初始化值的笨办法,然后单独赋值
  2. 由于不能内联初始化值,因此必须明确指定其类型。
  3. let f <- fun x -> ... 的语法很别扭。它有点像 fun x 在尖括号中。
  4. 将此类函数作为参数传递给 callback 时,您需要始终将其包装在 lambda 表达式中,如我的示例:(fun() -> temporal()),否则 callback 将获得引用函数调用时的状态 callback,而不是预定时间的状态。

原回答

要实现 Lisp 所实现的目标,您需要做 Lisp 所做的事情 - 即,按照 Lisp 定义函数的方式定义函数,而不是作为程序执行之外的静态构造,而是作为将名称映射到代码的字典,即 Map<string, obj -> obj>

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

因为你的函数可能都是不同的类型,所以整个过程都需要擦除类型(因此 obj -> 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