Тривиль — вопрос про стандартные функции

Стандартные функции, увы, нужны. Например, для дин массивов (slices) в Го есть функции: len, cap (capacity), make, append и copy, работающие с любыми слайсами (независимо от типа элемента). Необходимый минимум — это len, make, append.

Вопрос в том, как их добавить в язык? Обычный способ (Оберон, Го) — встроить обработку в компилятор (compiler magic) и реализовать в run-time.

В этой подходе, есть одна проблема — Лень. Та, что двигатель прогресса. Во-первых, встраивать — это надо делать. Во-вторых, если не встраивать, то будет проще менять их имена, добавлять/удалять. А это очень удобно для прототипирования языка.

Вариант с библиотечной реализацией тоже есть, даже несколько. Очевидный — это параметризованные функции (generic functions).

Описываем: фн длина<A>(а: А) {}, возможно задаем ограничение (constraint), что это должен быть массив и реализуем. Все замечательно, но это совсем не Тривиль, сложность компилятора в разы возрастает. И начинаются всякие фокусы, смотри C#, Java, Kotlin… и прочие.

Еще один способ — через overloading, но для этого набор типов, к которым применяется функция, должен быть ограничен.

Итого, известные варианты:

  1. встроить в компилятор (Go)
  2. обобщенные типы, возможно, с добавкой перегрузки (много где)

Можно ли придумать что-нибудь достаточно простое для понимания и с тривиальной реализацией?

Перехожу в режим бреда (или мозгового штурма).

Вариант 3. Над-типы

Добавляем в язык тип «Любой дин массив» (Любой вектор?) и разрешаем использовать его в параметрах функции и, может быть, где-то еще (пока не важно). Пишем:

фн длина(а: Любой вектор)  @внешняя{имя: «vectorlen»}

И так как любой дин массив, это дескриптор, то реализация на Си тривиальна.

Правда если подумать о том, что длина бывает еще у строк и у статических массивов, становится понятно, что решение не достаточно. Можно, конечно, задать разные имена для длин, применимых к разным типам, но как-то не хочется.

Вариант 4. Над-типы + параметризация + ограниченная перегрузка

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

мульти фн длина<А>(а: А): Цел

настройка фн длина<А=Строка> @внешняя{имя: «stringlen»}

настройка фн длина<А=Любой вектор> @внешняя{имя: «vectorlen»}

Разрешаем делать настройку только в том же модуле, в котором определена мульти-функция (чтобы было попроще), делаем модуль и импортируем его (явно или неявно). Вуаля, есть длина.

Правда, остается куча вопросов:

  • Как, например, написать сигнатуру функции append? (делать что-то вроде Swift associated types?)
  • Что делать со статическим массивом, для которого мы ожидаем, что длина есть константа?
  • Что делать, если захочется поддержать многомерные статические массивы? Как записывать длину второго измерения?

Кажется, что Лень уже на стороне встроенных функций. Но, может быть, есть еще варианты?

Вариант 5. ???

 

10 комментариев


  1. Всё это не то. Массивы, срезы, словари т. п. — всё это структуры данных. не надо их тащить в сам язык. Их нужно оставить в системной библиотеке и делать примерно так:
    длн := имя.Длина();
    ёмк := массив.Ёмкость();

    Можно сделать и через сеттеры/геттеры в рамках объектной модели.
    Имхо ,надо максимально исключать количество форм представления сущностей. Надо стремиться к единообразию и предсказуемости. Например, как в Юникс: всё есть файл.

    Ответить

    1. Я считаю иначе. Достаточно подробно об этом здесь. Если коротко, то базовый набор типов должен быть языке (точнее, во всех языках семейства), включая массивы, дин. массивы, классы, структуры, вариантные типы и интерфейсы. Естественно, я не говорю о Тривиле, так как он тривиль.

      Типы, которые строятся из базовых, например, хеш-таблицы или деревья, должны быть в библиотеках. Возможно, что со временем, такие типы должны втягиваться в языки. Как, например, тип map в Го. Я не считаю этот шаг Го верным, он, скорее, был вынужденным, но он, безусловно, увеличивает простоту программирования на Го.

      Ответить

      1. Целые, байты, дробные — являются фундаментальными типами. Строки, списки, словари, массивы — составные типы. Все составные типы — в библиотеки. Можно сделать послабление: наиболее употребительные составные типы импортировать автоматически (что должно регулироваться настройками в специальном пакете СИСТЕМА.АвтоИмпорт). Но всё-таки, составные типы не должны быть обязательной частью языка.

        Ответить

        1. Тут у нас взгляды отличаются, и это проявляется не первый раз в нашей переписке.

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

          По сути, для всех, кто пишет прикладные вещи, язык — это ядро + библиотеки. И они могут и не разделять два эти уровни языка.

          Исходя из этого подхода, происходит выбор. Например:

          Задача: Сделать Тривиль — это язык (+ компилятор +библиотеки +toolchain) для удобного (простого) написания компилятора(ов) следующих, не тривиальных, языков.
          Из задачи следует многое чего, но в том числе: ядро + библиотеки должны быть удобны для написания компилятора.

          И далее, например, без форматного вывода мне неудобно: а) отлаживаться б) выдавать текст на Си.
          Что мне нужно, чтобы просто написать удобную библиотеку форматного вывода строк?
          — строки (это понятно)
          — дин. массивы (нужен динамический массив байтов для UTF-8)
          — классы (нужна удобная абстракция)
          — вариадик параметры (понятно)
          — минимальная рефлексия (надежность и безопасность)

          И далее смотрим, что из этого ядро (то есть делается компилятором), а что библиотеки. Мой ответ такой: рефлексия — частично компилятор + runtime, частично библиотека. Остальное — компилятор + runtime.

          Ответить

          1. Безусловно — отладочный вывод в консоли — наше всё. И я не предлагаю выкинуть составные типы за пределы базовых возможностей. Я предлагаю ввести регулировку, которая будет делать вид, что «вот это» — встроено в язык. И эта регулировка позволит программисту включать в язык новые типы, или существующие типы, но с расширенным поведением.

            Если мы сейчас говорим про некий простейший язык (L-какой-то там) — тогда вообще надо хорошо подумать, прежде чем реализовывать очередную семантическую возможность. Тут я не владею контекстом в полной мере.


  2. А что касается переопределений, то их (имхо) вообще надо избегать всеми возможными способами.
    сахар := вода + углекислота + ультрафиолет;

    Попробуй догадайся, что творится в этой записи и вообще что такое возможно.

    сахарок := сахар.Сделать(вода, углекислота, ультрафиолет);

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

    Ответить

    1. В целом я согласен — readability first. Но в данном случае, использование слова длина (len) для разных типов не добавляет когнитивных проблем. Более того, оно упрощает программирование в сравнении с подходом, когда для нескольких разных «массивов» используются разные способы (имена) получения длины.

      Ответить

      1. Можно сделать один общий метод .Длина(). Независимо от спрашиваемого типа. По умолчанию для целых (например) — всегда будет возвращаться 1 (т.к. по сути это и есть один элемент). Для списка — будет возвращаться что-то (начиная с нуля). Таким образом можно вообще к типу прицепить что угодно:
        .Размер()
        .Указатель()
        .Ссылка()
        .Тип()
        .Поток()
        .Процесс()
        и т. п. И такой подход не обязывает _всё_ тащить в компилятор.

        Ответить

          1. Чтобы не тащить эти функции в сам язык. А в типе может быть что угодно.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *