Временная рекурсия в 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())

Это идет с несколькими неудобствами, хотя:

  1. Вы не можете объявить переменную, инициализировать ее и сделать ее рекурсивной одновременно: изменяемые значения не могут быть рекурсивными. Таким образом, вы должны использовать неловкий трюк с Unchecked.defaultOt<_> в качестве значения инициализации, а затем назначьте его отдельно.
  2. Поскольку вы не можете инициализировать значение inline, вы должны явно указать его тип.
  3. Синтаксис let f <- fun x -> ... неловко Это вроде как fun x в угловых скобках.
  4. При передаче такой функции в качестве аргумента 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
Другие вопросы по тегам