Обновить
0
0

Студент/ Геймдизайнер

Отправить сообщение

мне до сих пор была интересна эта серия статей, так что я бы подождал одну про синхронизации, спасибо.

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

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

----

по поводу конструкторов:

в расте у полей значения по умолчанию запрещены, и каждое поле нужно обязательно инициализировать; чтобы же получить значение по умолчанию нужно использовать Default::default() или какой-нибудь _::new(), а чтобы проинициализировать напрямую нужноуказать значение для кпждого поля по отдельности.

противоположным является подход например в C, Java или C#, где если не указать значение по умолчанию, то выберется конструктор по умолчанию, null или т.п. Конечно, в C# есть required, который правда всё равно для части случаев будет так себе, а в плюсах можно написать какой-то собственный простенький тип, в котором будет что-то вроде

template<class T>
operator T() const
{
  static_assert(false);
}

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

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

заодно синтаксис value.{/*lambda*/} мне напоминает что-то похожее в сишарпе или джаве (хотя наверное в котлине оно и вовсе будет околоидиоматичным, хотя и не уверен):

new SomeType() {{
  some_field = ...;
  ...
}}
some_value with {some_field = ..., ...}
some_value.apply{
  some_field = ...
}

some_value.also{
  it.some_field = ...
}

впринципе, в C# (и Rust тоже вроде как) также можно и сделать extension-метод, чтобы работал так же как also в котлине, но всё равно котлин в этом плане как будто красивейший, и подход из аргентума мне напоминает его, тем более что это ещё и идёт по умолчанию — по дизайну решение безусловно красивое.

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

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

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

и если тут утверждается, что тут счёт ссылок неатомарный, то что будет в многопотоке?

есть ли какая-то гарантия что я не протащу слабую ссылку в другой поток имея возможность редактировать основную из этого?

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

чисто технически, есть ведь ещё и типы-степени (функции)… сосредотачиваться лишь на суммах и произведениях — не так хорошо, я полагаю, но как бы с другой стороны, функции должны быть даже в тех япах, где нету структур… немного спускаясь в полуфилософскую шутливость, операции открылись словно наоборот от классической алгебры. если конечно не вспоминать о first class – функциях и замыканиях…

В этом есть какие-то минусы кроме нарушения около-стереотипа о том, что раз умножение, то обязательно именно двух элементов?

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

да и если и рассматривать только пространства где произведения могут быть только от двух элементов, с этим же вопросом было бы интересно подойти по поводу того, как понять для ккких пространств определено произведение от n элементов для произвольного неотрицательного целого n. если условно произведение от 1 элемента определено для каждого пространства, от 2 лишь для 0, 1, 3, 7, от 0 при беглом обдумывании непонятно как определять кроме как для 1- и 0-мерного пространства — и вот это типа обобщить для произвольного фиксированного числа множителей.

знаю это, просто посчитал кодпоинты за символы

так-то да, просто я посчитал управляющие символы так же за символы.

но если делать именно так, то для условного о̴̷̰͙͓̰ͫ͗̉͜ͅ никаких 32 бит не хватит.

при том, для часто используемых сочетаний символ-лигатура (ё, だ, ã, …) и так есть отдельные кодпоинты. отдельно взятые лигатуры ведь нужны именно когда нужно олигатурить произвольный символ, нет? (хотя отдельно взятого э́ какого-нибудь не хватает или отдельных кодпоинтов для разных вариаций одного символа (привет, cjk) — но тут скорее вопрос к создателям юникода)

UTF-32 разве как раз этого не делает?

[[maybe_unused]] добавили в C++17. Даже до этого, если нет согласия с линтером и есть достаточная уверенность, что он брешет, вариант с (void) или любой другой подобной глушилкой был бы не так плох.

Собственно, я впринципе с точки зрения линтера не особо понимаю, в чём проблема, если переменная не использовалась в одной из веток — важнее же чтобы использовалась хотя бы в одной, разве нет?

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

Как будто векторное произведение определёно на любой мерности, просто придётся работать с тензорами, а не векторами.

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

Но вообще, если не лезть в тензоры, можно просто сказать, что век. произведение в ланной мерности N определено для N - 1 векторов.

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

это не так? в 4-мерном пространстве двум векторам может быть перпендикулярна плоскость — не значит ли это, что любой вектор между двумя точками этой плоскости будет перпендикулярен изначальному вектору?

jit-компилятор мб смог бы, а вот aot только если в несколько проходов. ну или сделать какую-то статическую переменную на случай "ладно, не хочу". но не думаю, что aot-компилятороделы будут использовать такой динамический подход.

Может быть, всё же дело в том, что я не джавист и джавовские практики мне не знакомы. Если так рассудить, явные исключения действительно схожи с Result, а неявные с panic. Но, в какой-то мере, мне кажется, что проблема исключений в некоторой степени и в том, что ты с ними работать будешь как с исключениями. Возможно, было бы удобно всё же сделать какой-то Result типа такого, чисто чтобы что-то делать локально внутри функции и локально с результатом конкретной функции — хотя это, наверное, и грозит потерей стектрейса (или нет, я не помню, как это работает).


public sealed interface Result<T, E extends Exception> {
    record Ok<T, E extends Exception>(T value) implements Result<T, E> {
        public T unwrap(){
            return value;
        }

        public T rethrow() {
            return value;
        }

        public <T0> Ok<T0, E> map(Function<T, T0> fn) {
            return new Ok<>(fn.apply(value));
        }

        //and other
    }
    record Err<T, E extends Exception>(@NotNull E err) implements Result<T, E> {
        public T unwrap(){
            throw new RuntimeException(err);
        }

        public T rethrow() throws E {
            throw err;
        }

        @SuppressWarnings("unchecked")
        public <T0> Err<T0, E> map(Function<T, T0> fn) {
            return (Err<T0, E>)this;
        }

        //and other
    }
    @FunctionalInterface
    interface ThrowingFn<T, E extends Exception>{
        T run() throws E;
    }

    @SuppressWarnings("unchecked")
    @NotNull
    static<T, E extends Exception> Result<T, E> catch_result(ThrowingFn<T, E> fn) {
        try {
            return new Ok<>(fn.run());
        } catch (Exception e) {
            return new Err<>((E)e);
        }
    }

    static<T> T unwrap_result(ThrowingFn<T, ? extends Exception> fn) {
        try {
            return fn.run();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    T unwrap();
    T rethrow() throws E;
    <T0> Result<T0, E> map(Function<T, T0> fn);

    //and other
}

Во всяком случае, я думаю, что есть пару вещей, которые изменить бы хотелось, чтобы всё работало приятнее:

  • Синтаксис

    Условный try{ } catch(ExceptionType e) {} лично для меня имеет пару проблем в дизайне:

    1) Фигурные скобки вокруг try и catch обязательные — рассчитаны лишь на то, чтобы оборачивать всю функцию — при необязательности для остальных конструкций.

    2) Тип исключения — если throws функции лишь одного типа, тип в catch можно бы и вывести из контекста

    3) Блок try всегда возвращает void — нужно заводить переменную вне блока для результата.

    4) Сахар на try{ } catch(ExceptionType e) {throw new RuntimeException(e)} всё же был бы приятен

  • Неявность и схожесть с неявными исключениями — поставь throws Exception на функцию — и не заметь throw там, где ты уже хочешь его поймать.

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

  • Неудобность с точки зрения типизации. Опять же, условный Function<A, B> уже запрещает кидать явные эксепшены. Если бы явные исключения становились частью возвращаемого типа, было бы удобнее. Хотя и можно сделать условный ThrowingFunction<ArgT, ResT, ExT extends Exception>, навроде того, что я написал сверху, но так-то большинству библиотек и фреймворков всё же будет всё равно.

Но может быть и такое, что это всё вообще не нужно, всё и так работает замечательно, я не прав и у меня мозг просто немного оrustенел, так сказать.

Если проблема в невидимых загрузках, то можно с C++23 вот так сделать:

void DoThing(this X& self) {
    for (int64_t k = 0; k < self->Size; k++) {
        FuncThatIsNotInlined(k);
    }
}

И вызывать как обычный метод.

Товарищ, вы боритесь менее, чем за 1 строку кода! Остановитесь! Разработчику полезно знать, где вообще подобный эксепшон в перспективе может появиться.

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

Function<A, B> f = this::someMethodThatThrows;

По этому мне придётся оборачивать:

Function<A, B> f = a -> {
  try {
    someMethodThatThrows(a);
  } catch (Exception e) {
    throw RuntimeException(e);
  }
};

Я всё же считаю, что такое оборачивание является некоторой проблемой и не так далеко ушло от какого-то err != nil в go — хоть и не нужно так часто.

Предположу, что отличие в том, что речь идёт о том, что инкапсуляция будет происходить на уровне файлов, а не на уровне линковки.

То есть условно если в классике, конечный пользователь скачает сразу файл (jar/dll/…), где будет уже динамически выбираться реализация, то здесь, я так понял, речь о том, чтобы на уровне конечного пользователя скачивать файл с реализацией в зависимости от цели.

Интересно, именно поэтому был придуман COM? — условно у нас в файлах где-то есть dll с реализацией, а сами мы при импорте можем лишь работать с интерфейсом и как-то его создавать.

