Макрос для пересоздания структур
От: Алексей П Россия  
Дата: 22.08.07 07:12
Оценка: 129 (3)
Основываясь на прошлых попытках, уже ничего не предлагаю , а просто хочу поделиться удобным макросом, идея которого взята из Haskell-а.
Допустим, есть класс
[Record]
class A {
    [Accessor] x: int;
    [Accessor] y: string;
    [Accessor] z: float;
}

и нужно создать экземпляр этого класса, основываясь на каком-то имеющемся, но с измененными полями. Понятно, что это можно сделать и вручную:
def a = A(1, "abc", 2.5f);
def b = A(a.X, "def", 1.4f);

Но если в классе не 3 поля, а 7-10, и делать это надо много раз, да еще и список полей может меняться, то это неудобно. Поэтому я написал макрос, который это автоматизирует, вот этот код эквивалентен второй строчке кода выше:
def b = RecreateRecord(a, y = "def", z = 1.4f);

Макрос выводит тип a, ищет подходящий конструктор и подменяет нужные параметры. Всё просто.
Если это кому-то интересно, вот код этого макроса, пользуйтесь как угодно:

using System;
using Nemerle.Compiler;
using Nemerle.Compiler.Parsetree;
using Nemerle.Compiler.Typedtree;
using Nemerle;

using Nemerle.Collections;
using Nemerle.Utility;

macro RecreateRecord(source, params assignments: list[PExpr])
{
    // пытаемся вывести тип первого параметра - того, что надо пересоздать
    match(Macros.ImplicitCTX().TypeExpr(source).ty.Fix())
    {
        | Class(tyinfo, _) as source_mtype =>
                def properties = $[ mem | mem is IProperty in tyinfo.GetMembers(BindingFlags.Public)];
                
                // проверить, существуют ли свойства с именами, такими же, 
                // как у параметров конструктора, либо преобразованными по правилу Accessor'а
                def ctor_fits(ctor)
                {
                    def parms = ctor.GetHeader().parms;
                    !parms.IsEmpty && parms.ForAll(parm => 
                        properties.Exists(prop => 
                            prop.Name == parm.Name || prop.Name == AccessorHelper.TransformName(parm.Name)
                        ) 
                    )
                }
                
                match(tyinfo.GetConstructors(BindingFlags.Public).Find(ctor_fits))
                { 
                    | None => 
                            Message.Error($"no suitable constructor found in type $(tyinfo.Name)");
                            source
                    
                    | Some(ctor) => 
                            def ctor_typed = TExpr.StaticRef(source_mtype, ctor, []);
                            def ctor_parms = ctor.GetHeader().parms;
                            
                            // список пар имя -> значение для параметров макроса
                            def new_values = assignments.Map(fun(expr) {
                                | <[ $name = $value ]> => 
                                        def name = name.ToString();
                                        unless(ctor_parms.Exists(parm => parm.Name == name))
                                            Message.FatalError($"the constructor doesn't have a parameter named $name");
                                        (name, value)
                                | _ => Message.FatalError($"expected list of param = value, got $expr")
                            });
                            
                            // выражения для параметров конструктора, уже с заменами из параметров макроса
                            def new_parms = ctor_parms.Map(parm =>
                                match(List.Assoc(new_values, parm.Name))
                                {
                                    | Some(value) => value
                                    | None => 
                                            def (Some(prop)) = properties.Find(prop => 
                                                prop.Name == parm.Name || prop.Name == AccessorHelper.TransformName(parm.Name)
                                            );
                                            <[ source_ref.$(prop.Name : dyn) ]>
                                }
                            );
                            
                            // результат
                            <[ 
                                def source_ref = $source;
                                $(ctor_typed : typed)(..$new_parms) 
                            ]>
                }
        | other_type =>
                Message.Error($"the recreating expression must be a class, but it is $other_type");
                source
    }
}

// Это из макроса Accessor из стандартной библиотеки Nemerle.
// Вообще-то, у меня тут было больше методов, но для этого макроса необходим только этот.
public module AccessorHelper
{
    public TransformName(name: string): string
    {
        def sb = StringBuilder ();
        mutable next_upper = true;
        foreach (ch in name)
            if (ch == '_') {
                next_upper = true;
            } else if (next_upper) {
                _ = sb.Append (char.ToUpper (ch));
                next_upper = false;
            } else
                _ = sb.Append (ch);
        sb.ToString ()
    }
}
Re: Макрос для пересоздания структур
От: VladD2 Российская Империя www.nemerle.org
Дата: 22.08.07 11:34
Оценка: 16 (1)
Здравствуйте, Алексей П, Вы писали:

АП>Макрос выводит тип a, ищет подходящий конструктор и подменяет нужные параметры. Всё просто.


Сразу одно замечание. Тип может не вывестись в месте применения. В таком случае нужно исползовать отложенное выполнение макроса. О том как это сделать я как раз написал во второй части статьи "Макросы Nemerle – расширенный курс"
Автор(ы): Чистяков Влад (VladD2)
Дата: 31.07.2007
Во второй части статьи о макросах Nemerle речь пойдет о макросах уровня выражения, о макросах, изменяющих синтаксис языка, а также о контексте компиляции, доступном в макросах, и тех возможностях, которые он предоставляет (типизации выражений, получении доступа к описанию типов проекта, информации о методах и т.п.).
. Она пока не выложена на сайт. Так что процитирую этот фрагмент статьи:

Откладывание выполнения макроса до момента, когда информация о типах становится доступной
Не будет преувеличением сказать, что на сегодня Nemerle обладает самым сложным механизмом вывода типов, по крайней мере, среди гибридных языков, т.е. языках программирования, поддерживающих ООП и ФП.
Система вывода типов Nemerle может выводить типы из использования. Причем использование, определяющее (или уточняющее) тип переменной, может быть хоть самым последним выражением метода. Более того, оно может быть косвенным. Классическим примером мощности вывода типов в Nemerle является работа с Dictionary[K, V]:

using System;
using System.Console;
using System.Collections.Generic;

def dic = Dictionary();
WriteLine(dic.GetType().FullName);
dic.Add("Текущая дата", DateTime.Now);

