Временная рекурсия в F# Интерактив
Контекст: я использовал Extempore и Opusmodus для создания компьютерной композиции в живых ситуациях (программирование классической музыки перед аудиторией). Так как я профессиональный разработчик.Net, я начал писать свое собственное программное обеспечение для.Net (комбинация F# и C#), находящееся под сильным влиянием как Extempore, так и Opusmodus. Теперь я подошел к вопросу реализации Temporal Recursion, так как он работает в Extempore, и не могу найти способ сделать это на платформе.Net. Некоторые направления и вдохновение были бы очень полезны.
Определение. Временная рекурсия проще всего определяется как любой блок кода (функция, метод и т. Д.), Который планирует себя для обратного вызова в определенный момент времени в будущем.
Примеры в схеме: Теоретически стандартная рекурсивная функция - это рекурсивная во времени функция, которая вызывает себя немедленно, то есть без какой-либо временной задержки. Например (на схеме):
;; A standard recursive function
(define my-func
(lambda (i)
(println 'i: i)
(if (< i 5)
(my-func (+ i 1)))))
Та же функция, но с использованием Temporal Recursion будет выглядеть примерно так:
;; 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 (сейчас) my-func (+ i 1)) выполняет функцию, аналогичную (my-func (+ i 1)) - оба отвечают за немедленный обратный вызов my-func, передавая увеличенное значение для меня. Однако способ, которым эти два рекурсивных вызова работают, существенно отличается. Временная рекурсия, которая формируется рекурсивным вызовом (callback (сейчас) my-func (+ i 1)), реализуется как событие, отличное от текущего состояния управления. Другими словами, в то время как вызов (my-func (+ i 1)) поддерживает поток управления и, возможно, (при отсутствии хвостовой оптимизации) стек вызовов, (callback (сейчас) my-func (+ i 1)) планирует my-func, а затем возвращает поток управления планировщику в реальном времени.
Мой вопрос Учитывая, что у меня есть планировщик, работающий на C# и работающий хорошо. Я также могу запланировать функцию F#, которая будет вызываться планировщиком C#. Но как выполнить запланированный вызов функции, когда сама функция может быть изменена в режиме реального времени с помощью 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 для правильной компиляции).
Для получения подробной информации о Temporal Recursion смотрите эту ссылку
Отличный пример того, почему Temporal Recursion является важной техникой в этой области:
КОД ДОБАВЛЕН, чтобы облегчить обсуждение временной рекурсии с помощью модуля параметров TemporalRecursion = open LcmMidi.MidiDotNet открыть LCM.Инструмент открыть LCM.Sound.Sounds open 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)
1 ответ
Ниже приведен исходный ответ, который я написал, который дает "общее" решение, как на 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())
Это идет с несколькими неудобствами, хотя:
- Вы не можете объявить переменную, инициализировать ее и сделать ее рекурсивной одновременно: изменяемые значения не могут быть рекурсивными. Таким образом, вы должны использовать неловкий трюк с
Unchecked.defaultOt<_>
в качестве значения инициализации, а затем назначьте его отдельно. - Поскольку вы не можете инициализировать значение inline, вы должны явно указать его тип.
- Синтаксис
let f <- fun x -> ...
неловко Это вроде какfun x
в угловых скобках. - При передаче такой функции в качестве аргумента
callback
, вы должны всегда заключать его в лямбда-выражение, как в моем примере:(fun() -> temporal())
, иначеcallback
получит ссылку на функцию, как это было во время вызоваcallback
не так, как это делается в назначенное время.
Оригинальный ответ
Чтобы достичь того, чего достигает Lisp, вам нужно сделать то, что делает Lisp, а именно определить функции так, как Lisp определяет функции, не как статические конструкции вне выполнения программы, а как словарь, отображающий имена в коде, т.е. Map<string, obj -> obj>
,
Для этого, очевидно, вам придется отказаться от let
построить и придумать свой. Давайте назовем это defun
, Он будет принимать имя и код в качестве параметров, помещать их в словарь (словарь должен быть изменяемым, конечно - и да, именно так он и работает в Лиспе) и возвращать "ссылку" своего рода - Оболочка, которая сама является функцией с той же сигнатурой, но при вызове сначала извлекает реальный код из словаря и выполняет его.
Поскольку ваши функции, вероятно, все разных типов, все это нужно будет стереть типы (следовательно, obj -> obj
). Это немного неприятно, но эй, это не хуже, чем Лисп!:-)
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
Но эй, вот что сделает Лисп!
Но мы еще не из леса: до сих пор не можем сделать рекурсию.
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
используется внутри лямбда-выражения, которое не вызывается сразу. Но компилятор этого не знает, отсюда и предупреждение.
Так что технически вы могли бы просто остановиться здесь. Но вся эта бессмысленность меня немного раздражает, поэтому я предложу другой способ сделать рекурсию: передать дополнительный параметр функции, который будет ссылкой на саму себя. Затем функция может вызвать его или передать 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