f # Вычислительные выражения для генерации кода

Хотя можно найти несколько примеров того, как выполнять синтаксические анализаторы композиционного рекурсивного спуска с выражениями вычисления f #, я попытался использовать их для противоположного. Чтобы создать легко читаемый код для генерации (c ++) исходных файлов из некоторых данных XML. Тем не менее, я застрял, и было бы очень признательно, если бы сообщество могло помочь мне найти мои недопонимания. Для всеобщего блага я надеюсь, что вскоре в этом посте будет продемонстрировано, как создавать генераторы кода классным способом с помощью выражений вычисления f # в монадическом стиле.

Вот как далеко я продвинулся (упрощенно, опуская входные данные для поколения для целей этого вопроса):

// in my full fledged application, State type also contains the Input data, used for generating code.
type State() = 
    let builder = new System.Text.StringBuilder()
    let mutable indentLevel : int = 0
    member this.Result() = builder.ToString()
    member this.Emit (s : string) : unit = builder.Append( s )
    // ... Methods allowing to do the indenting right, using indentLevel. And adding Output to the builder instance.
    member this.Indent() = indentLevel <- indentLevel + 1
    member this.Exdent() = indentLevel <- indentLevel - 1
// The return value of the Formatters is State only to allow for |> pipelining.
type Formatter = State -> State
type FormatterBuilder() = 
    // Q: Bind() Kind of Looks wrong - should it be a generic, taking one generic first Parameter? See Class function below.
    member this.Bind (state,formatter) = formatter state
    member this.Return state = state              // Q: Not sure if this is the way to go. Maybe some Lambda here?!

let format = new FormatterBuilder()

// Q: Now Comes the part I am stuck in!
// I had the idea to have a "Block" function which 
// outputs the "{", increases the indent Level, 
// invokes the formatters for the Content of the block, 
// then reduces the indent Level, then Closes "}". 
// But I have no idea how to write this.
// Here my feeble attempt, not even sure which Parameters this function should take.
let rec Block (formatters : Formatter list) (state : State) : State =
    format 
        {
            state.EmitLine("{") // do I Need a "do!" here?
            state.Indent()
            formatters |> List.iter (fun f -> do! f state) // Q: "state" is not really propagated. How to do this better?
            state.Exdent()
            state.EmitLine "}"
        }
// Functions with "Get" prefix are not shown here. They are supposed to get the Information
// from the Input, stored in State class, which is also not shown here.
let rec Namespace (state : State) : State =
    format
        {
             state.EmitLine(GetNameSpace state)
        }
