Big code. Learning to generate F# sources using Fantomas. Part 4. Extensions, generalizations and methods

In the last part we learned how to define our own types and modules. We have given all landmarks specific types and can now provide them with individual edge properties:

In this part we will primarily talk about Fluent API, but we will also delve into the topic of parameters in generics and functions. This is the last article that goes into detail AST. I will definitely return to codogen, but the subject of discussion will be the principles of generating the output code, and their implementation, due to their linearity, will be taken out of the equation.

Stage 3. Connections

Paths are valuable in themselves, so they should be included in the source code as an independent element. The amount of data is small, so I was content Map<int, int Set>. For each landmark identifier there are many of its neighbors within the selected graph. There are 3 such graphs, and they were defined manually in the module AttractionRelations V 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]
        ]
        ...

Generation result

Used by us AstHelpers And MyAstHelpers are written in type extensions, so the strength of the feature has been demonstrated and should be obvious. We will create three modules, each of which will define type extensions for types of specific attractions:

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

Having opened such a module, we can navigate through the corresponding graph:

open HaywayRelationExts

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

Generator

A significant part comes down to calling three times generateNet, which takes a vehicle name and a road graph. Some nodes may not support the generated transport, so points of interest need to be filtered and supplemented with connections:

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

I like that, from an AST point of view, type extension is a kind of type declaration. Same type <name> =but instead = given with. Such definitions cannot have a constructor. Excluding these two points, all other parameters are filled in according to the same scheme:

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
        )

Stage 4. Intersections

The code from Stage3.Generated.fs there is a flaw. Cannot be used in F# open inside a regular expression (such as a list or function). If you accidentally open two modules at a time, you can navigate through two columns at once. That is, if you plan to use more than one type of transport, then you will have to somehow isolate the modules from the third stage in separate contexts at the level of modules and namespaces. Without checking the graph, it is impossible to calculate such an error in the path, since the movement occurs between nodes that have no connection to the transport. To prevent this from happening, it is necessary to create another layer of types, where each type will be tied to a specific attraction and mode of transport:

Now we will have not just a type for an attraction, but also a type for each type of transport accessible from this point. I called this concept an intersection, which is a little strange when applied to airports, but well characterizes this node in the graph space. For all intersections, we introduce two types of labels in 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

It looks a little heavy and in some way contradicts the purity of the description of the subject area for which they use F#. However, it is not a domain specific application, but an API/DSL that is mostly machine generated. Here it is necessary to place emphasis differently, and in the case of F#, automatic type inference begins to play a huge role. I think the F# community would gain a lot by moving away from the habit of expressing complex infrastructure domains through pure generics and value types.

Generation result

We need to generate 2 modules for each type of transport. In the module _Crossroad the types of specific intersections of their connections will be determined:

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

Connections added via type ... with, but could be given directly in the types. However, this would require recursive references between types, which is provided either by a recursive module or a large bunch of type ... and ... and. It is better not to use both of them unless absolutely necessary. Moreover, if types and extensions are declared within the same module, then they are baked as one whole. The result is the same, but the writing is cleaner.

New types from _Crossroad they know about the old ones, but not vice versa. In the module _CrossroadAuto we will supplement the old types with properties that will lead from old types to new ones, from landmarks to their intersections:

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

_CrossroadAuto will automatically open when opened PhotoTour thanks to {<AutoOpen>].

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

During the test runs, in addition to criticism from below (“why is it so difficult”), there was also criticism from above (skip the paragraph if you don’t agree with them). Asked why 'attraction AttractionCrossroad was not developed into a dual generic AttractionCrossroad<'transport, 'attraction>. Under 'transport understood as a simple type marker of the form type Hayway = class end. In this case 'attraction HaywayCrossroad became just a pseudonym over AttractionCrossroad<Hayway, 'attraction>. If the remaining code is left unchanged, a collision occurs. HaywayCrossroad.СкульптураОлень has the property ДворецЗемледельцевA AttractionCrossroad<Hayway, Attractions.СкульптураОлень> no, although in meaning it seems like it should.

