D как улучшенный C

Original author: Уолтер Брайт (Walter Bright)
  • Translation

Уолтер Брайт — «великодушный пожизненный диктатор» языка программирования D и основатель Digital Mars. За его плечами не один десяток лет опыта в разработке компиляторов и интерпретаторов для нескольких языков, в числе которых Zortech C++ — первый нативный компилятор C++. Он также создатель игры Empire, послужившей основным источником вдохновения для Sid Meier’s Civilization.


Цикл статей о Better C

Язык D был с самого начала спроектирован так, чтобы легко и напрямую обращаться к C и, в меньшей степени, C++. Благодаря этому в нём доступны бесчисленные C-библиотеки, стандартная библиотека C и конечно же системные API, которые как правило построены на API языка C.


Но C — это не только библиотеки. На C написаны многие большие и неоценимо полезные программы, такие как операционная система Linux и значительная часть программ для неё. И хотя программы на D могут обращаться к библиотекам на C, обратное неверно. Программы на C не могут обращаться к коду на D. Невозможно (или по крайней мере очень сложно) скомпилировать несколько файлов на D и слинковать их в программу на C. Проблема в том, что скомпилированные файлы на D могут обращаться к чему-то, что существует только в рантайме D, а добавлять его в линковку обычно оказывается непрактично (рантайм довольно объёмный).


Также код на языке D не может существовать в программе, если D не контролирует функцию main(), потому что именно так происходит запуск рантайма D. Поэтому библиотеки на D оказываются недоступны для программ на C, а программы-химеры (смесь C и D) становятся непрактичными. Нельзя взять и «просто попробовать» язык D, добавляя модули на D в существующие модули программы на C.


Так было до тех пор, пока не появился Better C.


Это всё уже было, идея не новая. Бьёрн Страуструп в 1988 году написал статью под названием A Better C. Его ранний компилятор C++ мог компилировать код на C почти без изменений, и можно было начать использовать возможности C++ тут и там, где это имело смысл — не жертвуя существующими наработками на C. Это была блестящая стратегия, обеспечившая ранний успех C++.


Более современный пример — Kotlin, в котором использовался другой подход. Синтаксически Kotlin не совместим с Java, однако он обеспечивает двустороннее взаимодействие с существующими Java-библиотеками, что позволяет постепенно мигрировать с Java на Kotlin. Kotlin — действительно «улучшенная Java», и его успех говорит за себя.


D как улучшенный C


D использует кардинально другой подход для создания улучшенного C. Это не надстройка над C, не надмножество С, и он не тащит за собой давние проблемы C (такие как препроцессор, переполнение массивов и т.д.). Решение D — это создание подмножества языка D, из которого убраны возможности, требующие инициализирующего кода и рантайма. И сводится оно к опции компилятору -betterC.


Остаётся ли урезанная версия D языком D? На это не так просто ответить, и на самом деле это лишь вопрос личных предпочтений. Основная часть языка остаётся на месте. Однозначно остаются все свойства, аналогичные C. В результате мы получаем язык, промежуточный между C и D.


Что убрано


Очевидно, что убран сборщик мусора, а вместе с ним — возможности, которые от него зависят. Память всё ещё можно выделять точно так же, как и в C: при помощи malloc или собственного аллокатора.


Классы C++ и COM всё ещё будут работать, но полиморфные классы D — нет, поскольку они полагаются на сборщик мусора.


Исключения, typeid, статические конструкторы и деструкторы модулей, RAII и юниттесты убраны. Но возможно, мы придумаем, как их вернуть.


На сегодняшний день в Better C уже стали доступны RAII и юниттесты. (прим. пер.)

Проверки assert изменены, чтобы использовать библиотеку C вместо рантайма D.


(Это неполный список, см. спецификацию Better C).


Что осталось


Гораздо более важно, что может предложить Better C по сравнению C?


