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

В этой части речь в первую очередь пойдёт про 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
ссылка на оригинал статьи https://habr.com/ru/articles/799233/
Добавить комментарий