Этот код выведет на консоль:
System.Collections.Generic.Dictionary`2[[System.String, mscorlib, Version=2.0.0.
0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.DateTime, mscorlib,
 Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]

Заметьте, что хотя в примере не указано явно ни одного параметра типа для Dictionary, компилятор все равно прекрасно выводит тип переменной.
Данный пример еще очень прост. На практике переменная еще до ее какой-либо инициализации может передаваться в перегруженный метод. При этом для определения того, какой из методов должен быть вызван, компилятор должен знать, что за тип у переменной, переданной методу в качестве параметра. Это заставляет компилятор вычислять типы переменных рекурсивно. В Typer-е есть список выражений, ожидающих типизации (содержащих так называемые неразрешенные переменные типов). Когда Typer завершает первый проход по методу, он просматривает этот список, и если он не пуст, но изменен по сравнению с предыдущим проходом, снова запускает процесс типизации. На каждом цикле типизации могут выявляться типы тех или иных переменных, что позволяет на следующих циклах типизации использовать эту информацию для выявления типов у других (связанных с этими) переменных. По сути, компилятор строит набор отношений между переменными (точнее, их типами) и пытается последовательно решить головоломку типов.
Если код корректен, то рано или поздно список выражений, ожидающих типизации, становится пустым, и процесс типизации оканчивается. Если программа содержит код, в котором невозможно определить типы (содержит циклические зависимости или просто ошибки), то компилятор выдает сообщения об ошибках и останавливает процесс типизации.
Как все это касается разработчиков макросов? А самым наипрямейшим образом. Дело в том, что процесс раскрытия макросов проходит как раз во время типизации. Когда компилятор последовательно разбирает все ветки нетипизированного AST (т.е. цепочку вариантов PExpr), типизирует их, и, если встречается PExpr.MacroCall, пытается раскрыть макрос, и типизировать получившееся в результате его выполнения выражение (имеющее опять же тип PExpr). Если это выражение тоже содержит обращения к макросам, то раскрываются и они.
Для выражений, находящихся до раскрываемого макроса, компилятор пытается вычислить типы, но, как уже говорилось, это не всегда возможно. Поэтому во время раскрытия макроса не вся информация о типах может быть доступна.
Чтобы продемонстрировать сказанное, создадим макрос, пытающийся вычислить информацию о типах выражения и выводящий эту информацию в консоль IDE. Вот его код:
macro PrintExpressionType(expr)
{
  def typer = Nemerle.Macros.ImplicitCTX();
  def tExpr = typer.TypeExpr(expr);
  
  def msg = $"Во время работы макроса тип '$tExpr' "
    + match (tExpr.Type.Hint)
      {
        | Some(ty) => $"известен: $(ty)."
        | None     =>  "НЕизвестен!"
      };
      
  Message.Hint(msg);
}

Теперь попытаемся применить его. В следующем случае:
mutable x = array[0];
PrintExpressionType(x);

Макрос выведет в консоль IDE:
Во время работы макроса тип 'x' известен: array [int-].

Однако стоит немного изменить пример:
mutable x = null;
PrintExpressionType(x);
x = array[0];

и макрос, что называется, «обломается».
Во время работы макроса тип 'x' НЕизвестен!

Что же делать?
Можно заставить компилятор отложить вычисление макроса, как говорится, до лучших времен. Делается это с помощью метода DelayMacro все того же контекста макроса (т.е. объекта типа Ty-per).
Изменим макрос следующим образом:
macro PrintExpressionType(expr)
{
  def typer = Nemerle.Macros.ImplicitCTX();
  def tExpr = typer.TypeExpr(expr);
  
  def msg = $"Во время работы макроса тип '$tExpr' "
    + match (tExpr.Type.Hint)
      {
        | Some(ty) => $"известен: $(ty)."
        | None     => "НЕизвестен!"
      };
      
  Message.Hint(msg);
    
  def result = typer.DelayMacro(fun (fail_loudly)
  {
    def tExpr = tExpr;
    match (tExpr.Type.Hint)
    {
      | Some(ty) =>
        // do something with the type
        Message.Hint($"Внутри DelayMacro тип для '$tExpr' известен: $(ty).");
        Some(<[ () ]>)

      | None =>
        when (fail_loudly)
          Message.Error(expr.loc, $ "невозможно вывести тип для '$expr'");
        
        None()
    }
  });

  result
}

И снова выполним код:
mutable x = null;
PrintExpressionType(x);
x = array[0];

На этот раз макрос выведет в консоль IDE следующее:
Во время работы макроса тип 'x' НЕизвестен!
Внутри DelayMacro тип для 'x' известен: array [int-].

Метода DelayMacro имеет следующую сигнатуру:
DelayMacro(resolve : bool -> option[PT.PExpr], 
           expected : TyVar = null) : PT.PExpr

Ему можно задать переменную типа, описывающую какой тип ожидается (если она не задана, то используется «свежая» переменная типа, допускающая любой тип), и функцию, которая будет вызываться, когда компилятор будет делать повторные проходы типизации.
По сути, метод DelayMacro возвращает слоеный пирог:
PExpr.Typed(TExpr.Delayed(...))

в который закладывается ссылка на Typer, переданную нами ссылку на функцию, локальный контекст, переменную типа и другие необходимые данные. Компилятор, встретив PExpr.Typed, просто разворачивает его содержимое и помещает в конструируемый им типизированный AST. На следующем цикле типизации компилятор обнаруживает TExpr.Delayed и пытается вызывать переданную нами ссылку на функцию. Если функция возвращает None(), то компилятор пытается вызвать ее на следующем витке типизации, и так далее. Если функция возвращает Some(PExpr(...)), то PExpr раскрывается и типизируется. Естественно, что при формировании этого PExpr мы вольны использовать уже доступную информацию о типах.
Таким образом, DelayMacro позволяет нам дождаться появления необходимой информации о типах и сгенерировать код на ее основе. Это действительно мощнейшая возможность, использование которой упрощается использованием лямбд и замыканий.


Если нужно для дела (и нет бумажной версии журнала), то могу выслать статью по мылу.
... << RSDN@Home 1.2.0 alpha rev. 637>>
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[2]: Макрос для пересоздания структур
От: Алексей П Россия  
Дата: 22.08.07 15:13
Оценка:
Здравствуйте, VladD2, Вы писали:

VD>Если нужно для дела (и нет бумажной версии журнала), то могу выслать статью по мылу.


Бумажная версия есть, но недавно => статью пока не читал. А на момент создания этого макроса ее, вероятно, вообще не существовало, и, как несложно догадаться, у меня ни разу не случилось невыведения типов.
Но как дойдут руки — допишу.
Re: Макрос для пересоздания структур
От: VladD2 Российская Империя www.nemerle.org
Дата: 22.08.07 17:03
Оценка:
Здравствуйте, Алексей П, Вы писали:

Несколько идей...

1. Можно было бы реализовать данную возможность не только для типов помеченных как Record, но и для любых других. Для этого нужно создать метаатрибут который будет добавлять соответствующую фунциональность к типу.
2. Тут недавно в Философии речь шла о Хаскелевском gmap. Можно было бы реализовать нечто похожее для дотнетовских типов. Так чтобы можно бвыло взять некую произвольный объкт, являющийся корнем некой произвольного графа объектов, и одним вызовом создать копию этого графа конвертирую значения некоторых типов с помощью передваемой этой фунции сслки на фунцию.
... << RSDN@Home 1.2.0 alpha rev. 637>>
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[2]: Макрос для пересоздания структур
От: Алексей П Россия  
Дата: 22.08.07 18:19
Оценка:
Здравствуйте, VladD2, Вы писали:

VD>1. Можно было бы реализовать данную возможность не только для типов помеченных как Record, но и для любых других. Для этого нужно создать метаатрибут который будет добавлять соответствующую фунциональность к типу.

А он и не обязан быть Record-ом. Требуется только выполнение следствий Record-а: существует не-пустой конструктор, для каждого параметра которого есть одноименное или почти одноименное свойство.

VD>2. Тут недавно в Философии речь шла о Хаскелевском gmap. Можно было бы реализовать нечто похожее для дотнетовских типов. Так чтобы можно бвыло взять некую произвольный объкт, являющийся корнем некой произвольного графа объектов, и одним вызовом создать копию этого графа конвертирую значения некоторых типов с помощью передваемой этой фунции сслки на фунцию.

Я пока даже не понял, как в Хаскелле-то gmap работает. Есть подозрение, что на списках — глубоко рекурсивно.
Re[3]: Макрос для пересоздания структур
От: VladD2 Российская Империя www.nemerle.org
Дата: 23.08.07 11:44
Оценка:
Здравствуйте, Алексей П, Вы писали:

АП>А он и не обязан быть Record-ом. Требуется только выполнение следствий Record-а: существует не-пустой конструктор, для каждого параметра которого есть одноименное или почти одноименное свойство.


А хороло было бы если я просто пометил бы тип неким атрибутом (ну, например, Recreatable) и этот атрибут сгенерил бы мне все что нужно для пересоздания объекта в описанном тобой стиле. При этом, естественно, не стоит требовать от класса наличия кострукторов и т.п.

АП>Я пока даже не понял, как в Хаскелле-то gmap работает. Есть подозрение, что на списках — глубоко рекурсивно.


Да это и не важно. В Немерле, то один фиг нужно генерировать код для этого gmap-а.
... << RSDN@Home 1.2.0 alpha rev. 637>>
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[4]: Макрос для пересоздания структур
От: Алексей П Россия  
Дата: 24.08.07 06:46
Оценка:
Здравствуйте, VladD2, Вы писали:

VD>Здравствуйте, Алексей П, Вы писали:


АП>>А он и не обязан быть Record-ом. Требуется только выполнение следствий Record-а: существует не-пустой конструктор, для каждого параметра которого есть одноименное или почти одноименное свойство.


VD>А хороло было бы если я просто пометил бы тип неким атрибутом (ну, например, Recreatable) и этот атрибут сгенерил бы мне все что нужно для пересоздания объекта в описанном тобой стиле. При этом, естественно, не стоит требовать от класса наличия кострукторов и т.п.


Подумав, пришел к выводу, что такой атрибут будет эквивалентен [Record]. Потому что для создания константного объекта необходимо передать ему значения всех его полей в конструкторе (или если есть какие-то вычисляемые поля, они должны вычисляться в том же конструкторе чистыми функциями, но это особый случай: только вручную), а это именно то, что делает Record без параметров. Так зачем плодить одинаковые атрибуты?

Или есть какой-то другой способ организовать пересоздаваемость?
Re[5]: Макрос для пересоздания структур
От: VladD2 Российская Империя www.nemerle.org
Дата: 24.08.07 12:10
Оценка:
Здравствуйте, Алексей П, Вы писали:

АП>Подумав, пришел к выводу, что такой атрибут будет эквивалентен [Record]. Потому что для создания константного объекта необходимо передать ему значения всех его полей в конструкторе (или если есть какие-то вычисляемые поля, они должны вычисляться в том же конструкторе чистыми функциями, но это особый случай: только вручную), а это именно то, что делает Record без параметров. Так зачем плодить одинаковые атрибуты?


Можно воссоздавать объект на основании его публичных изменяемых свойств. При этом не заданные свойства получают значений свойств исходного объекта.
... << RSDN@Home 1.2.0 alpha rev. 637>>
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.