In F#, type extensions cannot be applied to a special case of a generic. That is to Option<int> you cannot add a property, but to Option<'a> Can. If you need a specific type, then you need to use System.Runtime.CompilerServices.ExtensionAttribute:

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

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

C# style adds only methods and only for the instance, which, moreover, will never become a direct part of the type, i.e., they will require opening the required namespace. I'm not particularly happy with this, but it's clearly not enough to always give preference to the F# style.

Generator

This time the generator is large, so we will disassemble it piece by piece. We again have three calls for each transport, and we again filter the data, as in the 3rd stage:

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 |}
    ]

Next, we create a constructor for the common ancestor of all intersections of this type of transport:

[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])
        ]
    ]

In real life, generic parameters are more difficult to learn than function parameters due to constructs, but in the world of AST this is not the case. And sometimes it seems to me that the algebraic types in AST give more hints about inlines than the IDE.

Next, for each attraction, we define an heir, which practically repeats stage 2 from the previous part, with the exception of the typification of the ancestor:

// Благодаря дактайпингу 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
        ]
    ]

Next, for each type we indicate its neighbors. This code almost repeats the 3rd stage, but it works with a different set of types, which are defined in the same module:

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
        ]
    ]

Here's the module _Crossroad ends and the module begins _CrossroadAutowhere the transition from landmarks to intersections is defined:

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
            ]
        ]
]

Finally, all this is built into the file:

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

Stage 5. Paths

After 4 stages, we can roam around the map, strictly following the graph. However, in practice, it is necessary not only to walk, but also to record the nodes passed. To do this, you will have to abandon movements between singletons and provide each node with a state. The general scheme will remain virtually unchanged. This time we will repeat the structure of the 4th stage, but complicate the details.

We will again need to define in Handmade.fs two root types for routes:

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

The new layer will be a superstructure on top of the previous one, and it will be possible to get into it from the corresponding intersection 'crossroad. To do this, it will need an initial state and a rule for its evolution. Moreover, for evolution, we feed it not a map of a new attraction, but the current node in the route. This is an obvious complication, but I duplicated it in this project to show an additional point of growth. IN updater an untyped version of the node is transmitted – UntypedAttractionRoutewhich is obtained through an implicit caste this. The point is that we can reverse cast and call a specific member:

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

Our nodes are identical in structure, but in my original design for the bike this is not the case. Firstly, there are several global categories in the form of common ancestors, which are used at the generation stage. Secondly, several dozen special locations have their own special type extensions. They could be tied to idbut this creates the risk of inconsistency between the left and right sides match. They can be expressed through templates, but then they cannot be used outside match. Type checking in such conditions seems to be the most stable and universal mechanics.

Generation result

We again need to generate 2 modules for each type of transport. In the module _Route the types of specific routes/nodes of their communication will be determined:

/// 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)

In the module _RouteAuto we will supplement the old types with methods that will lead to new types:

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

Generator

Let's filter the data again.

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 |}
    ]

Next comes the definition of common ancestor. Looking at it, it's a shame that F# can't automatically pick up generic constructs for types the same way it does for functions:

[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
            ]
        ]
    ]

A slightly less scary node declaration:

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
            ]
        ]
    ]

Next, we define extensions with transitions, where we encounter the definition of a method for the first time:

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 []
        ]
    ]

From an AST point of view, method and function definitions are almost the same in nature. The only difference is SynLeadingKeyword and outer wrapper:

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

A function becomes a function thanks to SynPat.LongIdent. This case requires a function/method name and a set of parameters. Above is the tupple version of the method. The parameters here are not typed, for these purposes it is necessary instead SynPat.Named transmit SynPat.Typed with the appropriate type, but I don't usually do that. If it does not concern patterns, then I prefer to use only names, and indicate the typing in the body of the method via SynExpr.Typed. This case corresponds to a record of the form a : stringwhich, being in parentheses, can be located at any point where you can place a. Initially, this move was dictated by the convenience of codogen, but then it penetrated into regular code. It turned out that refactoring code with typing at the point of use is much more convenient than with typing at the point of transmission.

