Привет, Хабр! Меня зовут Станислав Чернышев, я автор книги «Основы Dart», телеграм-канала MADTeacher и доцент кафедры прикладной информатики в Санкт-Петербургском государственном университете аэрокосмического приборостроения. 

В этой статье я хочу:

  • поделиться очень черновым наброском 9-й главы - «Макросы и механизм аугментации (Augmentations)». Несмотря на то, что данная функциональность пребывает в экспериментальном режиме и требует для своего прощупывания Dart 3.5.0-152.dev (и старше), текущей реализации макросов и аугментации достаточно для того, чтобы разобраться в тех принципах, которые будут лежать в их основе.

  • рассказать об изменениях, которые случились с книгой "Основы Flutter" за прошедший месяц.

Оглавление

Введение

«Программисты ненавидят повторения», – именно с этой фразы начинается документ с мотивацией разработчиков языка Dart о необходимости добавления макросов, тем самым открывая рядовым программистам возможность заявить любителям Kotlin, Java и т.д., что теперь настала и наша очередь «обмазываться» аннотациями ? Другими словами, макросы привносят в Dart элементы статического метапрограммирования, позволяющего писать код, который оперирует другим кодом, как если бы это были данные. Они принимают другой код в качестве параметров и могут осуществлять над ним ряд операций: просматривать, проверять, создавать, изменять и возвращать. А под статическим понимается то, что эти операции будут выполнены во время компиляции приложения.

Но чем макросы отличаются от обычной кодогенерации или механизмов рефлексии (метапрограммирования) самого Dart? Относительно кодогенерации, которая использовалась повсеместно для ускорения разработки приложений, макросы:

  • Не записываются на диск, тем самым очищая директорию проекта от сгенерированных файлов с кодом;

  • Не требуют длительной перекомпиляции аннотированных частей кода в проекте;

  • Позволяют просматривать генерируемый код в режиме реального времени. Здесь надо сказать отдельное спасибо разработчикам за переработанный анализатор и добавленный механизм аугментации. После аннотирования макросом класса, функции и т.д., производится его запуск и анализатор позволяет не только перейти к просмотру полученного кода, но и предоставляет сводку по ошибкам, если таковые присутствуют в сгенерированном коде или произошли в процессе запуска макроса;

  • Напрямую дополняют существующий класс, функцию и т.д.

Несмотря на приведенные достоинства макросов, кодогенерация хоть и уходит на второй план, но не канет в забвение. Это связано с тем, что ее возможности куда шире, чем могут предложить макросы. К тому же, на механизмы кодогенерации не налагаются такие жесткие ограничения, как использование только стандартного (урезанного) набора библиотек, а именно:

  • dart:async

  • dart:collection

  • dart:convert

  • dart:core

  • dart:math

  • dart:typed_data

Такой подход обусловлен тем, что разработчики языка не хотят предоставлять макросам возможность напрямую обращаться к файловой системе, сети или запускать процессы. Что избавляет нас от ситуации, в которой после подключения пакета с макросом и его использования для аннотации класса, функции и т.д., возможна «учетка» ваших данных на какой-нибудь удаленный сервер >_<

С рефлексией все куда печальней. По сути, с появлением макросов, команда Dart решает еще одну проблему языка - невозможность использовать библиотеку для рефлексии dart:mirrors, когда сборка приложения осуществляется через aot-компиляцию (Привет, Flutter!). В любом случае, ее внедрение сильно скажется на производительности системы, в то время как макросы (статическое метапрограммирование) осуществляют кодогенерацию на этапе компиляции.

Но, прежде чем перейдем к более близкому знакомству с макросами, нам необходимо разобраться с механизмом аугментации, благодаря которому и стала возможна их имплементация в Dart. Стоит отметить, что на данный момент макросы и аугментация находятся в экспериментальном режиме и их API может поменяться до выхода в релиз (начало 2025 года) еще ни один раз.

Включение экспериментальных функций Dart

В качестве демонстрационного примера по применению макросов, используем анонсированный командой Dart макрос «@JsonCodable» для сериализации/десериализации классов. Так как макросы пока доступны только c версии 3.5.0-152 (в моем случае использовалась 3.5.0-278.0.dev), которая находится в разработке, убедитесь, что она (или версия постарше) имеется на вашем компьютере и по умолчанию используется в создаваемых проектах. Если же у вас развернуто несколько версий Dart, тоже не беда. Указать нужную можно на шаге добавления флага экспериментального режима для запускаемых приложений. Есть несколько способов, как это можно сделать. Первый заключается в добавлении флага в глобальные настройки и будет применяться ко всем создаваемым проектам, а второй в конфигурации только конкретного проекта.

Если хотите пойти по первому пути, то откройте VS Code и нажмите связку клавиш «Ctrl+,», либо перейдите по пути «File->Preferences->Settings» и введите в графе поиска открывшейся вкладки слово «dart». В появившейся выдаче выберите раздел SDK и найдите в нем следующий пункт «Dart: Sdk path»:

Настройка экспериментального режима
Настройка экспериментального режима

После того, как нажмете на ссылку «Edit in setting.json» и откроется одноименный файл, найдите в нем свойство, начинающееся с «dart.sdkPath» (отвечает за путь до используемого Dart SDK) и добавьте после него следующие строки:

 "dart.sdkPath": "C:\\Dart\\dart-3",
 "dart.cliAdditionalArgs": [
        "--enable-experiment=macros"
    ],

 Если вам претит сама мысль о глобальной установки файла, то в текущем проекте перейдите на панель «Run and Debug» (Ctrl+Shift+D) и нажмите на создание файла «launch.json»:

Создание файла «launch.json»
Создание файла «launch.json»

В появившемся виджете выберите «Dart & Flutter»:

Создание файла «launch.json»
Создание файла «launch.json»

После чего в открывшемся файле «launch.json» добавьте свойство «toolArgs»:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "macros_first_blood",
            "request": "launch",
            "type": "dart",
            "toolArgs": [
                "--enable-experiment=macros",
            ],
        }
    ]
}

Единственный минус такого подхода, что приложение нужно запускать только через «F5». Если попробуете запустить по нажатию мышки на «Run» над функцией main, экспериментальный флаг не будет применен в качестве параметра при запуске, что приведет к ошибке времени компиляции.

Вне зависимости от выбранного способа добавления экспериментального флага, линтеру необходимо указать, какая из экспериментальных функций будет вами использоваться в процессе разработки. Для этого в корневой директории проекта откройте файл «analysis_options.yaml» и добавьте:

analyzer:
  enable-experiment:
    - macros

Для следующего примера создайте новый консольный проект «macros_first_blood», переименовав в директории «bin» файл «macros_first_blood.dart» в «main.dart» и проделав необходимые шаги по включению экспериментального флага macros. Чтобы добавить в качестве внешней зависимости проекта макрос @JsonCodable, откройте файл «pubspec.yaml», указав в разделе «dependencies» пакет «json»:

name: macros_first_blood
description: A sample command-line application.
version: 1.0.0

environment:
  sdk: ^3.5.0-278.0.dev

dependencies:
  json: ^0.20.2

После сохранения (Ctrl+S) и подкачки указанной зависимости в проект, откройте файл «main.dart», приведя его к следующему виду:

// main.dart
class Batman {
  final String name;
  final int age;

  Batman(this.name, this.age);

  @override
  String toString() {
    return 'Batman{name: $name, age: $age}';
  }
}


void main(List<String> arguments) {
  var batman = Batman('Bruce Wayne', 30);
  print(batman); // Batman{name: Bruce Wayne, age: 30}
}

Для добавления сериализации в json и десериализации объекта теперь не надо подключать пакет «json_serializable» и запускать кодогенерацию. Или вручную добавлять фабричный именованный конструктор «fromJson» и метод «toJson». Достаточно импортировать макрос и аннотировать им класс:

// main.dart
import 'package:json/json.dart';

@JsonCodable()
class Batman {
  // без изменений
}


void main(List<String> arguments) {
  var batman = Batman('Bruce Wayne', 30);
  print(batman); // Batman{name: Bruce Wayne, age: 30}

  final  json = <String, dynamic>{
    'name': 'Bruce Wayne',
    'age': 30
  };

  var macroBatman = Batman.fromJson(json);
  print(macroBatman); // Batman{name: Bruce Wayne, age: 30}
  print(macroBatman.toJson()); // {name: Bruce Wayne, age: 30}
}

Со сгенерированным макросом кодом можно ознакомиться, нажав на строку «Go to Augmentation», которая появится в VS Code ниже добавленной аннотации @JsonCodable():

Переход к сгенерированному коду
Переход к сгенерированному коду

После нажатия на переход, откроется вкладка с добавленным к классу кодом, который нельзя (да и нет возможности) редактировать:

augment library 'file:///путь до main.dart';

import 'dart:core' as prefix0;

augment class Batman {
  external Batman.fromJson(prefix0.Map<prefix0.String, prefix0.Object?> json);
  external prefix0.Map<prefix0.String, prefix0.Object?> toJson();
  augment Batman.fromJson(prefix0.Map<prefix0.String, prefix0.Object?> json, )
      : this.name = json[r'name'] as prefix0.String,
        this.age = json[r'age'] as prefix0.int;
  augment prefix0.Map<prefix0.String, prefix0.Object?> toJson() {
    final json = <prefix0.String, prefix0.Object?>{};
    json[r'name'] = this.name;
    json[r'age'] = this.age;
    return json;
  }
}

При любом добавлении нового поля класса или изменении имени уже существующего, код в открытой вкладке будет модифицироваться в режиме реального времени.

Механизм аугментации (Augmentations)

Аугментация или как ее можно перевести – дополнение, помимо поддержки имплементации макросов в Dart, дает разработчикам куда больше свободы, чем было у них до этого. Она позволяет распределить реализацию класса, функции и т.д. как внутри одного файла, так и по нескольким. Такая формулировка, отчасти, напоминает уже существующий механизм разнесения по нескольким библиотекам – «part of». Но он не позволяет производить реального разделения реализаций.

На данный момент времени Dart не полностью реализует спецификацию механизма аугментации, но уже добавленных функций достаточно для более тесного знакомства с ним. Главное, не забудьте добавить экспериментальный флаг macros в «analysis_options.yaml» и настройки запуска приложения.

Создайте новый консольный проект «augment_class», после чего откройте одноименный файл в директории «bin». В идеале, следующий код с разделением реализации класса Batman должен быть полностью рабочим:

// augment_class.dart
class Batman {
  final name;
  final age; 
}

augment class Batman {
  Batman(this.name,this.age);

  @override
  String toString() {
    return 'Batman{name: $name, age: $age}';
  }
}

void main(List<String> arguments) {
    var batman = Batman("Bruce", 20);
    print(batman.toString());
}

Никакие ошибки не подсвечиваются, анализатор не трубит и какой-либо проблеме. Но программа не скомпилируется. Это связано с тем, что аугментация (разделение реализаций) в одном файле до сих пор не была реализована. Чтобы сделать пример рабочим, воспользуемся аугментацией библиотек.

В директории «bin» создайте новый файл «aug_batman.dart» и переместите в него дополнение класса Batman, указав в первой строке, что текущая библиотека будет являться дополнением существующей, а именно «augment_class.dart»:

// aug_batman.dart
// указываем функционал какой библиотеки 
// дополняют описываемые реализации в текущем файле
augment library 'augment_class.dart'; 
      

augment class Batman {
  Batman(this.name,this.age);

  @override
  String toString() {
    return 'Batman{name: $name, age: $age}';
  }
}

А в файле «augment_class.dart» добавьте строку с импортом дополнения, ее синтаксис несколько отличается от привычного, но к этому легко привыкнуть:

// augment_class.dart
import augment 'aug_batman.dart'; // подключение дополнения

class Batman {
  final name;
  final age; 
}

void main(List<String> arguments) {
    var batman = Batman("Bruce", 20);
    print(batman.toString()); // Batman{name: Bruce, age: 20}
}

Аугментация классов позволяет разбить большой класс на несколько составных частей, отдельно выделив геттеры, сеттеры и разбив по смысловой части совершаемые операции над данными. Такой подход избавляется нас проблем механизма «part of», при использовании которого нам было необходимо объявить все методы и поля класса в одном файле, а в отдельные выносить функции, вызываемые из методов.

И это не все существующие фишки рассматриваемого механизма! Аугментации, наравне с функциями, могут подвергаться и методы класса:

// aug_batman.dart
// указываем функционал какой библиотеки 
// дополняют описываемые реализации в текущем файле
augment library 'augment_class.dart'; 
      

augment class Batman {
  Batman(this.name,this.age);

  augment void printAge(){
    print(age+1);
  }

  @override
  String toString() {
    return 'Batman{name: $name, age: $age}';
  }
}

// augment_class.dart
import augment 'aug_batman.dart'; // подключение дополнения

class Batman {
  final name;
  final age; 

  void printAge() {
    print(age);
  }
}


void main(List<String> arguments) {
    var batman = Batman("Bruce", 20);
    print(batman.toString()); // Batman{name: Bruce, age: 20}
    batman.printAge(); // 21
}

В данном случае у нас перезаписался исходный метод printAge у класса Batman и была вызвана реализация дополнения. Согласно спецификации, если в дополняемом методе или функции мы в какой-то момент хотим вызвать первоначальную реализацию, которую дополняем, следует использовать функцию верхнего уровня augmented(). Если того требует реализация дополняемого метода, посредством augmented можно передавать данные и получать возвращаемое значение. Но, это в теории… на практике так пока не работает =(

Макросы

Для объявления макроса необходимо воспользоваться модификатором класса macro и указать, какой из интерфейсов API макросов будет реализовываться. Такой подход позволяет задать к каким конструкциям кода (функция, библиотека, класс, конструктор класса и т.д.) будет применим макрос, реализующий интерфейс и на каком этапе генерации кода будет осуществляться вызов макроса.

Macros API

В целом, интерфейсы API можно разбить на 3 категории:

  • TypesMacro – используется для добавления объявления новых типов на верхнем уровне текущей библиотеки. Имеет наивысший приоритет в процессе генерации кода.

  • DeclarationsMacro – используется для добавления новых объявлений (функций, методов класса и т.д.), не относящихся к типам. Имеет средний приоритет в процессе генерации кода.

  • DefinitionMacro – используется для добавления аугментации, когда необходимо дополнить или изменить реализацию уже существующих объявлений функций, методов класса и т.д. Имеет низкий приоритет в процессе генерации кода.

Если ваш макрос реализует 2 или все 3 категории интерфейсов, то необходимо учитывать их приоритет в генерации кода. Иначе может случиться неприятная ситуация, приводящая к ошибке – макрос с более высоким приоритетом пытается оперировать кодом, которого еще нет, так как он добавляется макросом с более низким приоритетом кодогенерации.

Ниже приведена таблица с соотнесением префиксов к какой части кода применимы существующие интерфейсы макросов с обозначенными категориями. Получить полное название интерфейса можно путем их объединения. Например, если для префикса Class существует одна из приведенных выше категорий (допустим – DeclarationsMacro), то полное название интерфейса будет – ClassDeclarationsMacro:

 

Категория интерфейса

Префикс

TypesMacro

DeclarationsMacro

DefinitionMacro

Library

+

+

+

Function

+

+

+

Variable

+

+

+

Class

+

+

+

Enum

+

+

+

EnumValue

+

+

+

Field

+

+

+

Method

+

+

+

Constructor

+

+

+

Mixin

+

+

+

Extension

+

+

+

ExtensionType

+

+

+

TypeAlias

+

+

-

Каждый из приведенных интерфейсов предоставляет один метод, принимающий на вход экземпляр декларации аннотируемой части кода (например, ClassDeclaration) позволяющую получить доступ к его свойствам (поля класса, список методов, модификатор класса, конструкторы и т.д.) и одна из разновидностей Builder, дающая доступ к добавлению кода в различные области в зависимости от категории интерфейса.

Несмотря на то, что по названию интерфейса можно понять к каким участкам кода он применим, у некоторых из них имеются более широкие возможности:

Префикс

Область применения

Library

Декларации библиотек

Function

Функция верхнего уровня, обычный или статический метод класса

Variable

Переменная верхнего уровня или поле (переменная) класса

Class

Класс

Enum

Перечисление

EnumValue

Поля перечислений

Field

Поле (переменная) класса

Method

Метод класса

Constructor

Конструктор класса

Mixin

Миксины

Extension

Методы расширения

ExtensionType

Расширения типов

TypeAlias

Псевдонимы типов

Следует отметить, что последовательность этапов генерации кода зависит не только от реализуемых макросом интерфейсов, но и от того, сколько уровней аннотации макросами будет использовано над целевым кодом. Так, например, при трехуровневой аннотации:

@Third()
@Second()
@First()
class MyClass {}

Сначала будет применен самый первый макрос (First), которым аннотировали класс. Далее с учетом исходного и добавленного первым макросом кода отработает второй (Second) и так далее.

Макросы функций и классов

Создайте и настройте новый проект «macro_function», добавив в качестве внешней зависимости в файл «pubspec.yaml» пакет macros, который с релизом макросов станет частью SDK:

# pubspec.yaml
dependencies:
  macros: ^0.1.2-main.3 # 

Далее откройте в директории «bin» файл «macro_function.dart» приведя его содержание к следующему виду:

// bin/macro_function.dart
import 'package:macro_function/macro_function.dart';

int add(int a, int b) => a + b;

void main() {
  print(add(4, 5)); // 9
}

Сам макрос будем реализовывать в одноименном файле каталога «lib» (не забыв удалить файл с тестами) и начнем с использования интерфейса FunctionDeclarationsMacro, который позволяет нам добавлять новые объявления на основе аннотируемой функции в текущую библиотеку. В качестве первого примера реализуем макрос для создания новой функции-декоратора, возводящей в квадрат результат аннотируемой функции:

// lib/macro_function.dart
import 'dart:async';

import 'package:macros/macros.dart';

macro class Pow2 implements FunctionDeclarationsMacro {
  const Pow2(); // обязателен константный конструктор
  
  @override
  FutureOr<void> buildDeclarationsForFunction(
    // хранит данные аннотированной функции
    FunctionDeclaration function, 
    // используется для добавления декларации в библиотеку
    DeclarationBuilder builder,
  ) async {
    // получаем все позиционные аргументы
    var args = function.positionalParameters;
    // получаем тип возвращаемого значения декорируемой функции
    var resultType = function.returnType;

    var code = DeclarationCode.fromParts([
      resultType.code,
      ' mul${function.identifier.name}', // имя добавляемой функции
      '(',      
      for (var field in args) // перечисляем все аргументы
        RawCode.fromParts([
          field.type.code,
          ' ',
          field.identifier,
          ', '
        ]),
      '){\n', // начало тела функции
      // получаем результат декорируемой функции
      'var result = ${function.identifier.name}(',
      ...args.map((field) => '${field.identifier.name}, '),
      ');\n',
      'return result*result;\n', // возвращаем результат
      '}\n' // конец тела функции
    ]);

    // добавляем декларацию в библиотеку
    builder.declareInLibrary(code);
  }
}

Подход с

       RawCode.fromParts([
          field.type.code,
          ' ',
          field.identifier,
          ', '
        ])

позволяет нам объявить List<Code> и заполнять его частями декларируемого кода по мере выполнения макроса, после чего передать на вход builder.declareInLibrary().

Теперь вернемся в директорию «bin», аннотируем макросом функцию и передадим в print новую сгенерированную функцию pow2add, после чего запустим приложение:

// bin/macro_function.dart
import 'package:macro_function/macro_function.dart';

@Pow2()
int add(int a, int b) => a + b;

void main() {
  print(pow2add(4, 5)); // 81
}

Поскольку пока VS Code через раз поддерживает переход к сгенерированному коду после добавления аннотации над функцией, воспользуемся небольшой хитростью. А именно – напишем простой макрос для объявления конструктора по умолчанию, добавим класс Batman в файл «bin\macro_function.dart» и обмажем его реализованным макросом. Для начала создайте в каталоге «lib» файл «macro_constructor.dart»:

// macro_constructor.dart
import 'dart:async';

import 'package:macros/macros.dart';

macro class DefaultConstructor implements ClassDeclarationsMacro {
  const DefaultConstructor();

  @override
  FutureOr<void> buildDeclarationsForClass(
            ClassDeclaration clazz, 
            MemberDeclarationBuilder builder,
   ) async {
    // получаем поля класса
    var fields = await builder.fieldsOf(clazz);
    var code = DeclarationCode.fromParts([
      clazz.identifier.name, // имя класса
      '(',      
      ...fields.map((field) => 'this.${field.identifier.name},'),
      ');'
    ]);
    // добавляем конструктор в класс
    builder.declareInType(code);
  }
}

Вернемся к точке входа в приложение:

// bin/macro_function.dart
import 'package:macro_function/macro_function.dart';
import 'package:macro_function/macro_constructor.dart';

@DefaultConstructor()
class Batman {
  final String name;
  final int age;

  @override
  String toString() {
    return 'Batman(name: $name, age: $age)';
  }
}

@Pow2()
int add(int a, int b) => a + b;

void main() {
  print(pow2add(4, 5)); // 81
  var batman = Batman('Bruce', 30);
  print(batman); // Batman(name: Bruce, age: 30)
}

Чтобы посмотреть код добавленного конструктора и функции-декоратора, нажмите на строку «Go to Augmentation» после макроса @DefaultConstructor():

augment library 'file:/// путь до bin/macro_function.dart';

import 'dart:core' as prefix0;

prefix0.int pow2add(prefix0.int a, prefix0.int b, ){
	var result = add(a, b, );
	return result*result;
}

augment class Batman {
Batman(this.name,this.age,);
}

Следующим шагом добавим макрос для функции-декоратора, которому на вход будем передавать значение, на которое должен быть умножен результат декорируемой функции и дополнительно добавим вывод значения в терминал. Для этого потребуется получить функцию print из библиотеки «dart:core». Но для начала добавим в файл «lib/macro_function.dart» следующую строку после импортов:

final _dartCore = Uri.parse('dart:core');

Данная переменная нам потребуется для получения префикса, через который в генерируемом коде будет совершено обращение к print. Это нужно сделать для того, чтобы наш код не конфликтовал с библиотекой dart:core. Получение функции с ее идентификатором (префиксом) можно получить через метод builder.resolveIdentifier(). В данный момент, из-за своей не совсем безопасной реализации, он помечен как Deprecated, но альтернативы пока отсутствуют =)