Программистов на C в первую очередь могут заинтересовать безопасность доступа к памяти в виде проверок границ массивов, запрет на утечку указателей из области видимости и гарантированная инициализация локальных переменных. Далее следуют возможности, которые ожидаются от современного языка: модульность, перегрузка функций, конструкторы, методы, Юникод, вложенные функции, замыкания, выполнение функций на стадии компиляции (Compile Time Function Execution, CTFE), генератор документации, продвинутое метапрограммирование и проектирование через интроспекцию (Design by Introspection, DbI).


Генерируемый код


Возьмём следующую программу на С:


#include <stdio.h>

int main(int argc, char** argv) {
    printf("hello world\n");
    return 0;
}

Она скомпилируется в:


_main:
push EAX
mov [ESP],offset FLAT:_DATA
call near ptr _printf
xor EAX,EAX
pop ECX
ret

Размер исполняемого файла — 23 068 байт.


Перенесём её на D:


import core.stdc.stdio;

extern (C) int main(int argc, char** argv) {
    printf("hello world\n");
    return 0;
}

Размер исполняемого файла тот же самый: 23 068 байт. Это неудивительно, потому что и компилятор C, и компилятор D генерируют один и тот же код, поскольку используют один и тот же генератор кода. (Эквивалентная программа на полноценном D занимала бы 194 Кб). Другими словами, вы ничего не платите за использование D вместо C при аналогичном коде.


Но Hello World — это слишком просто. Возьмём что-то посложнее: пресловутый бенчмарк на основе решета Эратосфена:


#include <stdio.h>

/* Eratosthenes Sieve prime number calculation. */

#define true    1
#define false   0
#define size    8190
#define sizepl  8191

char flags[sizepl];

int main() {
    int i, prime, k, count, iter;

    printf ("10 iterations\n");
    for (iter = 1; iter <= 10; iter++) {
        count = 0;
        for (i = 0; i <= size; i++)
            flags[i] = true;
        for (i = 0; i <= size; i++) {
            if (flags[i]) {
                prime = i + i + 3;
                k = i + prime;
                while (k <= size) {
                    flags[k] = false;
                    k += prime;
                }
                count += 1;
            }
        }
    }
    printf ("\n%d primes", count);
    return 0;
}

Перепишем на Better C:


import core.stdc.stdio;

extern (C):

__gshared bool[8191] flags;

int main() {
    int count;

    printf("10 iterations\n");
    foreach (iter; 1 .. 11) {
        count = 0;
        flags[] = true;
        foreach (i; 0 .. flags.length) {
            if (flags[i]) {
                const prime = i + i + 3;
                auto k = i + prime;
                while (k < flags.length) {
                    flags[k] = false;
                    k += prime;
                }
                count += 1;
            }
        }
    }
    printf("%d primes\n", count);
    return 0;
}

Выглядит почти так же, но кое-что следует отметить:


  • Приписка extern(C) означает использование соглашения о вызове из языка C.
  • D обычно хранит статические данные в локальном хранилище потока (thread-local storage, TLS). C же хранит их в глобальном хранилище. Аналогичного поведения мы достигаем при помощи __gshared.
  • Инструкция foreach — более простой способ пройтись циклом по известному промежутку.
  • Использование const даёт знать читателю, что prime никогда не изменится после инициализации.
  • Типы iter, i, prime и k выводятся автоматически, уберегая от ошибок с непредвиденным приведением типов.
  • За количество элементов в flags отвечает flags.length, а не какая-то независимая переменная.

Последний пункт ведёт к скрытому, но очень важному преимуществу: при обращении к массиву flags происходит проверка его границ. Больше никаких ошибок из-за выхода за границы массива! И нам для этого даже не нужно было ничего делать.


И это только верхушка айсберга всех возможностей Better C, которые позволят вам улучшить выразительность, читабельность и безопасность ваших программ на C. К примеру, в D есть вложенные функции, и по моему опыту, они практически не оставляют мне поводов прибегать к запретной технике goto.


От себя лично могу сказать, что с тех пор, как появилась опция -betterC, я начал переводить на D многие мои старые, но всё ещё используемые программы — по одной функции за раз. Работая по одной функции и запуская набор тестов после каждого изменения я постоянно сохраняю программу в рабочем состоянии. Если что-то сломалось, мне нужно проверить только одну функцию, чтобы найти причину. Мне не очень интересно дальше поддерживать свои программы на C, и с появлением Better C для этого больше нет причин.

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 89

    0

    Интересно, для начала, узнать размер оверхеда от проверки всех массивов на выход за границы.
    Сахарок с auto, бесспорно, хорош, как и foreach.
    Про TLS не очень понятно, ну и extern C тоже.

      +1

      По умолчанию все проверки убираются из релизной версии кода, кроме функций, помеченных атрибутом @safe. Но также есть опция компилятора -boundscheck, которая позволяет настроить желаемое поведение.

        0

        TLS — это отдельная область памяти для каждого потока. Эту память можно безопасно использовать из своего потока, а передавать ссылки на неё в другой поток компилятор не даст, если она не защищена мьютексом или ещё как-либо не обеспечена безопасность многопоточной работы. В данном случае программа однопоточная, так что не очень понятно зачем выключать TLS.

          0
          Но она не дармовая.
            0

            Для однопоточного приложения никакой разницы.

              0
              Для однопоточного оно и не надо
        –9
        А чем данный подход лучше C-расширений для Python?
        Другой синтаксический сахар?
          +4
          В Питоне — интерпретатор и сборка мусора.
          Тут — компилятор и нет сборки мусора.
            0
            Это я понимаю. Просто какая выгода использовать не язык Си?
            Делать embedded на D вряд ли кто-то станет.
            Для быстрых параллельных вычислений есть кресты.
            А для прикладных задач работы с данными — очевидный выбор Python.
            Зачем тогда нужен этот betterC? В чем поинт?
              0

              В том, что всё это можно делать на одном языке.

                +3
                Делать эмбеддед в том числе. BetterC обладает скоростью и компактностью С, но надежнее, удобнее — выше уровнем абстракций.
                А еще миграция позволит получить более надежные версии старых С-программ. Если конечно, игра стоит свеч.

                — — — далее уже не уровень BetterC / C -----------------------
                Для быстрых вычислений — чаще всего обязательна многопоточность (применение SIMD считаем равносложным для языков). Здесь сравнивать стоит удобство абстракций параллельного вычисления. Здесь нужен уже полноценный D с его библиотекой параллелизации и упрощения работы при многопоточности

                А Питон — я даже не знаю, почему его хвалят — синдром утенка? Язык-клей, преподающийся в школах, неплохой уровень абстракций, и отвратительная собственная производительность (в т.ч. однопоточность), которая нивелируется только наличием громадного количества С-библиотек для подклеивания.

                Ну да, для хелловорлдов разного уровня, склепать быстренько что-либо одноразовое, в т.ч математических разовых расчетов он хорош, но не более.
            0

            Есть в теме спецы по C++? Я со стороны вижу, что 17 и 20й стандарты сильно поменяли(меняют) язык.
            Но как это изнутри чувствуется? Есть ли потребность в замене c++, если используется наиболее современный стандарт?

              +4
              C++ 20 никак не чувствуется и ещё год, как не почувствуется.
                0

                На проде — это понятно.
                Но всё же.

                  0
                  Да там и без «проды». Компиляторы вроде все ещё в полном объёме стандарт не поддерживают.
                +1
                С каждым стандартом всё лучше и лучше. Вспоминая С++ без лямбд, std::move и прочих плюшек, это, мягко скажем, раздражало.
                С другой стороны, порог входа повышается, новичку уже сложнее разбираться в коде со всеми новомодными прибамбасами. А впереди ещё пугают концептами.
                  0

                  концепты сами собой напрашивались, но синтаксис как обычно сложный.

                  0
                  Эта тема предлагается как замена С. ИМХО, вполне адекватная — легкий путь.

                  Но D уже не позиционируется как замена С++, слишком разные парадигмы в библиотеках. И да, вряд ли кто на такое пойдет. Скорее, как кооперация
                    +2

                    Да, есть. UB и небезопасность никуда не уходят.

                      +1

                      C введением 17 циклы по kv-хранилищам стали нагляднее со структурированными биндингами (прощай boost::adapters), дедукция типов для заметно уменьшила когнитивную нагрузку (std::array {1,2,3,4} сам выведет и тип и размер). Замечательные <optional>, <string_view> и (наконецта!) <filesystem>.
                      C++20 живьем пока не встречал.

                        0
                        Folded expression сильно упростила во многих местах использование variadic template'ов.
                          0

                          У нас нет столько собственных шаблонов, так что конкретно это профита не добавило. std::any по той же причине не попало в мой список — везде boost::any и переход на std::any пока не планируется.

                    • UFO just landed and posted this here
                        0

                        Это просто перевод.

                        –2

                        Не очень понятно преимущество в виде проверок границ массивов и инициализации переменных. На C тоже можно проверять границы массивов и инициализировать абсолютно все переменные. Да, C не позволяет красиво применять оператор [] для доступа к элементу с проверкой. Но тот же CPP с его "перегрузкой" операторов вполне разрешает делать проверку с использованием оператора [], инициализировать все переменные, а также использовать умные указатели для борьбы с утечками памяти.

                          +1

                          D не просто гарантирует инициализацию памяти, но гарантирует отсутствие в ней мусора. Например, в С можно сравнить две, казалось бы, одинаковые структуры, но получить ложь, ибо между полями в структуре из-за выравнивания может оказаться мусор.


                          Вообще, если интересно почитать о преимуществах D перед C, то можете глянуть эту статью: https://habr.com/ru/post/276227/

                            –1
                            Хм-м, это как это вы так «сравниваете» структуры, через memcmp() что ли? Сурово, сурово. А что, если в этих структурах, например, есть элементы с плавающей точкой, особенно с NaN'ами всякими или +0.0/-0.0? Тогда результат такого сравнения будет отличаться от поэлементного. Для поэлементного же сравнения потенциальный мусор в паддинге значения не имеет.

                            P.S. Прочел по вашей ссылке, что в D при сравнении структур «под капотом просто используется быстрое сравнение диапазонов памяти». Надеюсь, что это все-таки не так (ну хотя бы не всегда).

                            P.P.S. Да, нашел сейчас онлайн-компилятор D, проверил — по крайней мере в случае элементов с плавающей точкой вроде бы «тупое» сравнение диапазонов памяти не используется, не считается, что NaN == NaN и так далее.
                              0

                              https://dlang.org/spec/expression.html#equality_expressions


                              У типов с плавающей точкой +0.0 И -0.0 считаются равными, NaN и NaN считаются неравными. Равенство структур проверяется по равенству всех членов, если не перегружен оператор сравнения.

                                0
                                Я про пассаж «в С можно сравнить две, казалось бы, одинаковые структуры, но получить ложь». То, что в D сравнение структур, скорее всего таки не делается через аналог memcmp() (по крайней мере, не всегда), я и так подозревал :)
                                +1

                                В D просто очень умный компилятор. Он, видя, что в структуре есть float, добавляет для неё реализацию xopEquals, которая сравнивает структуры поэлементно.


                                Сам алгоритм сравнения структур описан тут: https://github.com/dlang/druntime/blob/master/src/object.d#L1312

                                  0
                                  Ну да, как я уже сказал — я подозревал, что в D компилятор с этим справится :) Просто C — он… как бы это сказать… язык, не обремененный абстракциями by design. Поэтому если программист таки сравнивает в C структуры через memcmp(), то ему лучше бы в любом случае твердо знать, что он делает, а иначе проблемы гарантированы — будь то проблемы с мусором в паддинге, с элементами с плавающей точкой, с элементами-указателями на некоторых архитектурах (там, где указатель представлен в виде сегмент: смещение) и так далее. Паддинг, короче, не самое страшное при этом :)
                                    0

                                    В C эту инициализацию можно забыть случайно, а потом удивляться почему ничего не работает. В D для её отключения придётся специально постараться именно зная что делаешь.

                                    +1

                                    Впрочем, от memcmp собираются избавиться: https://dlang.org/changelog/2.085.0.html#no-cmpsb

                                  +1

                                  Эм, в C тоже можно сравнивать структуру по элементам. Если нужен синтаксический сахар, то можно использовать CPP, перегрузить оператор сравнения, который будет сравнивать так, как нужно вам.
                                  Если хотите использовать memcmp для сравнения, то C никак вам не запрещает в структурах инициализировать одним значением байты выравнивание (зачастую нулями). Да, на C это будет некрасиво, но если нужен синтаксический сахар, то есть CPP.
                                  Вообще, плох тот язык системного программирования, который по умолчанию делает кучу ненужных вещей. Вещи, вроде, проверки границ массива, инициализации байтов выравнивания должны делаться исключительно там, где в этом есть необходимость.

                                    +1

                                    Зачем использовать CPP, если есть D?
                                    Все эти проверки и инициализации появились не просто так, а как решение самых распространённых уязвимостей.

                                      –1

                                      Затем, что C даёт выбор: я могу инициализировать структуру и тогда автоматически все байты выравнивания занулятся (а также все поля, которые не были указаны при инициализации), могу не инициализировать структуру и тогда ничего зануляться не будет. В D, как я понял, если не делаешь инициализацию сам, то структура сама инициализируется нулями. В итоге будет ряд случаев, когда компилятор не сделает оптимизацию и в рантайме будет выполнен ряд лишних операций. Можно попросить компилятор кидать варнинги (а их отображать как ошибки), когда какая-то переменная не инициализирована, а в местах где инициализация не требуется ставить атрибут аля "так и задумано". С массивами тоже всё тривиально: делаем функции, которые работают с "проверенными данными" (То есть, если программист не накосячил, то выхода за пределы массива не будет) и функции, работающие с внешними данными (проверки в рантайме потребуются). Для минимизации ошибки программиста ставим ассерты и запускаем дебаг версию с санитайзерами.

                                        0

                                        D не лишает вас этого выбора.

                                          0
                                          В D, как я понял, если не делаешь инициализацию сам, то структура сама инициализируется нулями

                                          Не нулями. Агрегатные типы инициализируются значениями по умолчанию для их полей — для double, например, это NaN, а не ноль. Можно указывать свои значения по умолчанию:


                                          struct S { int x = 42; }
                                          S s;
                                          assert(s.x == 42);

                                          При этом любую инициализацию всегда можно отключить:


                                          struct S { int x = 42; }
                                          S s = void;
                                          // Теперь s.x равно бог весть чему.

                                          Язык вам ничего не навязывает. Он по умолчанию делает «как правильно», но оставляет вам возможность выжать из железа максимум, если вы знаете, что делаете.


                                          В итоге будет ряд случаев, когда компилятор не сделает оптимизацию и в рантайме будет выполнен ряд лишних операций.

                                          Компилятор умеет оптимизировать лишние инициализации.

                                    0
                                    Ты неверно задал вопрос.

                                    Преимущество D-betterC не только и не столько в этом. Ты при этом получаешь язык более высокого уровня с шаблонами, CTFE, ООП, юниттестами и много еще с чем… Бесплатно, и с легкой миграцией
                                    0
                                    Есть ли популярные проекты, реализованные на D? Какова его область применения?
                                    +3
                                    У D было много лет до взлета Go и Rust.
                                    Но D не взлетел в отличие от вышеупомянутых. И D уже не взлетит.
                                      –1

                                      И далеко они там взлетели? Вот вам рейтинг TIOBE, например:

                                        –2
                                        В Тиобе Дланг проиграет обоим (рейтинг же по количеству гуглящих): Го — из-за массовости, Расту — из-за сложности — почти каждую строку приходится гуглить =)

                                        Но я думаю, ему надо не взлетать, а найти устойчивую нишу.

                                        Jack of all trades master of none 0\
                                          0
                                          Вот тут пишут, что D универсальный язык.
                                          И на меньшее, видимо, несогласны.
                                            0

                                            Универсальность языка определяет области его возможного применения, а не популярность и необходимость всё на него переписывать.

                                        0

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

                                          0
                                          Что за деффекты?
                                            –2

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

                                              0

                                              и у каждого свои ещё, отсутствие исключении и дженериков всегда будет удерживать код на го на уровне перла или пхп из 90х, от которых все стремительное сбегали потом

                                          –2

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

                                            +2
                                            Ничего не понял, но очень интересно, можно перевод по трезвому? Обоих комментариев.

                                              0

                                              С++ изобрели потому, что проекты на си т е без ООП почти всегда вырастали в помойки которые невозможно развивать и даже поддерживать сложно, у молодежи этого понимания нет им проще писать функции чем думать о полиморфизме и солиде, поэтому клоны си без классов и наследования, порождающие такие же безнадёжные помойки, но не сразу и в условиях дикой ротации кадров не важно, заходят ну и конечно просто в их раскрутку вваливаются огромные ресурсы, а диланг например всегда был сам по себе

                                                +2

                                                Ну то что в 1980х без ООП не получалось делать масштабируемых удобных систем не означает, что ООП в 2020 году это серебрянная пуля. Кроме шуток, наследование реализации это скорее минус, чем плюс, что было видно на протяжении эволюции от плюсов и через жабу в современные ООП языки. Динамический диспатч тоже плохая штука, плох как для понимания разработчиком что где вызывается, так и для компилятора которому это убивает 90% оптимизаций. А больше В ООП никаких своих фишек и нет.

                                                  –2
                                                  ООП дает шанс сделать хорошо и примеров, когда у разработчиков получилось использовать это преимущество великое множество, когда же у вас отобрали ООП, исключения, аккуратную непротиворечивую нотацию этого шанса не будет даже в перспективе развития, а будет как раз больше различного «динамического диспатча». Пример из веба:
                                                  в CMS WordPress написанном на процедурном пхп4, что-бы поменять лейаут страниц (2 колонки на 3) надо хачить код на помеси хтмл и пхп прямо в темах
                                                  а в CMS LifeRay это делается в настройках лейаута страницы (или можно программно).
                                                    +3

                                                    А почему именно ООП? Может ФП дает шанс сделать хорошо? Или RxP? Или ещё что-то?


                                                    Примеров монстров в ООП стиле мне тоже хватает, из последнего с чем я работал — PDFKit, где документ наследует класс шрифта, текста и чёрта в ступе.

                                                      0
                                                      Пока что остальные технологии архитектурного построения не зарекомендовали себя лучше. Ждемс.

                                                      А плохо написать программу легко с любой технологией или без =)
                                                        0
                                                        фп дает шанс на алгоритмическом и локальном уровне, ооп — на архитектурном. Избыточное увлечение ими по отдельности действительно приводит к ухудшению качества кода, а вот разумное сочетание наоборот улучшает. Например, у меня код одного проектика был структурирован по ООПшному и эМВиСишному с помощью фреймворка Ангуляр, а некоторые события требующие сложной логики обрабатывались потоками Rx и все вместе выглядело достаточно пристойно, понятно и доступно для доработок. Если бы я пренебрегал стримами, локальный код был бы скорее всего более тяжелым для понимания, если отрицал бы ооп — готовность системы к конфигурации и доработкам, скорее всего, бы пострадала.
                                                          0

                                                          Попробуйте MobX, с ним код будет ещё проще, чем со стримами, а работать будет быстрее.

                                                            0
                                                            оно такое может показаться проще, но на самом деле это запутывает код, там где у вас бизнес-логика в одном блоке, она будет разманазна на какие-то ассемблерные магии, разобраться в которых и с дебагером будет сложно не говоря о таких вещах, чтобы взять и сделать переход к определению того, что происходит при нажатии условной кнопки.
                                                            Rx и подобные вещи хороши пока вы код одного применения умещается на экране. Вот например вам надо сделать реализацию дранэндропа или обрабатывать ввод поля ввода. Для этого надо взаимодействовать сразу с последовательностью событий, т.е. взять 3 и если там что-то подошло под критерий (есть все из: драгстарт, драгмув, драгенд) обработать, тут rx будет удобнее и изящнее, потому что выражение примерно так и будет записано как: стрим.взять(3).офильтровать(условие).выполнить(код).запуск().
                                                            Mobx тут вероятно может быть использован как rx и тогда не ясно зачем он нужен вообще. Стримы скорее всего со временем добавят в язык и rx тоже станет лишним.
                                                            Альтернативно, он может быть использован аналогично императивному подходу с хранением промежуточных состояний, что практически всегда хуже и фактически сложнее, т.к. потребует явной проработки множества кейсов.
                                                              0
                                                              стрим.взять(3).офильтровать(условие).выполнить(код).запуск().

                                                              Дрегендроп так не работает.


                                                              разобраться в которых и с дебагером будет сложно

                                                              Не сложнее, чем со стримами.

                                                            0
                                                            фп дает шанс на алгоритмическом и локальном уровне, ооп — на архитектурном.

                                                            Ээ, нет, вот прям вообще нет. ФП не является ничуть менее "абстрактным", а с системами эффектов на самом деле куда более. По крайней мере, в ООП я вот не нашел паттерна, как сделать так, чтобы один компонент был синхронный и всегда возвращал результат, второй асинхронный и всегда возвращал, третий синхронный который может вернуть ошибку, четвертый — асинхронный и может не найти объект (вернуть Option). И чтобы со всеми ними можно было работать однотипно (в терминах ООП: наследовать один интерфейс).


                                                            А потоки Rx это и есть ФП — гуглите про FRP. И я вот не вижу, где оно менее архитектурно чем какие-нибудь акка акторы/стримы или упомянутый ангуляр :)

                                                              –1
                                                              в ООП я вот не нашел

                                                              Fibers абстрагируют от асинхронности.
                                                              Exceptions — от ошибок.


                                                              А потоки Rx это и есть ФП

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

                                                                0
                                                                Fibers абстрагируют от асинхронности.
                                                                Exceptions — от ошибок.

                                                                Нет, эксепшны ни от чего не абстрагируют. Абстрагируют Result и прочие способы алгебраически работать с ошибками. На эксепшнах это сделать пыталась Java с Checked, но как известно это не очень удачно вышло.


                                                                Так покажите пример. Как написать код, который проходит по списку объектов и для каждого вызывает некоторую функцию. Причем функция может быть синхронной/нет, завершатсья с ошибкой/нет, возвращать объект всегда/иногда возвращать нулл/… Чтобы это было всё в типах, конечно. Чтобы не было трай кетчей на функцию которая ошибки никогда не бросает, чтобы не было необходимости писать Promise.resolve() для функции которая всегда синхронно вовзращает результат и её искусственно натягиваете на асинхронность, ну и так далее.

                                                                  0

                                                                  Result как раз от ошибок не абстрагирует, а выпячивает в интерфейс. Исключения же позволяют писать код, не зная какие исключения будут через него пролетать. Аналогично файберы позволяют вызывать функции не зная синхронные они или нет.


                                                                  Ну а код с этими абстракциями получается тривиальным — его любой школьник напишет.

                                                                    0
                                                                    Result как раз от ошибок не абстрагирует, а выпячивает в интерфейс. Исключения же позволяют писать код, не зная какие исключения будут через него пролетать. Аналогично файберы позволяют вызывать функции не зная синхронные они или нет.

                                                                    Так это не плюс, а минус же. Например, аггрегировать такие исключения в многопоточном коде целый геморрой


                                                                    Ну и вы так и не показали, как включать асинхронность на стороне клиента по-желанию, в типах.

                                                                      0

                                                                      Вы сказали, что что-то не нашли. Я вам подсказал, где искать. Дальше вы уж как-нибудь сами.


                                                                      Всё нормально у многопоточки с исключениями.


                                                                      Что вы имеете ввиду под "включением асинхронности на стороне клиента" вообще не ясно.

                                                                        0
                                                                        Всё нормально у многопоточки с исключениями.

                                                                        Скорее всё очень плохо


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


                                                                        let foo: Something = MyFoo<Sync>();
                                                                        let asyncFoo: Something = await MyFoo<Async>();
                                                                            0

                                                                            И что это? Вы код покажите, с конкретной сигнатурой myFoo которая позволяет так писать

                                                                              –1

                                                                              Там есть много примеров кода и вся необходимая для его понимания теория.

                                                                      +1
                                                                      Исключения же позволяют писать код, не зная какие исключения будут через него пролетать.

                                                                      Не понимаю, зачем это надо.


                                                                      То есть, вы либо пишете библиотечный код с условным типом MonadError e m => ..., и тогда вам плевать на конкретный тип ошибок, либо вы пишете модульный код вроде (MonadError r m, Injectable MyError r) => ..., и вам снова плевать на конкретный тип ошибок, главное — что он поддерживает MyError, либо вы пишете ядро бизнес-логики, и тогда вам не плевать.


                                                                      Аналогично файберы позволяют вызывать функции не зная синхронные они или нет.

                                                                      По аналогичным причинам не понимаю, зачем.

                                                                        0

                                                                        Ну вот подумайте, зачем, например, функции filter знать про все эти MonadError, MonadAsync, MonadLogger, MonadAnythingElse.

                                                                          +1

                                                                          Добавьте там M на конце:


                                                                          λ> :t Control.Monad.filterM
                                                                          Control.Monad.filterM
                                                                            :: Applicative m => (a -> m Bool) -> [a] -> m [a]

                                                                          Она и не знает про конкретные монады (и даже ограничивается аппликативными функторами, а не монадами).

                                                                            –1

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

                                                                              +1

                                                                              А это не костыли. Одна функция подразумевает эффекты (любые), другая — нет. Разделять их на самом деле полезно.

                                                    +1
                                                    очередной «убийца си» без классов

                                                    Вроде, классы тут есть, но без полиморфизма.
                                                      +2

                                                      Классы с полиморфизмом есть в полноценном D. В Better C только структуры. Они не поддерживают наследование, но могут содержать методы, конструкторы и деструкторы. Есть любопытная техника, которая позволяет реализовать полиморфизм без наследования: alias this.


                                                      struct A {
                                                          int x = 42;
                                                      }
                                                      struct B {
                                                          A a;
                                                          alias a this; // Теперь B позволяет обращаться к членам A как к своим собственным.
                                                      }
                                                      B b;
                                                      assert(b.x == 42);

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

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

                                                          Вот не совсем понятно, почему полиморфизм не завезли. Я понимаю dynamic downcast. Но что такого особенного требуется для vtable dispatch и интерфейсов?

                                                            0

                                                            Как вы из сишного кода будете делать этот vtable dispatch? Режим betterC был введён для ограничения возможностей D с целью прозрачной интеграции в программы на C. Если такая интеграция не требуется — просто пользуйтесь полноценным D со всеми его рантайм возможностями.

                                                              0
                                                              со всеми его рантайм возможностями.

                                                              Вот ключевое. К примеру, нужны в одном из компонентов полиморфные типы, без выставления их в С. Без рантайма это вполне делается. Чем тогда betterC лучше какого-нибудь Zig или Rust? Теряем 90% стандартной библиотеки просто потому, что она наглухо завязана на GC.

                                                                0

                                                                Для сишников "полиморфные типы" так же не актуальны как и сборщик мусора.


                                                                Если вам всё же это всё нужно — ну не используйте betterC, он вам, очевидно, не нужен.

                                                                  0
                                                                  Для сишников "полиморфные типы" так же не актуальны как и сборщик мусора.

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


                                                                  Если вам всё же это всё нужно — ну не используйте betterC, он вам, очевидно, не нужен.

                                                                  Ну вот в Rust как-то получается сделать и статический, и динамический диспатч без какого-либо рантайма. И выразительность при этом не страдает.

                                                                    0

                                                                    Вы перескочили на обобщённые типы. К полиморфизму они имеют мало отношения.


                                                                    Это всё замечательно, но в C это разве прозрачно интегрируется?

                                                                    +1
                                                                    Посмотрите на драйверы ядра в Linux. Чем не полиморфные объекты на Си?

                                                      Only users with full accounts can post comments. Log in, please.