Voldemort типы в D

  • Tutorial
Данный пост расскажет об уникальной фишке D — Voldemort типы. Типы, которые можно использовать, но нельзя назвать. Данное название не очень подходит им, но Walter Bright очень любит так их называть. Voldemort типы очень часто встречаются в стандартной библиотеке Phobos, особенно в модулях std.algorithm и std.array. Осваивающие D могут часами штудировать документацию в поисках типа, возвращаемого из splitter или joiner, а возвращают они именно Voldemort типы. После этого поста можно смело открывать исходники std.algorithm, ибо никакие Сами-Знаете-Кто вам будут не страшны.

Он самый

Иногда, взаимодействие существующих возможностей может привести к неожиданным сюрпризам. Мне нравится считать, что мы изначально заложили Voldemort типы в D, но на самом деле они были найдены Андреем Александреску. Что это за Voldermort типы? Читайте дальше.



Во-первых, немного вводной информации. Для понимания материала необходимо иметь представление о Ranges. В первом приближении, они были придуманы как замена итераторам и очень схожи с подходом C# с его интерфейсом IEnumerable. В языке D любой тип, реализующий следующие методы, будет представлять из себя InputRange:
front - get the first element of the range
popFront - remove the first element of the range
empty - are there more elements in the range?


Эти методы реализуют основу для итерации по типу. Теперь just fo fun, давайте спроектируем InputRange, который вернет бесконечную последовательность случайных чисел. (Мы также называем такие функции генераторами.)

Он может выглядеть так (не очень хороший генератор случайных чисел, но скоро им станет):
module rnd;
 
struct RandomNumberGenerator {
 
    this(uint seed) {
        next = seed;
    popFront();  // get it going
    }
 
    @property int front() {
    return ((next / 0x10000) * next) >> 16;
    }
 
    void popFront() {
    next = next * 1103515245 + 12345;
    }
 
    @property bool empty() { return false; }
 
  private:
    uint next;
  }


И функция, которая вернет его:
RandomNumberGenerator generator(uint seed) {
  return RandomNumberGenerator(seed);
}


И прекрасная программа, которая напечатает 10 таких чисел:
import std.stdio;
import rnd;
 
void main() {
  int count;
  foreach (n; generator(5)) {
      writeln(n);
      if (++count == 10)
      break;
  }
}


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

Я бы мог отметить его атрибутом private и другие модули, кроме rnd, не смогли бы получить к нему доступ. Но этот тип все еще здесь, вне зоны, которой он принадлежит, и другие члены модуля все еще могут обращаться к нему, запривачен он или нет (в D private объявления не спрятаны от других объявлений внутри одного модуля).

Перейдем теперь к более веселым вещам. Сперва, D поддерживает вывод типов для объявлений, поэтому я могу написать так:
auto g = RandomNumberGenerator(seed);


И g будет автоматически присвоен тип RandomNumberGenerator. Это стандартная вещь. Подергав эту ниточку еще немного, и мы можем выводить возвращаемые из функций типы:
auto square(double d) {
  return d * d;
}
 
auto x = square(2.3);


И компилятор поймет, что функция square вернет double, так как это тип выражения после return. И конечно, переменная x также будет типа double. Теперь давайте перепишем нашу функцию для генератора таким образом:
module rnd;
 
auto generator(uint seed) {
 
  struct RandomNumberGenerator {
 
  @property int front() {
      return ((seed / 0x10000) * seed) >> 16;
  }
 
  void popFront() {
      seed = seed * 1103515245 + 12345;
  }
 
  @property bool empty() { return false; }
  }
 
  RandomNumberGenerator g;
  g.popFront();    // get it going
  return g;
}


Произошло что-то обворожительное. RandomNumberGenerator стал типом, который находится внутри области функции generator. Его просто не видно вне функции. Также он не может быть назван — это и есть Voldemort тип.

Мы можем только получить экземпляр этого типа:
auto g = generator(5);


И дальше использовать g. Я знаю о чем вы думаете — использовать typeof и создать другой экземпляр RandomNumberGenerator:
auto g = generator(4);
typeof(g) h;


Sorry, это не сработает, компилятор не позволит объявить Voldermort тип вне его области видимости (техническая причина — нет доступа к локальной переменной seed).

Теперь осталась только одна деталь, которая меня раздражает, цикл:
int count;
foreach (n; generator(5)) {
    writeln(n);
    if (++count == 10)
    break;
}


Он выглядит так старомодно. С помощью ranges, мы можем обойтись без толстого цикла, и вместо него использовать range take , чтобы просто взять первые 10 элементов этого range:
void main() {
  foreach (n; take(generator(5), 10))
  writeln(n);
}


И дальше использовать writeln, чтобы совсем избавиться от цикла:
void main() {
  writeln(take(generator(5), 10));
}


Являются ли Voldemort типы действительно только existential типами?


Дан тип T, и тип U, который может быть извлечен из T и который состоит в определенном отношении c T, является existential типом.

