В прошлой части мы научились определять собственные типы и модули. Мы облекли все достопримечательности в конкретные типы и теперь можем снабдить их индивидуальными свойствами-ребрами:
В этой части речь в первую очередь пойдёт про 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