Pull to refresh
239.55
FirstVDS
Виртуальные серверы в ДЦ в Москве

Большой код. Учимся генерировать F#-исходники с помощью Fantomas. Часть 4. Расширения, обобщения и методы

Level of difficultyHard
Reading time17 min
Views722

В прошлой части мы научились определять собственные типы и модули. Мы облекли все достопримечательности в конкретные типы и теперь можем снабдить их индивидуальными свойствами-ребрами:

В этой части речь в первую очередь пойдёт про Fluent API, но мы также поковыряем тему параметров в дженериках и функциях. Это последняя статья, где детально разбирается AST. Я определённо будут возвращаться к кодогену, но предметом разбирательств будут принципы формирования выходного кода, а их реализация ввиду их линейности будет выноситься за скобки.

Этап 3. Связи

Пути ценны сами по себе, поэтому они должны быть в исходниках в качестве самостоятельного элемента. Объём данных небольшой, поэтому я довольствовался Map<int, int Set>. На каждый идентификатор достопримечательности приходится по множеству её соседей в рамках выбранного графа. Таких графов 3 штуки, и они были определены руками в модуле AttractionRelations в Handmade.fs:

module AttractionRelations =
    let (hayways : Map<int, int Set>), railroads, flights = 
        let hayways = [
            // Id карты * [Id соседей, при условии, что сосед.Id > this.Id]
            15, [16; 18; 20]
            16, [17; 18; 20]
            17, [19]
            18, [20]
            19, [20; 21]
            20, [21]
        ]
        ...

Результат генерации

Используемые нами AstHelpers и MyAstHelpers написаны на расширениях типов, так что сила фичи была продемонстрирована и должна быть очевидна. Мы создадим три модуля, в каждом из которых будут определены type extensions для типов конкретных достопримечательностей:

module HaywayRelationExts =
    type Attraction.ЗаповедникБасеги with
        member this.СкульптураОлень = Attraction.СкульптураОлень.instance

Открыв такой модуль, мы сможем перемещаться по соответствующему графу:

open HaywayRelationExts

Attractions.ЗаповедникБасеги.СкульптураОлень

Генератор

Значимая часть сводится к трёхкратному вызову generateNet, который принимает имя транспорта и граф дорог. Некоторые узлы могут не поддерживать генерируемый транспорт, поэтому достопримечательности надо отфильтровать и дополнить их связями:

let generateNet subtitle roads =
    let attractions = [
        for attraction in AttractionCards.all do
            match Map.tryFind attraction.Id roads with
            | None -> () 
            | Some roads -> {| attraction with Roads = roads |}
    ]

    [Messages.generated]
    |> SynModuleDecl.NestedModule.create (Modules._RelationExts subtitle) [
        for attraction in attractions do
            SynModuleDecl.Types.CreateByDefault [
                SynTypeDefn.``type <name> with =`` $"{Modules.attraction}.{Syntaxify.name attraction}" [
                    for otherId in attraction.Roads do
                        let otherName =
                            AttractionCards.all
                            // Верно, пока на каждую точку приходится лишь одна карта.
                            |> Seq.find ^ fun p -> p.Id = otherId
                            |> Syntaxify.name

                        $"{Modules.attraction}.{otherName}.{Types.Attraction.instance}"
                        |> Ident.parseSynExprLong 
                        |> SynMemberDefn.Member.``member this.<name> =`` otherName
                ]
            ]
    ]

// Как бы цикл...
generateNet Names.hayway AttractionRelations.hayways
generateNet Names.railroad AttractionRelations.railroads
generateNet Names.flight AttractionRelations.flights

Мне нравится, что, с точки зрения AST, расширение типов является разновидностью декларации типа. Тот же type <name> =, но вместо = дано with. Конструктора у таких определений быть не может. Исключая эти два пункта, все остальные параметры заполняются по той же схеме:

type SynTypeDefn with
    static member ``type <name> with =`` name members =
        SynTypeDefn.CreateByDefault(
            SynComponentInfo.CreateByDefault(Ident.parseLong name)
            , SynTypeDefnRepr.ObjectModel.CreateByDefault(
                // Вместо SynTypeDefnKind.Unspecified.
                SynTypeDefnKind.Augmentation.CreateByDefault()
            )
            , SynTypeDefnTrivia.CreateByDefault(
                SynTypeDefnLeadingKeyword.Type.CreateByDefault()
                // Вместо `equalsRange =`
                , withKeyword = Some Range.Zero
            )
            // Primary/implicit конструктор пропущен.
            , members = members
        )

Этап 4. Перекрёстки

У кода из Stage3.Generated.fs есть изъян. В F# нельзя использовать open внутри обычного выражения (например, списка или функции). Если случайно открыть два модуля за раз, то перемещаться можно будет сразу по двум графам. То есть если предполагается использовать более одного вида транспорта, то придётся как-то изолировать модули из третьего этапа в отдельных контекстах на уровне модулей и пространств имён. Не сверяясь с графом, вычислить такую ошибку в пути нельзя, так как перемещение происходит между узлами, не имеющими привязки к транспорту. Чтобы этого не происходило, необходимо создать ещё один слой типов, где каждый тип будет привязан к конкретной достопримечательности и виду транспорта:

Теперь у нас будет не просто по типу на достопримечательность, но ещё и по типу на каждый вид транспорта, доступный из этой точки. Данное понятие я назвал перекрёстком, что немного странно применительно к аэропортам, но хорошо характеризует данный узел в пространстве графа. Для всех перекрёстков введём два типа-метки в Stage4.Handmade.fs:

// Чтобы у них было что-нибудь общее.
type UntypedAttractionCrossroad (attraction : Attraction) =
    member this.Card = attraction.Card
    member this.Attraction = attraction