The last one is a module with intersection expansion, where a method with parameters is defined.

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
                ]
            ]
        ]
]

Broken promises

The final project code is available Here.
The end result of our hike is the following code:

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

In fact, this code is different from what I showed at the beginning of the last part. But by now the reader should have developed a healthy sense of disdain for some API details. This is not a sacred cow, and it can be changed in any way depending on the tasks at hand. There is no difficulty in hiding a couple of stages behind a veil if all the necessary moves have been written down in the meta. If the information has not been lost, then all future steps turn into banal projections over the data.

In this same category I will include further expansions in the form of mandatory or optional data transfer at each step of the way. Both API options are implemented linearly and, in relation to the existing code, have only a large number of regular and generic parameters.

Save system

Participants in the Photo Tour crowd campaign have promo cards with additional attractions. Of course, no one supplemented the field, so the promo cards duplicate the numbers from the database. This means that there are sometimes 2 cards per number. If you enter this information into the game, then some of the logic in the generators will crumble, which is quite logical, since we are not just supplementing the data, but expanding the domain of definition.

This is a common problem, and in such cases the generators will produce invalid code for some time. In the case of non-terminal nodes, this can cause serious problems with navigation or even trivial reading. That is, you cannot fix the generator, because you have to look at the code that has died. Potentially, git should work here, with which you can roll back output files, but on complex graphs git cannot cope purely organizationally.

I usually automatically take snapshots of all involved files before and after running the script. This information is used to avoid running subsequent generators in vain in the absence of changes; there are even fewer runs than when targeting git. The same snapshots are used to rollback to previous states through direct overwriting of files. Here questions inevitably arise about application masks, snapshots not tied to the launch, do/undo etc., but this is all decided the way the user, that is, you, wants. The main thing is to think about the fact that both the states before and after the changes are valuable. Invalid ones are needed to understand what is wrong, and valid ones are needed to start from a checkpoint in RAM, and not from the beginning of the level.

I don't think that creating a harness for the generator system will be a serious task. This is a by-product that is written in idle moments. But still, this task must be at least visible on the horizon.

Fabulous.AST

In the second part of this article series, I lamented the fact that the F# community had not created an alternative FsAst after his death. It turned out that I was wrong. There is a package Fabulous.AST, which gives another projection over the syntax tree. It came out a little less than six months ago and stabilized in the summer of 2023, but all this passed by my social circle, even despite the corresponding search activities as part of the preparation of the cycle. I don't really follow Fabulous-om and still thought that they were based on string concatenation, as was in v1. I would have continued to think this way if not for the mention video V F# Weekly 2024.#2.

I don’t presume to evaluate the usefulness of a video or package outside of a specific task, but at least those who feel a lack of information should familiarize themselves with them. When I got into the AST area, we had three and a half videos and a couple of articles, so for two sentences from the video on Fabulous.AST 4-5 years ago I would have given a lot. I'll probably try to put it in sometime Fabulous.AST into codogen in some future article, if the context is right, but in a practical sense, I personally don’t need this package yet.

Bottom line

That's it for an introduction to codogen. Fantomas– it ends, and after that I will slowly (very, very slowly) move on to practice. I don't care much about well-standardized tasks, since the dotnet community quickly solves them at a completely tolerable level. Therefore, I will rather talk about general approaches, which will result in libraries with a very narrow focus.

In other words, if you are faced with a task that can be solved with the help of codogen, but which you have been postponing while waiting for some new information, then there is no point in further waiting. 20% of the knowledge that will give 80% of the result was laid out in these 4 articles. I admit the possibility of some adventures if you suddenly need to generate lambdas, builders or something else, but these are not insurmountable obstacles, since by this moment the nature of the discrepancies between the layman’s idea of ​​the code and its real AST should already be clear.

Author of the article @kleidemos


The UFO flew in and left a promotional code here for our blog readers:
— 15% on any VDS order (except for the Warm-up tariff) – HABRFIRSTVDS

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *