воскресенье, 12 сентября 2010 г.

О вычислительных выражениях

Уже почти год пишу на F#, и мне так понравились вычислительные выражения! Удивительно простая в использовании штука. Такое выражение можно записать в императивном стиле как почти обычный код на F#, используя while, for и try, а оно потом на выходе будет преобразовано компилятором F# в выражение, состоящее из композиции функции. Императивная конструкция превращается в сугубо функциональное выражение. Замечательная идея. Сюда же легко вписываются монады и моноиды. Вот, с монадными трансформерами накладка.

Поддержка таких выражений – это просто непередаваемая по значимости вещь для императивных монад типа Async для асинхронных вычислений. Более того, не нужно поддерживать на уровне языка продолжения – для них можно использовать те же вычислительные выражения (но нужна поддержка TCO на уровне исполняющей среды). Продолжения не совсем императивны, но все же блоки try-with и try-finally обрабатывать в таком языке как F# надо. И здесь нет известной проблемы Common Lisp.

Кстати, о Лиспе. Считаю, что вычислительные выражения и макросы мешают друг другу. Эти выражения предполагают, что есть ограниченное множество конструкций языка, которые могут быть “офункционалены” (превращены в функции). Макросы это нарушают. Когда я добавлял поддержку вычислительных выражений в Немерле, то оставлял макросы как есть. Не знаю, можно ли придумать здесь что-то лучше? (интересно, а как макросы и продолжения сосуществуют в Схеме?)

Мне теперь стало очень не хватать вычислительных выражений в Скале и Окамле. Скаловский for-comprehension смотрится как-то слабо, особенно для императивных монад, когда вычисления нужно разбавить обычным кодом. Что касается Окамла, то для него есть одно расширение, но очень хотелось бы, чтобы решение было стандартизировано и “из-коробки”. Надеюсь, что когда-нибудь добавят.

7 комментариев:

  1. а можно маленький пример использования? если есть возможность, то на лиспе. нет - на чём угодно.

    ОтветитьУдалить
  2. Только F# :)

    Вот, пример с асинхронными вычислениями. Периодически посылает сообщение SaveModels. Кусок с do! - это асинхронное подвычисление (асинхронное усыпление системного потока, т.е. без фактического простаивания).

    <------
    let saver = async {

      if server.SavingPeriod > 0 then

        while true do

          do! Async.Sleep server.SavingPeriod
          agent.Post SaveModels
    }

    do! Async.StartChild saver |> Async.Ignore
    ------>

    А это имитационная [законченная] модель станка. Вычисление Hold удерживает текущий поток исполнения имитационного процесса в течение заданного времени.

    <------
    type Machine (e, id) =

      inherit Process (e)

      override x.Activate = dynamicscont {
        while true do
          let! startUpTime = DynamicsCont.lift time
          let upTime = expovariate upRate
          do! x.Hold (upTime)
          let! finishUpTime = DynamicsCont.lift time
          totalUpTime <- totalUpTime +
            (finishUpTime - startUpTime)
          let repairTime = expovariate repairRate
          do! x.Hold (repairTime)
      }
    ------>

    В обоих случаях код async {...} и dynamicscont {...} со всеми while, let! и do! преобразуется компилятором в композиции функций. let! - это монадический bind. do! - специализированная версия let! для типа unit.

    ОтветитьУдалить
  3. Еще до кучи. Считывается граф из XML с помощью стандартного парсера XmlReader. Как и раньше, let! - это монадический bind. Опять же, все вычислительное выражение xmlreader {...} преобразуется в композицию функций.

    <------
    static let read =
      XmlReader.elem "graph" <| xmlreader {

        let! id = XmlReader.attr "guid"
        let! (nodes, rels) = readContents

        let graph = Graph (id)
        graph.Nodes.AddGraph (nodes, rels)

      return graph
    }
    ------>

    ОтветитьУдалить
  4. > Скаловский for-comprehension смотрится как-то слабо, особенно для императивных монад, когда вычисления нужно разбавить обычным кодом.

    Да. Нужно бы подумать, можно ли написать соответствующий плагин для компилятора.

    ОтветитьУдалить
  5. Я правильно понял что это вариация на тему хаскельной do-нотации?

    ОтветитьУдалить
  6. Да, очень близко к нотации do. В одном ряду с for-comprehension из Скалы и Linq из C#.

    ОтветитьУдалить
  7. @alexey-rom

    Я и надеюсь на такой плагин :) Уж очень удобны эти выражения, например, как генераторы ленивой последовательности (моноид seq).

    ОтветитьУдалить