Впрочем, даже без COM можно сделать отдельную динамическую библиотеку, в которой будет реализация интерфейса, а сами мы будет иметь лишь C++-овый интерфейс с помощью хедера. И вообще, зачем в примере нужна статическая библиотека, если можно просто через extern функцию в хедере то же самое?

Но хочу сказать, идея интересная. Я так понимаю, что конечному пользователю мы просто шлём различные версии либы, а сам хедер пишем универсальный, как и саму программу — действительно интересно. Впрочем, естественно, в одном файле все архитектуры не учесть — так что не могу быть уверенным в полезности данной техники, если эта самая либа не будет использоваться у конечного пользователя прямо чтобы много где.

По виду, это C++ с майкрософтовскими дополнениями — но даже так, есть странные моменты. Может, псевдокод.

Скажу больше субъективно, но всё же думаю, что если судить больше по общепринятым практикам, чем по определениям и принципам, то это всё же ближе к функциональному или типичному императивному программированию, чем к ооп.

Немного объясню, что имею ввиду:

В (типичном) ООП-стиле обычно принято реализовывать все функции "внутри" и использовать абстракции (интерфейсы):

interface ISomething{
  void DoSomething(IAnother that);
}

sealed class Smth1 : ISomething {
  public void DoSomething(IAnother that) {/*…*/}
}

abstract class Smth2 : ISomething {
  int i;
  public abstract void DoSomething(object that);
  public int DoAnother() {/*…*/}
}

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

readonly record struct ISomething(
  Action<IAnother> DoSomething
);

readonly struct Smth1{
  public ISomething as_interface{get;}
  
}

//без приватных полей
readonly record struct Smth2{
  public ISomething as_interface{get; init;}
  public Func<int> DoAnother{get;},
}

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

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

struct Smth1 {/**/}
struct Smth2 {/**/}

enum SomeSmthINeedRightNow {
  Smth1(Smth1),
  Smth2(Smth2)
}

fn do_1st(this: Smth1) {/**/}
fn do_2nd(this: Smth2) {/**/}

fn do_1st_or_2nd(this: SomeSmthINeedRightNow) {
  match this{
    Smth1(this) {/**/}
    Smth2(this) {/**/}
  }
}

Грубо говоря, нечто, существующее ещё с самого C с тех пор когда был придуман switch{case: /**/}

В какой-то мере, конечно, да — сами интерфейсы/трейты/тайпклассы сами по себе являются енумами/юнионами с потенциальном бесконечным количеством вариантом, но как бы именно различие подхода в том, что в случае классических union, enum или sealed interface перебрать все варианты является полностью возможным, а в классическом (не-sealed) ООП возможно лишь взять и предположить класс испольщуя rtti, при том, возможно, не угадать.

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

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

(например, неверно, что (1 + 1) × 1 = 2)

Но разве до тех пор, пока мы не работаем с ∞, ⊥ или делением на 0, колёса не будут работать как и классическая алгебра?

Ну, то есть вроде бы, в отличие от классики,

x×z + y×z = \left(x + y\right) × z + 0 × z

, то есть требуется дополнительное

0 × z

, но ведь как бы и стоит понимать, что

0 × 1 = 0

, то есть как бы

\left(1 + 1\right) × 1 = \left(1 + 1\right) × 1 + 0 × 1 = 1 × 1 + 1 × 1 = 1 + 1 = 2

, что, впринципе, по итогу, противоречит сказанному.

Ну или, банально, нет препятствий просто посчитать прямо:

(1 + 1) × 1 = 2 × 1 = 2

Из всего, что можно говорить о бесконечности, колёса мне кажутся самыми красивыми, ибо они полностью определены аксиоматически, при этом в них нет пресловутой "неопределённости", а так же не используют странные понятия вроде "бесконечно больших/малых" или пределов. И, сравнивая с похожими системами, эта кажется самой какой-то реалистичной, как бы не хотелось говорить о "реализме" в отношении математики.

Можно меня поправить или дополнить, если что.

Дополнение:

Хотя, думаю, ту формулу можно переписать как \left(x + y\right) × z = x × z + y × z + 0 × z, так что нет необходимости вставлять влево лишнее слагаемое и это можно можно сделать на этаме суммы произведений…

Дополнение 2:

Нет, нельзя. Потому что

(1+1)×∞≠1×∞+1×∞+0×∞=⊥И с другой же стороны,

⊥ = ∞ + ⊥ = (1 + 1) × ∞ + 0 × ∞ = 1 × ∞ + 1 × ∞ = ⊥

, что, по идее, должно быть уже верным.

К тому, что ∞ + ∞ = ⊥, нужно ещё привыкнуть…

Информация

В рейтинге
Не участвует
Откуда
Беларусь
Дата рождения
Зарегистрирован
Активность

Специализация

Разработчик игр
C++ stl
C#
Visual Studio