type 'attraction AttractionCrossroad when 'attraction :> Attraction (attraction : 'attraction) =
    inherit UntypedAttractionCrossroad(attraction)
    // Скроет свойство предка, но не переопределит его.
    // Позволит вернуться к конкретному 'attraction
    member this.Attraction = attraction

Выглядит тяжеловато и в каком-то роде противоречит чистоте описания предметной области, ради которой на F# и перекатываются. Однако это не предметная область, а API / DSL, которое по большей части генерируется машиной. Здесь необходимо иначе расставлять акценты, причём в случае F# огромную роль начинает играть автоматический вывод типов. Думаю, что F#-сообщество многое бы выиграло, если бы отошло от привычки выражать сложные инфраструктурные домены через чистые дженерики и value-типы.

Результат генерации

Нам надо для каждого вида транспорта сгенерировать по 2 модуля. В модуле _Crossroad будут определены типы конкретных перекрёстков их связи:

module HaywayCrossroad =
    // Общий тип.
    type 'attraction HaywayCrossroad when 'attraction :> Attraction (attraction) =
        inherit AttractionCrossroad<'attraction>(attraction)

    // Конкретные перекрёстки.
    type СкульптураОлень private () =
        inherit HaywayCrossroad<Attraction.СкульптураОлень>(Attractions.СкульптураОлень)
        static member val instance = СкульптураОлень()

    type ДворецЗемледельцев private () =
        inherit HaywayCrossroad<Attraction.ДворецЗемледельцев>(Attractions.ДворецЗемледельцев)
        static member val instance = ДворецЗемледельцев()

    // Связи между перекрёстками.
    type СкульптураОлень with
        // Это ссылка на **перекрёсток** другой достопримечательности.
        member this.ДворецЗемледельцев = ДворецЗемледельцев.instance

Связи добавлены через type ... with, но могли бы быть даны непосредственно в типах. Однако это потребовало бы рекурсивных ссылок между типами, что обеспечивается либо рекурсивным модулем, либо большой связкой из type ... and ... and. И то, и другое лучше не применять без острой необходимости. При этом, если типы и расширения объявлены в рамках одного и того же модуля, то они запекаются как одно целое. Результат тот же, но написано чище.

Новые типы из _Crossroad знают о старых, но не наоборот. В модуле _CrossroadAuto мы дополним старые типы свойствами, которые будут вести из старых типов к новым, от достопримечательностей к их перекрёсткам:

[<AutoOpen>]
module HaywayCrossroadAuto =
    type Attraction.ДворецЗемледельцев with
        member this.ViaHayway = HaywayCrossroad.ДворецЗемледельцев.instance

_CrossroadAuto автоматически откроется при открытии PhotoTour благодаря {<AutoOpen>].

Attractions
    .ДворецЗемледельцев
    // Доступно из-за [<AutoOpen>] на HaywayCrossroadAuto.
    .ViaHayway
    // Свойство вшито в HaywayCrossroad.ДворецЗемледельцев и доступно везде
    // , независимо от открытых модулей/пространств.
    .СкульптураОлень
    // Свойство AttractionCrossroad<'attraction>
    .Attraction
// : Attractions.СкульптураОлень
Обобщение по транспорту

На тестовых прогонах кроме критики снизу (»почему так сложно») была и критика сверху (пропускайте параграф, если не с ними). Спрашивалось, почему 'attraction AttractionCrossroad не был развит до двойного дженерика AttractionCrossroad<'transport, 'attraction>. Под 'transport понимался простой тип маркер вида type Hayway = class end. В этом случае 'attraction HaywayCrossroad становился всего лишь псевдонимом над AttractionCrossroad<Hayway, 'attraction>. Если оставшийся код оставить без изменений, то возникает коллизия. HaywayCrossroad.СкульптураОлень имеет свойство ДворецЗемледельцев, а AttractionCrossroad<Hayway, Attractions.СкульптураОлень> нет, хотя по смыслу вроде как должно.

В F# расширения типов нельзя применять к частному случаю дженерика. То есть к Option<int> добавить свойство нельзя, а к Option<'a> можно. Если нужен конкретный тип, то надо использовать System.Runtime.CompilerServices.ExtensionAttribute:

[<Extension>]
type Exts =
    [<Extension>]
    static member ДворецЗемледельцев' (_ : HaywayCrossroad.СкульптураОлень) = 
        HaywayCrossroad.ДворецЗемледельцев.instance

Attractions.СкульптураОлень.ViaHayway.ДворецЗемледельцев'().ЖигулёвскиеГоры

C#-стиль добавляет только методы и только у экземпляра, которые к тому же никогда не станут непосредственной частью типа, т. е. будут требовать открыть нужное пространство имён. Меня это не особо устраивает, но этого явно недостаточно, чтобы всегда отдавать предпочтение F#-стилю.

Генератор

В этот раз генератор большой, так что разбирать будем по частям. У нас снова три вызова по каждому транспорту, и мы снова фильтруем данные, как на 3-ем этапе:

let generateNet subtitle roads = [
    let attractions = [
        for attraction in AttractionCards.all do
            match Map.tryFind attraction.Id roads with
            | None -> () 
            | Some roads -> {| attraction with Roads = roads |}
    ]

Дальше формируем конструктор для общего предка всех перекрёстков данного вида транспорта:

[Messages.generated]
|> SynModuleDecl.NestedModule.create (Modules._Crossroad subtitle) [
    SynModuleDecl.Types.CreateByDefault[
        let attraction =
            let literal = "attraction"
            {|
                AsSynType = Ident.parseSynType $"'{literal}"
                AsString = literal
            |}

        let info = 
            SynComponentInfo.CreateByDefault(
                Ident.parseLong ^ Types._Crossroad subtitle
                // Дженерик параметры в "`a TypeName" форме.
                , typeParams = Some ^ SynTyparDecls.SinglePrefix.create attraction.AsString
                , constraints = [
                    // Список ограничений, в данном случае "`attaction :> Attraction"
                    Ident.parseSynType Types.attraction
                    |> SynTypeConstraint.WhereTyparSubtypeOfType.create attraction.AsString
                ]
            )
        let ctor = 
            SynSimplePats.SimplePats.simple [attraction.AsString]
            |> SynMemberDefn.ImplicitCtor.create
        SynTypeDefn.create info ctor [
            // Передаём входной аргумент.
            [Ident.parseSynExprLong attraction.AsString]
            |> SynMemberDefn.ImplicitInherit.create
                // Постфиксная версия дженерика синтаксически недопустима.
                // Типизируем предка дженерик-параметром.
                (SynType.App.classicGeneric Types.attractionCrossroad [attraction.AsSynType])
        ]
    ]

В жизни дженерик-параметры из-за констрейтов усваиваются труднее, чем параметры функций, но в мире AST это не так. И иногда мне кажется, что алгебраические типы в AST дают по инлайнам больше подсказок, чем IDE.

Далее для каждой достопримечательности определяем по наследнику, что практически повторяет этап 2 из прошлой части, за исключением типизации предка:

// Благодаря дактайпингу attractionName удаётся извлечь даже из анонимного рекорда.
for Syntaxify.Name attractionName in attractions do
    SynModuleDecl.Types.CreateByDefault[
        SynTypeDefn.``type <name> private () =`` attractionName [
            [Ident.parseSynExprLong $"{Modules.attractions}.{attractionName}"]
            |> SynMemberDefn.ImplicitInherit.create (
                SynType.App.classicGeneric (Types._Crossroad subtitle) [
                    Ident.parseSynType ^ $"{Modules.attraction}.{attractionName}"
                ]
            )

            Ident.parseSynExprLong attractionName
            |> SynExpr.app SynExpr.Const.unit
            |> SynMemberDefn.Member.staticMemberVal Types.Crossroad.instance
        ]
    ]

Далее для каждого типа указываем его соседей. Этот код почти повторяет 3-й этап, но он работает с другим набором типов, который определён в этом же модуле:

for attraction in attractions do
    SynModuleDecl.Types.CreateByDefault [
        SynTypeDefn.``type <name> with =`` (Syntaxify.name attraction) [
            for otherId in attraction.Roads do
                let otherName = 
                    AttractionCards.all
                    |> Seq.find ^ fun p -> p.Id = otherId
                    |> Syntaxify.name

                Ident.parseSynExprLong $"{otherName}.{Types.Crossroad.instance}"
                |> SynMemberDefn.Member.``member this.<name> =`` ^ otherName
        ]
    ]

Здесь модуль _Crossroad заканчивается, и начинается модуль _CrossroadAuto, где определён переход из достопримечательностей в перекрёстки:

SynModuleDecl.NestedModule.createAutoOpen (Modules._CrossroadAuto subtitle) [
    for Syntaxify.Name attractionName in attractions do
        SynModuleDecl.Types.CreateByDefault [
            SynTypeDefn.``type <name> with =`` $"{Modules.attraction}.{attractionName}" [
                $"{Modules._Crossroad subtitle}.{attractionName}.{Types.Crossroad.instance}"
                |> Ident.parseSynExprLong
                |> SynMemberDefn.Member.``member this.<name> =`` ^ Types.Attraction.via_ subtitle
            ]
        ]
]

Наконец всё это встраивается в файл:

yield! generateNet Names.hayway AttractionRelations.hayways
yield! generateNet Names.railroad AttractionRelations.railroads
yield! generateNet Names.flight AttractionRelations.flights

Этап 5. Пути

После 4 этапов мы можем шастать по карте, точно соблюдая граф. Однако на практике необходимо не просто ходить, но ещё и записывать пройденные ноды. Для этого придётся отказаться от перемещений между синглтонами и снабдить каждый узел состоянием. Общая схема практически не изменится. В этот раз мы повторим структуру 4-го этапа, но усложним детали.

Нам снова потребуется определить в Handmade.fs два корневых типа для маршрутов:

type UntypedAttractionRoute (crossroad : UntypedAttractionCrossroad) =
    member this.Card = crossroad.Card
    member this.Crossroad = crossroad

type AttractionRoute<'crossroad, 'attraction, 'state> 
        when 'crossroad :> 'attraction AttractionCrossroad 
        and 'attraction :> Attraction 
        (crossroad : 'crossroad, initialState : 'state, updater : 'state -> UntypedAttractionRoute -> 'state) =
    inherit UntypedAttractionRoute(crossroad)

    // Скроет свойство предка, но не переопределит его.
    member this.Crossroad = crossroad
    member this.Updater = updater
    // Вычисление в момент вызова.
    member this.State = updater initialState this

    member this.StopRoute () =
        this.Crossroad.Attraction, this.State

Новый слой будет надстройкой над предыдущим, и в него можно будет попасть из соответствующего перекрёстка 'crossroad. Для этого ему потребуется начальное состояние и правило его эволюции. Причём для эволюции мы скармливаем ему не карту новой достопримечательности, а текущий узел в маршруте. Это явное усложнение, но я его продублировал в этом проекте, чтобы показать дополнительную точку роста. В updater передаётся нетипизированный вариант ноды — UntypedAttractionRoute, который получается через неявный каст this. Суть в том, что мы можем произвести обратный каст и вызвать у него специфичный член:

match node with
| :? ConcreteRouteType as concreteRoute -> concreteRoute.SpecialMember
| ... -> ...

Наши узлы идентичны по своей структуре, но в моём исходном проекте для велика это не так. Во-первых, там есть несколько глобальных категорий в виде общих предков, которые используются ещё на этапе генерации. Во-вторых, несколько десятков особых локаций имеют свои особые type extensions. Их можно было привязать к id, но это порождает риск несоответствия левой и правой части match. Их можно выразить через шаблоны, но тогда их нельзя использовать за пределами match. Проверка типа в таких условиях выглядит наиболее стабильной и универсальной механикой.

Результат генерации

Нам снова нужно для каждого вида транспорта сгенерировать по 2 модуля. В модуле _Route будут определены типы конкретных маршрутов/узлов их связи:

/// Generated.
module HaywayRoute =
    // Общий предок.
    type HaywayRoute<'crossroad, 'attraction, 'state>
        when 'crossroad :> 'attraction AttractionCrossroad and 'attraction :> Attraction
        (crossroad, initialState, updater) =
        inherit AttractionRoute<'crossroad, 'attraction, 'state>(crossroad, initialState, updater)

    // Конкретные узлы.
    type 'state НабережнаяБрюгге(initialState, updater) =
        inherit
            HaywayRoute<HaywayCrossroad.НабережнаяБрюгге, Attraction.НабережнаяБрюгге, 'state>(
                HaywayCrossroad.НабережнаяБрюгге.instance,
                initialState,
                updater
            )

    // Связи между узлами.
    type 'state НабережнаяБрюгге with
        member this.ЗаповедникБасеги() =
            ЗаповедникБасеги(this.State, this.Updater)

В модуле _RouteAuto мы дополним старые типы методами, которые будут вести к новым типам:

[<AutoOpen>]
module HaywayRouteAuto =
    type HaywayCrossroad.ЗаповедникБасеги with
        member this.StartRoute(initialState, updater) =
            HaywayRoute.ЗаповедникБасеги(initialState, updater)

Генератор

Снова фильтруем данные.

let synExpr = Ident.parseSynExprLong

let generateNet subtitle roads = [
    let attractions = [
        for attraction in AttractionCards.all do
            match Map.tryFind attraction.Id roads with
            | None -> () 
            | Some roads -> {| attraction with Roads = roads |}
    ]

Далее идёт определение общего предка. Глядя на него, становится жалко, что F# не может автоматически подхватывать констрейты дженериков для типов так же, как он это делает для функций:

[Messages.generated]
|> SynModuleDecl.NestedModule.create (Modules._Route subtitle) [
    SynModuleDecl.Types.CreateByDefault[
        let crossroad, attraction, state =
            let generic literal =
                {|
                    AsString = literal
                    AsSynType = Ident.parseSynType $"'{literal}"
                |}
            generic "crossroad"
            , generic "attraction"
            , generic "state"

        let info =
            SynComponentInfo.CreateByDefault(
                Ident.parseLong ^ Types._Route subtitle
                , typeParams = (
                    SynTyparDecls.PostfixList.create [
                        crossroad.AsString
                        attraction.AsString
                        state.AsString
                    ]
                    |> Some
                )
                , constraints = [
                    SynType.App.postfix Types.attractionCrossroad attraction.AsSynType
                    |> SynTypeConstraint.WhereTyparSubtypeOfType.create crossroad.AsString

                    Ident.parseSynType Types.attraction
                    |> SynTypeConstraint.WhereTyparSubtypeOfType.create attraction.AsString 
                ]
            )
        let ctor =
            SynMemberDefn.ImplicitCtor.create ^ SynSimplePats.SimplePats.simple [
                crossroad.AsString
                Types.Route.initalState
                Types.Route.updater
            ]

        SynTypeDefn.create info ctor [
            SynMemberDefn.ImplicitInherit.create (
                SynType.App.classicGeneric Types.attractionRoute [
                    crossroad.AsSynType
                    attraction.AsSynType
                    state.AsSynType
                ]
            ) [
                synExpr crossroad.AsString
                synExpr Types.Route.initalState
                synExpr Types.Route.updater
            ]
        ]
    ]

Чуть менее страшная декларация узла:

for Syntaxify.Name attractionName in attractions do
    SynModuleDecl.Types.CreateByDefault[
        let info =
            SynComponentInfo.CreateByDefault(
                Ident.parseLong attractionName
                , typeParams = Some ^ SynTyparDecls.SinglePrefix.create "state"
            )
        let ctor =
            SynMemberDefn.ImplicitCtor.create ^ SynSimplePats.SimplePats.simple [
                Types.Route.initalState
                Types.Route.updater
            ]
        SynTypeDefn.create info ctor [
            SynMemberDefn.ImplicitInherit.create (
                SynType.App.classicGeneric (Types._Route subtitle) [
                    Ident.parseSynType $"{Modules._Crossroad subtitle}.{attractionName}"
                    Ident.parseSynType $"{Modules.attraction}.{attractionName}"
                    Ident.parseSynType "'state"
                ]
            ) [
                synExpr $"{Modules._Crossroad subtitle}.{attractionName}.{Types.Crossroad.instance}"
                synExpr Types.Route.initalState
                synExpr Types.Route.updater
            ]
        ]
    ]

Далее определяем расширения с переходами, где первый раз сталкиваемся с определением метода:

for attraction in attractions do
    SynModuleDecl.Types.CreateByDefault [
        SynTypeDefn.``type '<arg> <name> with =`` "state" (Syntaxify.name attraction) [
            for otherId in attraction.Roads do
                let otherName =
                    AttractionCards.all
                    |> Seq.find ^ fun p -> p.Id = otherId
                    |> Syntaxify.name

                SynExpr.tuple [
                    synExpr $"this.{Types.Route.State}"
                    synExpr $"this.{Types.Route.Updater}"
                ]
                |> SynExpr.paren
                |> SynExpr.app <| synExpr otherName
                |> SynMemberDefn.Member.``member this.<name> (<args>)`` otherName []
        ]
    ]

С точки зрения AST, определения методов и функций имеют почти одинаковую природу. Различаются лишь SynLeadingKeyword и внешняя обёртка:

type SynMemberDefn.Member with
    static member ``member this.<name> (<args>)`` name args body =
        SynBinding.create 
            (SynLeadingKeyword.Member.CreateByDefault())
            (SynPat.LongIdent.CreateByDefault(
                Ident.parseSynLong $"this.%s{name}"
                , SynArgPats.Pats.CreateByDefault [
                    // Если выдавать элементы здесь, то получится каррированная версия.
                    match args with
                    | [] ->
                        // Вместо пустого тупла.
                        SynPat.Const.CreateByDefault SynConst.Unit
                    | args ->
                        SynPat.Tuple.CreateByDefault(
                            elementPats = [
                                // Если выдавать элементы здесь, то они попадут в тупл.
                                for arg in args do
                                    SynPat.Named.create arg
                            ]
                            , commaRanges = 
                                List.map (fun _ -> Range.Zero) args.Tail
                        )
                        // Тупл надо завернуть в скобки руками.
                        |> SynPat.Paren.CreateByDefault
                ]
            ))
            body
        |> SynMemberDefn.Member.CreateByDefault

Функция становится функцией благодаря SynPat.LongIdent. Этот кейс требует имя функции/метода и набор параметров. Выше представлена тупл-версия метода. Параметры здесь не типизированы, для этих целей надо вместо SynPat.Named передавать SynPat.Typed с соответствующим типом, но обычно я так не делаю. Если дело не касается паттернов, то я предпочитаю использовать только имена, а типизацию указывать в теле метода через SynExpr.Typed. Этот кейс соответствует записи вида a : string, которая, будучи в скобках, может находиться в любой точке, куда можно поместить a. Изначально этот ход был продиктован удобством кодогена, но потом он проник и в обычный код. Оказалось, что рефакторить код с типизацией по месту использования гораздо удобнее, чем с типизацией по месту передачи.

Последним идёт модуль с расширением перекрёстков, где определяется метод с параметрами.

SynModuleDecl.NestedModule.createAutoOpen (Modules._RouteAuto subtitle) [
    for Syntaxify.Name attractionName in attractions do
        SynModuleDecl.Types.CreateByDefault [
            SynTypeDefn.``type <name> with =`` $"{Modules._Crossroad subtitle}.{attractionName}" [
                synExpr $"{Modules._Route subtitle}.{attractionName}"
                |> SynExpr.app ^ SynExpr.paren ^ SynExpr.tuple [
                    synExpr Types.Route.initalState
                    synExpr Types.Route.updater
                ]
                |> SynMemberDefn.Member.``member this.<name> (<args>)`` Types.Route.startRoute [
                    Types.Route.initalState
                    Types.Route.updater
                ]
            ]
        ]
]

Нарушенные обещания

Итоговый код проекта доступен здесь.
Конечным результатом нашего похода является следующий код:

let attraction, path = 
    Attractions.ДворецЗемледельцев.ViaHayway
        .StartRoute(PersistentVector.empty, fun state node -> state.Conj node.Crossroad.Card)
        .ЖигулёвскиеГоры()
        .ХребетЯлангас()
        .ДворецЗемледельцев()
        .ЖигулёвскиеГоры()
        .StopRoute()
    
attraction
|> Expect.equal "" Attractions.ЖигулёвскиеГоры

let expectedPath = PersistentVector.ofSeq [
    AttractionCards.ДворецЗемледельцев
    AttractionCards.ЖигулёвскиеГоры
    AttractionCards.ХребетЯлангас
    AttractionCards.ДворецЗемледельцев
    AttractionCards.ЖигулёвскиеГоры
]
path
|> Expect.equal "" expectedPath

На самом деле этот код отличается от того, что я показывал в начале прошлой части. Но к текущему моменту у читателя должно было сформироваться здоровое чувство пренебрежения к некоторым деталям API. Это не священная корова, и её можно изменять произвольным образом в зависимости от поставленных задач. Нет никаких сложностей в том, чтобы спрятать пару этапов за завесой, если все необходимые ходы были записаны в мете. Если информация не была потеряна, то все будущие шаги превращаются в банальные проекции над данными.

В эту же категорию я отнесу дальнейшие расширения в виде обязательной или опциональной передачи данных на каждом шаге пути. Оба варианта API реализуются линейно и по отношению к имеющемуся коду обладают лишь большим числом обычных и дженерик параметров.

Система сохранений

У участников крауд-компании Фототура есть промо-карты с дополнительными достопримечательностями. Разумеется, поле никто не дополнял, поэтому промо-карты дублируют номера из базы. Это означает, что на один номер иногда приходится по 2 карты. Если вводить эту информацию в игру, то часть логики в генераторах посыпется, что вполне логично, так как мы не просто дополняем данные, а растягиваем область определения.

Это частая проблема, и в подобных случаях некоторое время генераторы будут давать невалидный код. В случае нетерминальных нод это может вызывать серьёзные проблемы с навигацией или даже банальным чтением. То есть вы не можете исправить генератор, потому что вам надо смотреть в код, который умер. Потенциально здесь должен работать git, с помощью которого можно откатывать выходные файлы, но на сложных графах git не справляется чисто организационно.

Обычно я автоматически снимаю снапшоты всех задействованных файлов до и после запуска скрипта. Эта информация используется, чтобы в отсутствие изменений не гонять последующие генераторы понапрасну, запусков получается ещё меньше, чем при ориентации на git. Эти же снапшоты применяются для отката к прошлым состояниям через прямую перезапись файлов. Тут неизбежно возникают вопросы о масках применения, снапшотах непривязанных к запуску, do/undo и т. д., но это всё решается так, как захочется пользователю, то есть вам. Главное, стоит задуматься о том, что ценность представляют как состояния до изменений, так и после. Невалидные нужны, чтобы понять, что не так, а валидные, чтобы стартовать с чекпоинта в оперативной памяти, а не с начала уровня.

Я не думаю, что создание обвязки для системы генераторов станет серьёзной задачей. Это побочный продукт, который пишется в минуты простоя. Но всё же эту задачу надо хотя бы различать на горизонте.

Fabulous.AST

Во второй части данного цикла статей я сетовал на то, что F#-сообщество не создало альтернативы FsAst после его смерти. Оказалось, что я был не прав. Есть пакет Fabulous.AST, который даёт ещё одну проекцию над синтаксическим деревом. Вышел он чуть меньше года назад и стабилизировался летом 2023, но всё это прошло мимо моего круга общения, даже несмотря на соответствующие поисковые мероприятия в рамках подготовки цикла. Я не особо слежу за Fabulous-ом и до сих пор думал, что они сидят на конкатенации строк, как было в v1. Я бы продолжал думать так и дальше, если бы не упоминание видео в F# Weekly 2024.#2.

Не берусь оценивать полезность видео или пакета вне конкретной задачи, но как минимум с ними стоит ознакомиться тем, кто ощущает нехватку информации. Когда я влезал в область AST, у нас было три с половиной видео и парочка статей, так что за два предложения из видео по Fabulous.AST 4-5 лет назад я бы очень многое отдал. Я, возможно, попробую когда-нибудь вставить Fabulous.AST в кодоген в какой-нибудь будущей статье, если выпадет удачный контекст, но в практическом смысле данный пакет лично мне пока не нужен.

Итог

На этом введение в кодоген на Fantomas-е заканчивается, и после этого я буду не спеша (очень-очень не спеша) переходить к практике. Меня мало заботят хорошо стандартизованные задачи, так как dotnet-сообщество их оперативно решает на вполне терпимом уровне. Поэтому я, скорее, буду говорить об общих подходах, в результате которых будут получаться библиотеки очень узкой направленности.

Иными словами, если перед вами стоит задача, которую можно решить при помощи кодогена, но которую вы откладывали в ожидании какой-то новой информации, то смысла в дальнейшем ожидании больше нет. 20% знаний, которые дадут 80% результата, были выложены в этих 4 статьях. Я допускаю возможность некоторых приключений, если вдруг вам потребуется генерить лямбды, билдеры или ещё что-нибудь этакое, но это не непреодолимые препятствия, так как к этому моменту уже должна быть ясна природа расхождений между обывательским представлением о коде и его реальным AST.

Автор статьи @kleidemos


НЛО прилетело и оставило здесь промокод для читателей нашего блога:
— 15% на заказ любого VDS (кроме тарифа Прогрев) — HABRFIRSTVDS

Tags:
Hubs:
Total votes 11: ↑9 and ↓2+7
Comments0

Articles

Information

Website
firstvds.ru
Registered
Founded
Employees
51–100 employees
Location
Россия
Representative
FirstJohn