А теперь, вооружившись полученными знаниями, реализуем новый макрос и аннотируем им функцию add:

// lib/macro_function.dart
import 'dart:async';

import 'package:macros/macros.dart';

final _dartCore = Uri.parse('dart:core');

macro class Mul implements FunctionDeclarationsMacro {
  final int scalar;
  const Mul(this.scalar);
  
  @override
  FutureOr<void> buildDeclarationsForFunction(
    FunctionDeclaration function, 
    DeclarationBuilder builder,
  ) async {
    var args = function.positionalParameters;
    var resultType = function.returnType;
    
    // идентификатор функции print
    var print = await builder.resolveIdentifier(_dartCore, 'print');
    var code = DeclarationCode.fromParts([
      resultType.code,
      ' mul${function.identifier.name}',
      '(',      
      for (var field in args)
        RawCode.fromParts([
          field.type.code,
          ' ',
          field.identifier,
          ', '
        ]),
      '){\n',
      '\n',
      'var result = ${function.identifier.name}(',
      ...args.map((field) => '${field.identifier.name}, '),
      ');\n',
      'result = result*$scalar; \n',
      print,
      "('Result: \${result}');\n",
      'return result;\n',
      '}\n'
    ]);

    builder.declareInLibrary(code);
  }
}


// bin/macro_function.dart
import 'package:macro_function/macro_function.dart';
import 'package:macro_function/macro_constructor.dart';

@DefaultConstructor()
class Batman {
  // без изменений
}

@Mul(2)
int add(int a, int b) => a + b;

void main() {
  print(muladd(4, 5)); // Result: 18 18
  var batman = Batman('Bruce', 30);
  print(batman); // Batman(name: Bruce, age: 30)
}

В итоге макрос сгенерирует следующий код:

prefix0.int muladd(prefix0.int a, prefix0.int b, ){
	var result = add(a, b, );
	result = result*2; 
	prefix0.print('Result: ${result}');
	return result;
}

На текущий момент времени реализация макроса с использованием интерфейса FunctionDefinitionMacro возможна только через костыли. А точнее, мы должны будем в макросе повторно написать часть функционала той функции, которую аннотируем или полностью заменить ее реализацию. Это связано с тем, что в механизм аугментации пока не поддерживает функцию augmented, через которую должна вызываться аннотируемая макросом функция. Поэтому в качестве примера реализуем макрос, который поменяет в функции add сложение на умножение.

// lib/macro_function.dart
macro class MulDef implements FunctionDefinitionMacro {
  const MulDef();
  
  @override
  FutureOr<void> buildDefinitionForFunction(
    FunctionDeclaration function, 
    FunctionDefinitionBuilder builder,
  ) {
    var args = function.positionalParameters;
    // добавляем код
    builder.augment(FunctionBodyCode.fromParts([
      '{\n',
      '\tvar result = ',
      ...args.map((field) => '${field.identifier.name} *'),
      '1;\n',
      '\treturn result;\n',
      '}',
    ]));
  }
}

// bin/macro_function.dart
import 'package:macro_function/macro_function.dart';
import 'package:macro_function/macro_constructor.dart';

@DefaultConstructor()
class Batman {
  // без изменений
}

@MulDef()
int add(int a, int b) => a + b;

void main() {
  print(muladd(4, 5)); // 20, а не 9
  var batman = Batman('Bruce', 30);
  print(batman); // Batman(name: Bruce, age: 30)
}

Обратите внимание, что для добавления кода в дополняемую функцию экземпляр FunctionDefinitionBuilder предоставляет только метод augment, тем самым подменяя исходную реализацию функции add:

augment prefix0.int add(prefix0.int a, prefix0.int b, ) {
	var result = a *b *1;
	return result;
}

Под конец разберем несколько примеров с классами. Допустим, мы хотим посредством макроса добавлять зависимость класса от какого-то интерфейса, чтобы программист после аннотации должен был реализовать его методы. Для начала в директории «lib»  создадим файл «macro_class.dart», добавив в него  интерфейс и макрос. На этот раз будем использовать ClassTypesMacro:

// lib/macro_class.dart
import 'dart:async';

import 'package:macros/macros.dart';

abstract interface class HelloInterface{
  void hello();
}


macro class Hello implements ClassTypesMacro {

  const Hello();
  @override
  FutureOr<void> buildTypesForClass(
    ClassDeclaration clazz, 
    ClassTypeBuilder builder,
  ) async{
      // путь до библиотеки с интерфейсом
      var iLibrary = Uri.parse(
      'package:macro_function/macro_class.dart',
    );
    var interfaces = NamedTypeAnnotationCode(
        name:await builder.resolveIdentifier(
          iLibrary, 'HelloInterface',
        ),
    );
    // добавляем зависимость класса от интерфейса
    builder.appendInterfaces([interfaces]);
  }
}

После аннотации этим макросом класса Batman из файла «bin/macro_function.dart», появится красное подчеркивание, требующее переопределить интерфейсный метод hello:

// bin/macro_function.dart
import 'package:macro_function/macro_function.dart';
import 'package:macro_function/macro_constructor.dart';

@DefaultConstructor()
class Batman {
  final String name;
  final int age;

  @override
  String toString() {
    return 'Batman(name: $name, age: $age)';
  }
}

@Pow2()
int add(int a, int b) => a + b;

void main() {
  print(pow2add(4, 5)); // 81
  var batman = Batman('Bruce', 30);
  print(batman); // Batman(name: Bruce, age: 30)
}
Аннотирование класса несколькими макросами
Аннотирование класса несколькими макросами

 Добавив в класс переопределение требуемого метода можно запустить приложение:

// bin/macro_function.dart
import 'package:macro_function/macro_function.dart';
import 'package:macro_function/macro_constructor.dart';
import 'package:macro_function/macro_class.dart';

@Hello()
@DefaultConstructor()
class Batman {
  final String name;
  final int age;

  @override
  String toString() {
    return 'Batman(name: $name, age: $age)';
  }
  
  @override
  void hello() {
    // TODO: implement hello
  }
}

@MulDef()
int add(int a, int b) => a + b;

void main() {
  print(add(4, 5)); // 20
  var batman = Batman('Bruce', 30);
  print(batman); // Batman(name: Bruce, age: 30)
}

Последним примером избавим несчастных от написания геттеров и сеттеров для приватных полей класса. Для этого добавьте в конец файла «lib/macro_class.dart» пару макросов, реализующих интерфейс FieldDeclarationsMacro:

// lib/macro_class.dart
macro class Get implements FieldDeclarationsMacro { // геттер
  const Get();

  @override
  Future<void> buildDeclarationsForField(
      FieldDeclaration field, 
      MemberDeclarationBuilder builder,
   ) async {
        
    final name = field.identifier.name;
    if (!name.startsWith('_')) {
      throw ArgumentError('@Get can only annotate private fields');
    }
    var publicName = name.substring(1);
    var getter = DeclarationCode.fromParts(
        [
          field.type.code, 
          ' get $publicName => ', 
          field.identifier, 
          ';',
        ]);
    builder.declareInType(getter);
  }
}

macro class Set implements FieldDeclarationsMacro { // сеттер
  const Set();

  @override
  Future<void> buildDeclarationsForField(
      FieldDeclaration field, 
      MemberDeclarationBuilder builder,
  ) async {
        
    final name = field.identifier.name;
    if (!name.startsWith('_')) {
      throw ArgumentError('@Set can only annotate private fields');
    }
    var publicName = name.substring(1);

    var print = await builder.resolveIdentifier(
          Uri.parse('dart:core'), 'print',
        );
    var setter = DeclarationCode.fromParts([
      'set $publicName(',
      field.type.code,
      ' val) {\n',
      print,
      "('Setting $publicName to \${val}');\n",
      field.identifier,
      ' = val;\n}',
    ]);
    builder.declareInType(setter);
  }
}

К сожалению, при обмазывании класса Batman еще большим количеством макросов, у меня начинаются глюки с анализатором. Поэтому перейдем в файл «bin/macro_function.dart», добавим класс Superman с одним приватным полем и аннотируем его реализованными макросами:

// bin/macro_function.dart
import 'package:macro_function/macro_function.dart';
import 'package:macro_function/macro_constructor.dart';
import 'package:macro_function/macro_class.dart';

@Hello()
@DefaultConstructor()
class Batman {
  // без изменений
}

@MulDef()
int add(int a, int b) => a + b;

class Superman {
  final String name;
  final int age;

  @Get()
  @Set()
  int _money;

  Superman(this.name, this.age, this._money);

  @override
  String toString() {
    return 'Superman(name: $name, age: $age)';
  }
}

void main() {
  print(add(4, 5)); // 20
  var batman = Batman('Bruce', 30);
  print(batman); // Batman(name: Bruce, age: 30)

  var superman = Superman('Clark', 40, 100);
  print(superman); // Superman(name: Clark, age: 40, money: 100)
  print(superman.money); // 100
  superman.money = 200;
  print(superman.money); // 200
}

Итоги знакомства с макросами

Что могу сказать по итогам своего знакомства с макросами? Штука крутая, но ее текущая реализация и те проблемы, которые встречаются при обмазывании класса большим количеством макросов (отваливается анализатор, тупит IDE и т.д.), заставляют сомневаться на счет полноценного релиза в начале 2025 года. Пока складывается ощущение, что макросы сильно сопротивляются в процессе их затаскивания в Dart. Так, например, изначально их анонс и предоставление разработчикам пакетов для «поиграться», должен был состояться в Dart 3.3, но его существенно сдвинули…

Отдельно стоит отметить механизм аугментации и открываемые им возможности разделения реализаций класса, функции и т.д. как внутри одного файла, так и по нескольким, что можно использовать для более удобного структурирования кода в проектах. Но это в теории... а на практике, пока реализованная функциональность не дотягивает даже до полшишечки от описанного в спецификации.

В любом случае, держу свои кулачки за рассмотренные в статье нововведения экспериментального режима Dart и надеюсь на лучшее!!!

Что там с книгой по Flutter?

Последнее время было достаточно всего интересного: выступление на Mobius 2024 Spring, мое назначение на должность руководителя образовательной программы магистратуры на кафедре прикладной информатики ГУАП и много еще чего по мелочи. Прекрасно понимая, что один книгу буду писать оооооочень долго, принял решение сформировать авторский коллектив. Итак, встречайте (в порядке присоединения соавторов):

За прошедший месяц мы утвердили структуру книги и распределили главы. На мне дополнительно одна из самых "интересных ролей" - технический редактор)) Все самые свежие материалы книги, прошедшие редактуру, будут выкладываться в курс на Stepik - Основы Flutter (в разработке). Это позволит нам собирать быструю обратную связь по добавленному материалу и вносить правки. Также пока продумываем возможность различных активностей для обсуждения книги (стримы, коллаборации и т.д.). Одна из них - предложить в комментариях тему сквозного проекта и если авторскому коллективу она понравится (т.е. авторы используют предложенный проект в качестве сквозного для книги) - будет отдельное упоминание человека в графе с благодарностями. Поэтому, если хотите похвастать перед своими друзьями, что тоже приложили руку к "Основам Flutter" - вы знаете, что нужно сделать ;)

Пока авторская электронная версия книги не планируется (будем обсуждать вопрос ближе к окончанию написания), только Stepik с последующим выходом на печатную версию.

P.S. В процессе обсуждения структуры книги, количество тем получилось таким, что было принято решение разбить на 2 части:

  • Основы Flutter. В этой книге будет всё, что нужно знать Flutter разработчику.

  • Продвинутый уровень Flutter (предварительное название). Сюда перекочует более углубленное изучение фреймворка, архитектура, подходы, пакеты и так далее.

Репозиторий с примерами из статьи

Мой канал в Telegram-канал

Курс на степике "Основы Flutter"

Курс на степике "Основы Dart"

Комментарии (2)


  1. undersunich
    27.06.2024 12:35
    +1

    После прочтения статьи появилось убеждение что все эти "аугментации" это эксперименты какого то аспиранта.Язык явно усложняется и пропадает его былая красота


    1. MADTeacher Автор
      27.06.2024 12:35

      Это вы еще некоторые предложения к спецификации Dart не видели :D

      Там, порой, такое можно встретить, что волосы дыбом встают >_<