let rec Class (classNode : XmlNode) (state : State) : State =
     Format
        { 
             do! TemplateDecl classNode state   // TemplateDecl function not shown in sample code
             do! ClassDecl classNode state
             do! Block [ NestedTypes classNode; Variables classNode; // ... ] // just to give the idea. Q: the list seems wrong here - how to do it better? 
        }
let GenerateCode() : string = 
     let state = new State()
     format
         {
             do! Namespace state    // Q: Is there a way to get rid of the passing of state here?
             do! Block 
                [   // Q: Maybe a Seq is better than a list here?
                 for c in State.Classes do // Q: requires override of a few functions in Builder class, I guess?!
                  do! Class c state
                ]
         }    
     state.Result()

Очевидно, что приведенный выше код в лучшем случае показывает только то, чего я пытаюсь достичь. Мое исследование не дало хороших примеров того, как использовать вычислительные выражения. Многие примеры, которые я нашел, останавливаются на том, чтобы показать, как объявляется построитель, или немного позже, но не демонстрируют, как на самом деле писать окончательные выражения.

Итак, если кто-то найдет время опубликовать реальный образец, который делает то, что пытается сделать мой бредовый код выше, это будет очень поучительно и заполнит пробел в том, что можно найти в Интернете в отношении этого (по крайней мере, для меня) сбивающего с толку аспекта. программирования на f #.

В моем примере кода выше я также не вижу, что я получаю от монады построителя. Код средства форматирования не выглядит чище по сравнению с немонадической реализацией.

Было бы здорово, если бы кто-нибудь добавлял подписи и типы в Параметры в ответных сообщениях; по крайней мере, для меня это намного более понятно по сравнению со стилем «пусть компилятор найдет типы».


person user2173833    schedule 25.06.2013    source источник
comment
Вас заинтересует функциональное решение, в котором не используются вычислительные выражения? У меня есть один, который я использовал в нескольких проектах для чего-то очень похожего, и он очень прост и удобен в использовании. Я буду рад опубликовать это, если хотите.   -  person Jack P.    schedule 26.06.2013
comment
@JackP. Это наверняка обогатит дискуссию и будет очень кстати! Хотя это не помогло бы мне понять, как работает монадическое программирование на f #, оно очень хорошо соответствовало бы текущей теме.   -  person user2173833    schedule 26.06.2013


Ответы (1)


Хорошо, как я уже упоминал в комментариях, это решение функционального стиля, которое я использовал некоторое время с хорошим успехом, хотя оно не чисто -функционально и просто использует некоторые простые функции вместо вычислительное выражение.

Сначала код: возьмите CodeGen.fs из моего репозитория facio. Если вы хотите увидеть, как я использовал эти функции на практике, посмотрите FSharpLex / Backend.Fslex.fs и FSharpYacc / Backend.Fsyacc.fs.

Итак, вот причины, по которым я использую генерацию кода как таковую:

  • Функции, которые я определил в модуле IndentedTextWriter, очень легкие и (IMO) простые в использовании. Если вы все же решите использовать мои функции в своем собственном коде, вы можете отказаться от атрибута [<RequireQualifiedAccess>] в модуле или изменить его на [<AutoOpen>], чтобы немного уменьшить шум.

  • Вместо реализации кучи кода для управления уровнем отступа и передачи строк отступа в базовый StringBuilder, я предпочитаю использовать _ 5_, поскольку он обрабатывает все это за вас, а также является экземпляром TextWriter, поэтому вы можете использовать его с такими функциями, как _ 7_ и _ 8_.

    Бонус: IndentedTextWriter включен в System.dll, и, поскольку вы почти наверняка будете на него ссылаться, вам даже не нужно добавлять дополнительную ссылку, чтобы использовать его!

  • IndentedTextWriter просто обертывает другой экземпляр TextWriter, поэтому код, который вы пишете с ним (например, используя мои функции в CodeGen.fs), не привязан к конкретной «цели». Другими словами, вы можете легко изменить его для записи в StringBuilder (с помощью StringWriter), файл на диске (с StreamWriter ) и так далее.

В вашем собственном коде вы можете сделать что-то вроде этого (просто чтобы дать вам представление):

let rec Block (formatters : Formatter list) (itw : IndentedTextWriter) =
    itw.WriteLine "{"
    IndentedTextWriter.indented itw <| fun itw ->
        formatters |> List.iter (fun fmtr -> fmtr itw)
    itw.WriteLine "}"

Еще одно замечание о вашем псевдокоде: поскольку ваше состояние форматирования изменяемо (как IndentedTextWriter в моем коде), действительно нет необходимости передавать его вне из ваших функций - то есть вам обычно нужно только для создания функций, которые принимают и возвращают значение состояния, когда эти состояния представлены неизменяемым объектом / значением.

Как ни странно, при передаче изменяемого писателя (как в нашем коде здесь) вам действительно нужен рабочий процесс «читателя» или какой-то его вариант. ExtCore содержит функции в стиле «читателя» для списков, массивов и т. д. в ExtCore.Control.Collections.Reader модуле, который вы могли бы использовать чтобы еще больше упростить ваш код.

person Jack P.    schedule 26.06.2013
comment
Спасибо за сообщение! Раньше я пытался создать немонадический синтаксический анализатор, имеющий изменяемое состояние, и обнаружил, что он плохо сочетается с поведением последовательностей (мой ввод был char seq). В конце концов, я отменил подход, когда моя отладка в неожиданные моменты выявляла неожиданные точки останова, и, конечно, тогда состояние уже изменилось. Итак, я подумал: мне нужны монады, потому что еще непредсказуемо, что такое поток управления f #. Вот почему на этот раз монады. Надеюсь сделать! дает предсказуемый поток управления. Думаю, я буду следовать вашему подходу, если здесь f # Guru не покажет, как это нужно делать :) - person user2173833; 27.06.2013
comment
Парсеры очень сложно писать, и это отличный пример того, как неизменяемое состояние может упростить ваш код. Генерация кода, которую я показал выше, является также монадической - при желании вы можете легко изменить мои функции для работы в рамках рабочего процесса. Такой способ генерации кода должен дать очень предсказуемый и простой для отладки результат. - person Jack P.; 27.06.2013
comment
Прошло несколько недель, и я снова обращаюсь к этому, пока не решенному вопросу, и чувствую необходимость добавить: монады призваны помочь в разработке мини-dsls. Основная цель такой dsl может заключаться в том, чтобы скрыть контекст (например, писателя) от нотации, чтобы он выглядел как фрагмент исходного кода C ++, а не писатель. WriteLine (...). Я до сих пор не знаю, как это осуществить. - person user2173833; 29.07.2013