Здравствуйте, vaskir, Вы писали:
V>Написал. На первый взляд ничего страшного не видишь?
В простых случаях должно работать.
Проблемы могут возникнуть в двух случаях:
1. Если пользователь объявил рукописный ToString(), будет сообщение об ошибке. Правильнее было бы проверять нет ли рукописного ToString() с генерацией ToString() только если его нет.
2. Более правильно было бы не печатать все поля текущего объекта, а печатать псевда-параметры конструкторов помеченных как RecordCtorAttribute. У таких конструкторов параметры размечаются атрибутами MappedMemberAttribute, где указывается реальное имя параметра. Посмотри через дкомпилятор как выглядит вариантный тип (это касается не только вариантных типов но и других видов записей). MappedMemberAttribute задает соответствие поля записи и параметра конструктора. Реализация использующая эти атрибуты будет более корректной. Но и твоя будет работать для 98% вариантных типов.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
[MacroUsage(MacroPhase.WithTypedMembers, MacroTargets.Class, Inherited = false, AllowMultiple = false)]
public macro Show(ty: TypeBuilder) {
ShowImpl.Transform (ty)
}
module ShowImpl {
GetRecordCtorParams(t: TypeBuilder): option[list[string]] {
def recordCtor =
t.GetConstructors(BindingFlags.Public)
.FirstOrDefault(x => x.IsCustomAttributeDefined("Nemerle.Internal.RecordCtorAttribute"))
.OfNull();
recordCtor.Map(x => {
x.GetParameters()
.Map(x => x.CustomAttributes)
.Flatten()
.Choose(x =>
match (x) {
| PExpr.Call(_, pars) =>
Some(pars.Choose(par =>
match (par) {
| PExpr.Literal(Literal.String(name)) => Some(name)
| _ => None()
}))
| _ => None()
})
.Flatten()
})
}
public Transform(ty: TypeBuilder): void {
match (ty) {
| _ when ty.IsDelegate with n = "delegates"
| _ when ty.IsEnum with n = "enums"
| _ when ty.IsInterface with n = "interfaces"
| _ when ty.IsModule with n = "modules" =>
Message.Error(ty.Location, $"The Show macro does not support $n.");
| _ => {
ty.GetVariantOptions().Iter(opt => {
unless (opt.GetDirectMembers().Any(x => x.Name == "ToString"))
{
match (GetRecordCtorParams(opt)) {
| None => ()
| Some(pars) => {
def fieldsExpr: PExpr = pars.Fold(<[]>, (f, acc) => {
match (acc) {
| <[]> => <[ $(f: usesite).ToString() ]>
| _ => <[ $acc + ", " + $(f: usesite).ToString() ]>
}
});
def body: PExpr =
match (fieldsExpr) {
| <[]> => <[ $(opt.Name) ]>
| _ => <[ $(opt.Name) + "(" + $fieldsExpr + ")" ]>
};
opt.Define(<[decl:
public override ToString(): string {
$body
}
]>);
}
}
}
});
}
}
}
}
Два вопроса:
Оптимально ли так определять наличие атрибута (по строковому имени)?
То же самое для определения, есть ли неунаследованный переопределенный ToString (opt.GetDirectMembers().Any(x => x.Name == "ToString"))
Получение имен аргументов через мэнчинг PExpr — ОК? Как-то низкоуровнево выглядит, учитываю, что у нас по идее уже есть типизированный метод.
Здравствуйте, vaskir, Вы писали:
V>Оптимально ли так определять наличие атрибута (по строковому имени)?
Нормально. Другие пути сложнее.
V>То же самое для определения, есть ли неунаследованный переопределенный ToString (opt.GetDirectMembers().Any(x => x.Name == "ToString"))
Тут ты забыл проверить отсутствие аргументов. Твой макрос может ложно сработать на ToString с параметрами, а ToString-а без параметров может и не быть.
V>Получение имен аргументов через мэнчинг PExpr — ОК? Как-то низкоуровнево выглядит, учитываю, что у нас по идее уже есть типизированный метод.
ОК, но есть более простые подходы. Ниже я привел отрефакторнный и поправленный вариант твоего макроса с комментариями. Надеюсь они будут полезны.
using Nemerle;
using Nemerle.Collections;
using Nemerle.Compiler;
using Nemerle.Compiler.Parsetree;
using System.Linq;
[MacroUsage(MacroPhase.WithTypedMembers, MacroTargets.Class, Inherited = false, AllowMultiple = false)]
public macro Show(ty: TypeBuilder) {
ShowImpl.Transform(Macros.ImplicitCTX(), ty)
}
module ShowImpl
{
public Transform(typer : Typer, ty: TypeBuilder): void {
// Функция мелкая и не имеет смысла за пределами данного метода. Делаем ее локальной!def getRecordCtorParams(tb: TypeBuilder): option[list[string]]
{
Macros.DefineCTX(typer); // нужно для "ttype:"
// Просим компилятор отдать нам FixedType для известного типа и берем у него TypeInfo.def mappedMemberAttribute = <[ ttype: Nemerle.Internal.MappedMemberAttribute; ]>.TypeInfo;
// Если в лямбде много букв, имеет смысл вынести ее в локальную функцию. Так читабельнее выходит.def extractFieldName(param)
{
// FindAttributeWithArgs находит нужный атрибут по TypeInfo и отдает результат в виде
// Some(имя_атрибута, [параметр_атрибута1, параметр_атрибута2, ...]). match (param.AttributesAndModifiers.FindAttributeWithArgs(mappedMemberAttribute, tb.GlobalEnv))
{
// "размтачиваем" его одним залпом. У нас должен быть ровно один параметр типа "строковый литерал".
| Some((_, [<[ $(name : string) ]>])) => name
| _ => assert(false) // Параметр должен быть железно! Если мы попали сюда, то что-то пошло не так :)
}
}
tb.GetConstructors(BindingFlags.Public)
// Find() возвращает результат завернутый в option[T]. Если уж ты решил поупражняться в ФП, то так удобнее буедт :).
.Find(method => method.IsCustomAttributeDefined("Nemerle.Internal.RecordCtorAttribute"))
// Ну, тут ты понял. Для тех кто не понял поясню, что первый Map() вызывается на option[Е].
// Если в option есть значение, выполняется переданная функция и результат заворачивается в Some().
// Иначе возвращается None().
.Map(ctor => ctor.GetParameters().Map(extractFieldName))
}
match (ty)
{
| _ when ty.IsDelegate with n = "delegates"
| _ when ty.IsEnum with n = "enums"
| _ when ty.IsInterface with n = "interfaces"
| _ when ty.IsModule with n = "modules" =>
Message.Error(ty.Location, $"The Show macro does not support $n.");
| _ =>
// Надо проверять наличие только ToString без параметров. Луже запихнуть эту проверку в локальную функцию, чтбы код был понятнее.def existsHandmadeToString(opt) { opt.GetDirectMembers().OfType.[IMethod]().Any(x => x.Name == "ToString" && x.Header.Parameters.IsEmpty) }
// Nemerle нормальный императивный язык. Iter() не нужны! Используем foreach :).
// К тому же он в Nemerle крутой! Он паттарн-матчинг поддерживает. В данном случае используем его
// гуард для фильтрации типов в которых метод ToString написан вручную.foreach (opt when !existsHandmadeToString(opt) in ty.GetVariantOptions())
{
// Если match планируется обрабатывать только один случай паттерн-матчинга, то можно использовать макру when или ifwhen (getRecordCtorParams(opt) is Some(pars))
{
def fieldsExpr = pars.Fold(<[]>, (f, acc) => {
match (acc) {
| <[]> => <[ $(f: usesite).ToString() ]>
| _ => <[ $acc + ", " + $(f: usesite).ToString() ]>
}
});
def body =
match (fieldsExpr) {
| <[]> => <[ $(opt.Name) ]>
| _ => <[ $(opt.Name) + "(" + $fieldsExpr + ")" ]>
};
opt.Define(<[decl:
public override ToString(): string {
$body
}
]>);
}
}
}
}
}
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
VD> Ниже я привел отрефакторнный и поправленный вариант твоего макроса с комментариями. Надеюсь они будут полезны.
Спасибо! Узнал много нового.
Да, возникает желание использовать Option.map/bind/fill/ofNull/... и функции типа
choose: ('a -> 'b option) -> 'a list -> 'b list (map 'a в 'b option и фильтрация != None за один проход). Не нашел ничего похожего в Nemerle (MapFiltered сначала фильтрует, потом преобразует)
или tryPick: 'a list -> ('a -> 'b option) -> 'b option (мэппим элементы, пока не вернется Some(_), тогда прерываемся и возвращаем этот Some(_))
Вообще, похоже, функции преобразования коллекций в Nemerle заметно отличаются от таковых в FP языках. + намного более широкое использование императивных конструкций типа foreach, when, unless.
VD> // Просим компилятор отдать нам FixedType для известного типа и берем у него TypeInfo. VD> def mappedMemberAttribute = <[ ttype: Nemerle.Internal.MappedMemberAttribute; ]>.TypeInfo;
FixedType — это .net тип, полученный по символу (Nemerle.Internal.MappedMemberAttribute в данном случае)?
VD> // "размтачиваем" его одним залпом. У нас должен быть ровно один параметр типа "строковый литерал". VD> | Some((_, [<[ $(name : string) ]>])) => name
Здравствуйте, vaskir, Вы писали:
V>Да, возникает желание использовать Option.map/bind/fill/ofNull/... и функции типа
Дело привычки. Я вот даже не знал, что делает этот ofNull. Пришлось по другому коду догадываться.
V>choose: ('a -> 'b option) -> 'a list -> 'b list (map 'a в 'b option и фильтрация != None за один проход). Не нашел ничего похожего в Nemerle (MapFiltered сначала фильтрует, потом преобразует)
V>или tryPick: 'a list -> ('a -> 'b option) -> 'b option (мэппим элементы, пока не вернется Some(_), тогда прерываемся и возвращаем этот Some(_))
V>Вообще, похоже, функции преобразования коллекций в Nemerle заметно отличаются от таковых в FP языках. + намного более широкое использование императивных конструкций типа foreach, when, unless.
В Немерле есть, что называется, стандартный джентельменский набор взятый из ML-я — Filter/Map/Fold/Rev/Iter. С помощью этих функций можно что угодно со списком сделать. Тот же choose просто превращается в — Map(...).Filter(...). Но здесь все это не особо нужно было.
VD>> // Просим компилятор отдать нам FixedType для известного типа и берем у него TypeInfo. VD>> def mappedMemberAttribute = <[ ttype: Nemerle.Internal.MappedMemberAttribute; ]>.TypeInfo;
V>FixedType — это .net тип, полученный по символу (Nemerle.Internal.MappedMemberAttribute в данном случае)?
FixedType — это вариантный тип которым, внутри компилятора, представляется немерловый выведенный или указанный явно тип.
Цитата "ttype:" просто упрощает код производят связывание указанной строки и формирует по нему FixedType. В случае с конструированными дженериками (т.е. с дженериками которым в качестве параметра типа подставлен конекртный тип) это существенно упрощает код.
V>PExpr.Literal(Literal.String(name)) --> <[ $(name : string) ]> V>Красиво
Квази-цитирование. Что приятно, работает в обе стороны. Плюс в сплайсах могут быть не только литералы, но и, например, имена переменных.
V>Спасибо за hint насчет foreach и when, действительно мощно.
Это еще самый простой случай. В Немерле и foreach, и when с if поддерживают полноценный паттерн-матчинг. Так что можно писать, например, так:
foriach (x is Какой_то_Паттерн(с_переменной) in lst)
Foo(с_переменной);
а when — это всего лишь гуард, который может идти за местом где паттерн идет.
Тоже самое в when:
class A {}
class B : A {}
...
def a : A = B();
when (a is B as b)
{
Foo(b); // b имеет тип B
}
Причем foreach, when и if — это всего лишь макросы, которые переписывают код в match-и и рекурсивные вызовы. В самом языке ни циклов, ни управляющих конструкций вроде match нет. Все реализуется через рекурсию и match.
Кстати, when и if паттернматчингу обучил я лично, задолбавшийсь каждый раз переходить на более громоздкий match когда требовался паттерн-матчинг.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, VladD2, Вы писали:
VD>Кстати, when и if паттернматчингу обучил я лично, задолбавшийсь каждый раз переходить на более громоздкий match когда требовался паттерн-матчинг.
if обучил я. А ты долго боялся, что компиляция замедлится.
... << RSDN@Home 1.2.0 alpha 5 rev. 62>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн