Как стать автором
Обновить
42
0

Типострадалец

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

Новые интересные концепции, языковые конструкции, приемы, устранение недостатков старых языков...

Вот только ничего из этого в Hare нет.

Автора почему-то тотально удалили с Хабра. Вот ссылка на копию в web archive

Нет, зачем нужны аргументы со значениями по умолчанию - понятно, это много где есть. Зачем докидывать undefined, если аргументов недостаточно - неясно. Недостаточное количество аргументов при вызове - это ошибка на вызывающей стороне, а поведение JavaScript способствует тому, чтобы эти ошибки обнаруживались позже, чем надо.

Можно самому писать довольно хитрые алгоритмы наследования

А можете рассказать, как это на практике помогает в разработке?

Далеко не всегда для корректного кода на JS тип выводится автоматом.

Если вы пишите на Javascript код, для которого специально построенный для вывода типов инструмент не может вывести типы, то с чего вы считаете, что эти типы будут ясны человеку, который этот код считает?

Угу, вот только:

  • в других языках программирования map принимает передаёт в отображающую функцию лишь один аргумент - текущий элемент.

  • в Javascript недостаточное количество аргументов при вызове не вызывает ошибку, а докидывает undefined.

Зачем это сделано и как это помогает разработчику - вопрос открытый

Фича для написания багов, ага

Если ты полез в массив из разных потоков одновременно - значит, ты ЗНАЕШЬ, что делаешь.

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

Без статического анализатора, который проверяет следование этим правилам, толку от них довольно мало.

Коллега, вы делаете мне больно своим кодом. Можно же без аллокации вектора char-ов и без лишней мутабельности:

impl FromStr for Address {
    type Err = ParseAddressError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let &[col, row] = s.as_bytes() else {
            return Err(ParseAddressError);
        };

        let col = match col {
            b'a'..=b'h' => col - b'a',
            b'A'..=b'H' => col - b'A',
            _ => return Err(ParseAddressError),
        };

        let row = match row {
            b'1'..=b'8' => row - b'1',
            _ => return Err(ParseAddressError),
        };

        Ok(Self { col, row })
    }
}

А если конструктор по умолчанию дорогой?

Моё C++-кунг-фу недостаточно сильно. Я не разобрался, как исполнить какой-то код до вызова делегирующего конструктора.

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

Более приближенный к реальности пример был бы убедительнее.

операция сравнения, это адресная арифметика.

Пожалейте сову. Object в Java тоже можно сравнивать на (не)равенство. Это не значит, что на Object есть арифметика.

Так а арифметика указателей тут причём? Указатель может быть невалидным и при этом не быть null

Давайте посмотрим на типичную функцию с некоторым количеством проверок на пограничные состояния:

Давайте, и я вам покажу, что дело не в отсутствии ранних возвратов, а в кривой архитектуре. А чтобы разговор был более конкретным - напишу минимальный дополнительный код, чтобы он компилировался:

Оригинальный код
#include <string>
#include <vector>
#include <algorithm>

struct Effects {};
struct Spell {
    bool isValid() const {
        return true;
    }
    Effects getEffects() const {
        return {};
    }
};

struct Example {
private:
    std::vector<Spell*> appliedSpells;
    void applyEffects(Effects) {}
public:
bool isImmuneToSpell(Spell*) {
    return false;
}
std::string applySpell(Spell* spell)
{
	if (!spell)
	{
		return "No spell";
	}

	if (!spell->isValid())
	{
		return "Invalid spell";
	}

	if (this->isImmuneToSpell(spell))
	{
		return "Immune to spell";
	}

	// if (this->appliedSpells.constains(spell))
    if (std::find(appliedSpells.begin(), appliedSpells.end(), spell) != appliedSpells.end())
	{
		return "Spell already applied";
	}

	appliedSpells.push_back(spell);
	applyEffects(spell->getEffects());
	return "Spell applied";
}
};

Для начала, зачем вообще принимать Spell по указателю? Указатель может быть null, и это то, что нам никогда не нужно и в данном контексте всегда является ошибкой. А посему можно принимать Spell по значению, и в этом случае бремя доказательства наличия заклинания лежит на вызывающей стороне. (В реальном коде принимали скорее по &&-ссылке, но ссылка также не может быть null). Имеем:

Код без указателей
#include <string>
#include <vector>
#include <algorithm>

struct Effects {};
struct Spell {
    bool isValid() const {
        return true;
    }
    Effects getEffects() const {
        return {};
    }
    auto operator<=>(Spell const&) const = default;
};

struct Example {
private:
    std::vector<Spell> appliedSpells;
    void applyEffects(Effects) {}
public:
bool isImmuneToSpell(Spell const&) {
    return false;
}
std::string applySpell(Spell spell)
{
	if (!spell.isValid())
	{
		return "Invalid spell";
	}

	if (this->isImmuneToSpell(spell))
	{
		return "Immune to spell";
	}

    if (std::find(appliedSpells.begin(), appliedSpells.end(), spell) != appliedSpells.end())
	{
		return "Spell already applied";
	}

	appliedSpells.push_back(spell);
	applyEffects(spell.getEffects());
	return "Spell applied";
}
};

Раз - и первый if ушёл. Бонусом получили вызов методов через точку вместо стрелочки, а ещё из-за требования предоставить оператор сравнения проявили тот факт, что сравниваются указатели на заклинания вместо самих заклинаний. Валидным такое поведение будет являться только в том случае, если мы все заклинания интернируем.

Следующий if - это вызов isValid. Сам факт наличия такого метода является ошибкой дизайна. Именно, как так получилось, что у нас есть тип Spell, который может содержать что-то, что не является заклинанием? Возможность создать невалидное заклинание означает, что валидность нужно проверять снова и снова, и вызывающий код должен эти ошибки обрабатывать, вне зависимости от того, возвращаются ли они через коды возврата или исключения. Проверку валидности нужно переместить туда, где ей самое место: в конструктор Spell:

struct Spell {
private:
    struct private_tag {};
    std::string name;
    Spell(private_tag, std::string_view name): name(name) {}
public:
    static Spell construct(std::string_view name) {
        if (name == "invalid") {
            throw std::invalid_argument("not a valid spell");
        }
        return Spell(private_tag {}, name);
    }
    // прочие методы
}

В этом случае вызывающая сторона или получает валидный Spell, или не получает никакого Spell.

Новый код без второго if:

Код с валидацией в конструкторе
#include <string>
#include <string_view>
#include <vector>
#include <algorithm>
#include <stdexcept>

struct Effects {};
struct Spell {
private:
    struct private_tag {};
    std::string name;
    Spell(private_tag, std::string_view name): name(name) {}
public:
    static Spell construct(std::string_view name) {
        if (name == "invalid") {
            throw std::invalid_argument("not a valid spell");
        }
        return Spell(private_tag {}, name);
    }
    Effects getEffects() const {
        return {};
    }
    auto operator<=>(Spell const&) const = default;
};

struct Example {
private:
    std::vector<Spell> appliedSpells;
    void applyEffects(Effects) {}
public:
bool isImmuneToSpell(Spell const&) {
    return false;
}
std::string applySpell(Spell spell)
{
	if (this->isImmuneToSpell(spell))
	{
		return "Immune to spell";
	}

    if (std::find(appliedSpells.begin(), appliedSpells.end(), spell) != appliedSpells.end())
	{
		return "Spell already applied";
	}

	appliedSpells.push_back(spell);
	applyEffects(spell.getEffects());
	return "Spell applied";
}
};

Следующий if - это проверка на наличие иммунитета к заклинанию. Как пишет автор:

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

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

Следующий if проверяет, есть ли заклинание в наборе уже применённых, и делает по этому условию возврат, если оно уже есть. В противном случае заклинание добавляется. Иными словами, набор заклинаний уникален. А знаете, какая есть структура данных, которая поддерживает этот инвариант? Множество! Более того, эта структура данных имеет меньшую асимптотику для поиска значения, чем вектор, что может стать более эффективным, когда число заклинаний вырастет до пары тысяч или около того.

Что ж, заменим std::vector на std::unordered_set:

auto [appliedSpell, inserted] = appliedSpells.insert(spell);
if (!inserted)
{
    return "Spell already applied";
}
applyEffects(appliedSpell->getEffects());
return "Spell applied";

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

template<> struct std::hash<Spell> {
    auto operator()(Spell const& s) const {
        return std::hash<std::string>{}(s.getName());
    }
};

Новый код:

Финальная версия
#include <string>
#include <string_view>
#include <unordered_set>
#include <algorithm>
#include <functional>
#include <stdexcept>

struct Effects {};
struct Spell {
private:
    struct private_tag {};
    std::string name;
    Spell(private_tag, std::string_view name): name(name) {}
public:
    static Spell construct(std::string_view name) {
        if (name == "invalid") {
            throw std::invalid_argument("not a valid spell");
        }
        return Spell(private_tag {}, name);
    }
    std::string const& getName() const {
        return name;
    }
    Effects getEffects() const {
        return {};
    }
    auto operator<=>(Spell const&) const = default;
};

template<> struct std::hash<Spell> {
    auto operator()(Spell const& s) const {
        return std::hash<std::string>{}(s.getName());
    }
};

struct Example {
private:
    std::unordered_set<Spell> appliedSpells;
    void applyEffects(Effects) {}
public:
bool isImmuneToSpell(Spell const&) {
    return false;
}
std::string applySpell(Spell spell)
{
	if (this->isImmuneToSpell(spell))
	{
		return "Immune to spell";
	}

    auto [appliedSpell, inserted] = appliedSpells.insert(spell);
    if (!inserted)
    {
        return "Spell already applied";
    }

	applyEffects(appliedSpell->getEffects());
	return "Spell applied";
}
};

Покажу отдельно итоговый applySpell:

std::string applySpell(Spell spell)
{
	if (this->isImmuneToSpell(spell))
	{
		return "Immune to spell";
	}

    auto [appliedSpell, inserted] = appliedSpells.insert(spell);
    if (!inserted)
    {
        return "Spell already applied";
    }

	applyEffects(appliedSpell->getEffects());
	return "Spell applied";
}

Осталось только два if-а, оба нужны для логики метода. Может ли тут пригодиться краткая запись для early return? Да, но, на мой личный взгляд, тут проблема стоит уже не так остро, особенно с учётом того, как похудел метод.

В общем, простите, автор, но в необходимости наличия краткой записи early return вы меня не особо убедили.

Насколько мне известно, Rust создавался под впечатлением (в частности) от OCaml непосредственно, и на OCaml даже была написана первая версия компилятора. Зачем вы его называете "старшим ML-братом Haskell" - неясно, в обоих языках есть фичи, которых нет в другом.

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

А как из первого вытекает второе?

А я правильно понимаю, что у сгенерированного cli::options есть конструктор по умолчанию? Если да, то как он работает с параметрами ENUM? И, кстати, что там происходит с ENUM с пустым списком вариантов?

И рамках действительного большого и сложного проекта все эти факторы играют существенно большую роль, чем ужасные недостатки C++ в сравнении с Rust.

То есть все эти факторы будут и в кодовой базе на Rust, и в кодовой базе на C++. Но в C++ будут ещё и грабли с недиагностируемым UB. По моему, выбор очевиден.

1
23 ...

Информация

В рейтинге
Не участвует
Зарегистрирован
Активность