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())
不过这带来了一些不便:
- 您不能同时声明变量、初始化它并使其递归:可变值不能递归。所以只好用
Unchecked.defaultOt<_>
作为初始化值的笨办法,然后单独赋值
- 由于不能内联初始化值,因此必须明确指定其类型。
let f <- fun x -> ...
的语法很别扭。它有点像 fun x
在尖括号中。
- 将此类函数作为参数传递给
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
上下文: 我一直在使用 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())
不过这带来了一些不便:
- 您不能同时声明变量、初始化它并使其递归:可变值不能递归。所以只好用
Unchecked.defaultOt<_>
作为初始化值的笨办法,然后单独赋值 - 由于不能内联初始化值,因此必须明确指定其类型。
let f <- fun x -> ...
的语法很别扭。它有点像fun x
在尖括号中。- 将此类函数作为参数传递给
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