Для примера, если у нас есть тип T, являющийся указателем, мы можем вывести из него базовый existential тип U с помощью:
import std.stdio;
 
void extractU(T)(T t) {
  static if (is(T U : U*))
    writefln("type %s is a pointer to an %s", typeid(T), typeid(U));
  else
    writefln("type %s is not a pointer", typeid(T));
}
 
void main() {
  int* t;
  extractU(t);
  double d;
  extractU(d);
}


Что выведет на экран:
type int* is a pointer to an int
type double is not a pointer


В то время как Voldemort типы безусловно прячут свою реализацию, это, однако, не делает их existential типами. Так как Voldemort типы не могут быть получены из некого über типа, они не являются existential.

Заключение

Voldemort типы стали замечательным открытием в D, что позволило инкапсулировать типы так, что их можно использовать, но нельзя назвать.

Источник: Walter Whight Voldemort Types In D
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 27

    +3
    Неплохое решение проблемы того, что класс-значение не обладает полиморфизмом (необходимы указатели) — который у языков типа Java|C# собственно и нет — там просто возвращается интерфейс.

    Но у конкретно этого подхода есть проблема — возвращаемый тип не указывается, что неприятно с точки документации кода (чтобы определить тип приходится погружаться в исходный код функции). Мне кажется было бы лучше писать что-то типа? extends Iterable (а-ля шаблоны с ограничениями) чтобы с одной стороны, было указание типов, с другой — сохранился бы статический полиморфизм.
      0
      В 99% случаев использования, возвращается тип, реализующий range, пользователь не должен задумываться какой именно это range, почти все функции в Phobos работают с range'ами. В остальных экзотических случаях, да, придется явно описать в документации какой интерфейс реализует скрытый тип. Идея с шаблонами — статическими интерфейсами очень интересна, думаю можно реализовать это с помощью рефлексии как library solution.

      Также можно поизвращаться и через рефлексию считать все поля и методы Voldemort типа и сконструировать свой (через mixin), а заодно функцию, которая превращает экземпляр Voldemort типа в кастомный. В общем, огромное поле для ненормального программирования.
        +1
        >В 99% случаев использования, возвращается тип, реализующий range, пользователь не должен задумываться какой именно это range

        Так я же говорю об интерфейсе, а не о реализации. Информация о том что структура итерабельна — оно должно быть в шапке функции, рефлекшн тут вообще не причем, я про читабельность и поддерживаемость кода говорю.
          0
          На самом деле синтаксических средств для такой маркировки возвращаемого типа в D не существует, а структуры не могут реализовывать интерфейсы.
        0
        В D все классы — полиморфные референсные типы, как и в C#. Voldemort types это, как правило, структуры.
        Соответственно, можно было бы вернуть интерфейс, но это было бы чертовски неудобно, ведь стандартная библитека практически не использует OOP, предпочитая статический полиморфизм через duck typing и шаблоны. Во всех случаях, когда используется Voldemort type, пользователь _не должен_ знать точного типа, с которым работает, это преднамеренно.
          +1
          >предпочитая статический полиморфизм через duck typing и шаблоны.

          Вот это мне кажется и плохо. В том смысле что duck typing конкретно здесь — не нужен (и вообще очень редко нужен). Мне как раз всегда было обидно, что статический полиморфизм и динамический разделяют — хотя логически между ними нет разницы.
            0
            Между ними огромная разница практически, а D — практичный язык. Динамический полиморфизм очень сильно бьёт по производительность за счёт лишнего indirection. Статический — за счёт template bloat. В целом нужно аккуратно выбирать, что из этого хуже. Специфика работы с range-типами располагает к первому варианту.

            Более того, динамический полиморфизм в базовых частях стандартной библиотеки добавил бы ещё одну причину, по которой её нельзя использовать в embedded & Co, а это и так болезненная тема для Phobos.
              +1
              >Между ними огромная разница практически, а D — практичный язык.

              Вы меня не поняли. Я прекрасно знаю чем отличается динамический от статического полиморфизма, вы не на тот вопрос отвечаете. Меня расстраивает, что эти два типа полиморфизма так сильно различают синтаксически, в то время как можно было свести разницу к миниму, сохранив преимущества статической явной типизации и в статическом полиморфизме.
                +1
                Делать их слишком схожими синтаксически опасно как раз таки из за различий «под капотом». Я довольно давно уже пытаюсь протолкнуть идею разрешить использовать interface в качестве constraint, в духе Rust traits, благо это сделать не очень трудно. Тогда можно было бы разрешить синтаксис в духе auto!InputRange, аналогично для параметров функций. Однако это предложение пока что не вызвало особо энтузиазма, а на pull request меня не хватает.
                  0
                  Такая фича сделала бы много пользы, например, я столкнулся с проблемой проверки наличия определенных методов у структур при реализации compile-time бекэндов для велосипедного сериализатора. Это можно сделать и специальным шаблоном, который в guarding выражениях проверит наличие методов, но через такой синтаксический сахар намного меньше monkey job.
                    0
                    Ну разница в сахаре между
                    void func(T : InputRange)(T range)
                    {
                    ...
                    

                    и
                    void func(T)(T range)
                        if (Implements!(T, InputRange))
                    {
                    ...
                    

                    не очень велика, но сообщение об ошибке в первом варианте будет намного понятнее.
                      0
                      Первый случай не пройдет со структурами, а Implements я нигде не могу найти в Phobos, а вроде очень полезная вещь.
                        +1
                        Я как раз и предлагал расширить первый случай для структур в контексте duck typing, возможно, с другим спецификатором вместо ":". Увы, не пошло :)

                        Пример реализации Implements из личных запасов — dpaste.1azy.net/6d8f2dc4. Хочу предложить в Phobos, но нужно оформить в более строгом виде, стандартная библиотека всё-таки.
              0
              Логически между ними довольно большая разница — ограничения сильно разные. Разницы практически нет в языках с тайпклассами, но только за счет того, что и на динамику, и на статику есть довольно сильные ограничения.
          0
          Стоит отметить, что подобные типы не вполне «призрачные», их можно получить и инстанциировать вне функции через typeof, никаких дополнительных преград кроме синтаксического неудобства тут не ставится.
            0
            Если только в этих структурах нет замыканий, иначе будет ошибка компиляции. В посте было про этот «хак».
              +1
              А, пардон, просмотрел. Я к тому, что в сообществе до сих пор нет согласия насчёт того, как компилятор должен себя вести в этом случае, как это часто и бывает со случайно открытыми фичами. Поэтому не стоит полагаться на такое поведение :)
            +4
            Было интересно прочитать. Пишите еще про D. Уже давно заглядываюсь на него, как на замену C++.
              +1
              Это жестоко на ночь такие фотки людям показывать!
                +3
                Я смотрю, Господин продолжает вселять ужас в сердца маглов :)
                0
                Такое захватывающее название, а по сути-то?

                В рантайме это решается интерефейсами (Iterable), в компайл-тайме — меткой «unspecified-type» в документации (например), чего вполне достаточно. От совсем буратин можно спрятать конструктор вспомогательного класса, чтобы не конструировали чего не надо, но мне это кажется уже излишним.
                  +2
                  в компайл-тайме — меткой «unspecified-type» в документации (например), чего вполне достаточно.


                  Так речь идет о внезапной возможности решить проблему более элегантным способом, итого из пространства имен целиком убираются вспомогательные вещи, которые в идеале должны быть доступны только реализации.
                  +1
                  Лямбды в С++ тоже можно назвать voldemort типами
                    +1
                    > Типы, которые можно использовать, но нельзя назвать.
                    А я, по названию, подумал сначала о подсчете ссылок перед убийством =)
                      +1
                      А в C++ мы делаем так:

                      auto rand_generator(unsigned seed)
                      {
                          return [=]() mutable
                          {
                              seed = seed * 1103515245 + 12345;
                              return ((seed / 0x10000) * seed) >> 16;
                          };
                      }
                      
                      int main()
                      {
                          vector<unsigned> numbers;
                          generate_n(back_inserter(numbers), 10, rand_generator(1337));
                      }
                      
                        0
                        По сути здесь тоже имеет место использование анонимных типов, которые можно использовать, но нельзя назвать :)
                          0
                          Давно не работал с C++, но скомпилировать получилось вот такой вариант:
                          #include <vector>
                          #include <algorithm>
                          #include <iostream>
                          
                          using namespace std;
                          
                          auto rand_generator(unsigned seed) -> function<unsigned (void)>
                          {
                            return [=]() mutable
                            {
                              seed = seed * 1103414245 + 12345;
                              return ((seed /0x10000) * seed) >> 16;
                            };
                          }
                          
                          int main()
                          {
                            vector<unsigned> numbers;
                            generate_n(back_inserter(numbers), 10, rand_generator(1337));
                          
                            for( vector<unsigned>::const_iterator i = numbers.begin(); i != numbers.end(); ++i)
                              cout << *i << ' ';
                          
                            return 0;
                          }
                          


                          Без «trailing return type» ни gcc 4.8, ни vs2012 компилировать не хочет. Также хочу узнать, существует ли способ вывести вектор на экран лучше, чем лобовым циклом?

                          Просто для сравнения, аналогичная программка на D, просто для сравнения синтаксиса языков, по мне C++11 лямбды очень многословны:
                          import std.range;
                          import std.stdio;
                          import std.algorithm;
                          
                          auto rand_generator(uint seed)
                          {
                          	return ()
                          	{
                          		seed = seed * 1103414245 + 12345;
                          		return ((seed / 0x10000) * seed) >> 16;
                          	};
                          }
                          
                          int main()
                          {
                          	writeln( repeat(rand_generator(1337), 10).map!(x => x()) );
                          	return 0;
                          }
                          


                          Да, новые лямбды в C++11 можно назвать Voldemort типами. Однако в D они обычно являются структурами, с ними удобнее работать, чем с лямбдами, и структуры не выделяют память в куче.

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое