Комментарии 185
На постсоветском пространстве идея "изобрести велосипед" (выдумать свой ЯП) обоснованно не пользуется популярностью ни у одиночек, ни у компаний. Из серьёзных проектов вспомнить можно разве что Kotlin, который, конечно, обвинений в велосипедостроении не избежал.
Недавно влюбился в Clojure. Функциональный язык для продуктивной веб-разработки.
Когда начинал читать, ожидал увидеть совершенно другие аргументы.
Проблема с исключениями в наших языках программирования — это то, что они по сути являются замаскированным GOTO. А мы уже выяснили, что использование GOTO — это плохо.С таким же успехом и циклы, условия, вызовы функций можно назвать замаскированными GOTO.
goto для выхода из сложного циклаСогласен, дело не в goto, а в способе его применения, в том же JS есть
break метка
для выхода из глубоких циклов.против GOTO высказывались в то время, когда этот оператор мог отправить вас внутрь цикла или даже в другую функцию
Это ещё фигня, программа вообще могла сама себя изменять в памяти! Править свой же собственный код! И это считалось хорошим искусным панорамированием Карл!
spolier: Память была не резиновая, и это позволяло здорово экономить память.
В этом случае само название и определение интерфейса становиться уже излишним — нас интересует только описываемая им операция, её параметры и результат.
Сомнительно, что излишними. Название интерфейса позволяет разделять одинаково именуемые операции с одинаковой сигнатурой, но разной семантикой.
Тогда уж лямбда-исчисление Черча.
Кажется, для оспаривания подобных заявлений придумали термин "turing tar-pit", чтобы не спешили выдавать "простоту" за простоту.
Вообще хорошо иметь стек языков, из которого можно выбрать тот, который обладает только нужными для данной задачи возможностями. (Хотя задача может быть неожиданная — иногда хочется просто развлечься и попрограммировать на чем-нибудь экзотическом.)
Мое мнение:
GOTO — если язык поддерживает оптимизацию хвостовых вызовов, то GOTO не нужен совсем. Если не поддерживает, некоторые вещи приходится делать некрасиво и менее эффективно. Хотя такие вещи обычно для человека-программиста не характерны, они могут возникать при генерации кода — мне разработчики суперкомпилятора Java (грубо говоря оптимизатора, выдающего код на том же языке) показывали пример реализации конечного автомата через таблицы, который не получалось перевести в код без GOTO не использую трюков с дополнительными переменными.
Исключения — лично мне больше нравится подход Erlang и Rust, где фатальные ошибки можно обработать только на границе процесса, а нефатальные надо реализовать с помощью кодов. В общем то наиболее полезны две стратегии обработки ошибок — повторить операцию или записать в лог и свалиться. Если их удобно поддержать в языке, использовать исключения придется только в каких-то экзотических случаях и для них можно сделать сложный синтаксис, что бы не повадно было.
Числовые типы нужны. Хотя они могут быть параметризованным вариантом одного типа, особенно если есть поддержка зависимых типов (как минимум refined types). Во первых, для эффективности — точность вычислений в разных местах программы может требоваться очень разная. Во вторых, некоторые операции не для всех типов осмыслены — я бы не хотел, что бы компилятор позволял обращаться к массиву по индексу с плавающей точкой, да и синус целого числа выглядит странно. В третьих, нужна возможность реализовать свои типы, похожие на числовые — комплексные, дуальные, матрицы и тп.
Сравнение ссылок — в императивном языке оно необходимо. Два равных по значению в данный момент объекта могут перестать быть равными через какое-то время и программист должен иметь возможность это контролировать. В декларативном языке, без мутабельных сущностей они действительно только путают.
Наследование — очень интересный вопрос. С одной стороны инструмент очень мощный, что бы от него отказываться, с другой очень сложный, и, что хуже, сложность его мало кто понимает (большинство программистов не слышали не только про топосы, но даже про принцип подстановки Лискофф).
Интерфейсы — как минимум в виде классов типов нужны. Роль часто предполагает несколько операций.
Рефлексия — у Страуструппа в главе «Правильное и неправильное использование RTTI» приведено несколько способов неправильного и ни одного правильного. Тем не менее при реализации интерпретаторов она полезна. Я бы хотел уметь ей управлять с помощью аннотаций или deriving.
Циклические зависимости — как парашют, нужны редко, но плохо, когда их не окажется, если понадобятся. Вообще, меня очень расстраивает страх многих перед рекурсией, я бы хотел жить в обществе с более высокой математической культурой.
… Rust, где фатальные ошибки можно обработать только на границе процесса, а нефатальные надо реализовать с помощью кодов.
С 1.9 это уже не совсем так: https://doc.rust-lang.org/std/panic/fn.catch_unwind.html, хотя идиоматичный подход остался тем же.
На самом нижнем уровне у нас адреса и goto. Нам это не нравится и мы обарачиваем это всё в ассемблер, Си и так далее. Чем больше мы делаем обёрток, тем медленнее и проще для нашего понимания становится код. До тех пор, пока он не становится слишком большим и медлительным, а программисты настолько запутываются во всех хитросплетениях и условностях, что становятся нигилистами, отрицающими пользу каких бы то ни было инструментов как таковых.
Всё вышенаписанное настолько же верно, сколь и бесполезно. Нет простого способа полностью описать сложную вещь. И, значит, нет такого языка программирования, который позволит решать весь массив стоящих перед программистами задач без шанса быть использованным критически неверно. И не будет. Не имеет смысла доказывать, что наследования хороши или плохи, наследование — это инструмент. Хорош или плох только конкретный инжинер, использующий его. Если инструмент опасен, хороший специалист, как Страуструп, так и напишет «можно сделать бо-бо, использовать строго в случае...». А не станет призывать выбросить его, не имея никакой альтернативы.
По статье в целом: один численный тип на всех — это дичь. Прощай производительность в мультимедиа задачах.
С каких пор Википедия считается АИ?
В языке С# (да и в Java, и в Javascript) нет ничего для предотвращения этой проблемы.
Как раз для этого случая в C# используются ref и out. В Java да, это доставляет массу неудобств
Если поля прям нельзя менять, то они и будут static/readonly/final, но мне надо быть уверенным (не знаю уж для чего) что передав об'ект в этот метод обратно он мне вернётся в неизменном виде и опять же сеттеры мне тут не особо помогают. В SQL если надо чтоб значение из процедуры вернулось в переменную, то при вызове у такого параметра будет явно стоять OUTPUT.
В D
есть модификатор доступа const
, который запрещает любые косвенные изменения.
void bar( const A a ) {
auto b = a.b;
b.foo = 2; // Error: cannot modify const expression b.foo
}
А наоборот? Т.е., создали объект, в конструкторе есть const
-параметр, его захватили во внутреннее поле, затем снаружи тот же объект пытаемся поменять.
Для этого уже есть модификатор immutable.
Можете показать пример?
https://dpaste.dzfl.pl/d288eabaee72
class A {
immutable B b;
this( immutable B b ) {
this.b = b;
}
}
class B {
auto foo = 1;
}
void main(){
auto a = new A( new B );
a.b.foo = 2; // Error: cannot modify immutable expression a.b.foo
}
Это compile-time или run-time? Если compile, то пометка immutable должна быть вверх по стеку вплоть до места создания?
Да, разумееется. В данном случае компилятор автоматически создаёт неизменяемый В исходя из контекста.
Т.е. как только B
где-то использовано как immutable
, весь тип становится immutable
? Или только конкретный экземпляр, использованный в конкретном пути выполнения?
Это отдельный производный тип immutable(B)
. Например, string — это алиас для immutable(char[])
.
Не сможем, типы-то разные. В этом случае нужно создать свою иммутабельную копию:
class A
{
immutable B b;
this( B b )
{
this.b = new immutable B( b.foo );
}
}
class B
{
int foo;
pure this( int foo )
{
this.foo = foo;
}
}
void main()
{
auto b = new B( 1 );
auto a = new A( b );
b.foo = 2;
writeln( a.b.foo ); // 1
}
Go — вроде-бы пример успешного применения второго подхода, но время покажет, чем станет этот язык лет через пять (про reflection и метапрограммирование в прадакшене уже говорят https://habrahabr.ru/post/306304/ )
Теоретически, выбор контейнера тоже можно отдать на откуп компилятору, ознакомленному с результатами профилирования. Для иммутабельных контейнеров его тип может быть выбран в библиотеке на момент создания — так сделано, например, в Scala.
Разделяемость между потоками может быть отражена в типах переменных (частично это реализовано в Rust) и компилятор будет ставить мьютексы там, где надо.
Высокоуровневые языки полезны если рассматривать неопытных программистов, которые оптимизировать особо не умеют, а на писать рабочий код на них проще.
Но массивы одномайтовых чисел и восьмибайтовых double должны различаться по типу данных, если предполагается их специфическая обработка.
должны различаться по типу данных, если предполагается их специфическая обработка
Вот мы и пришли к тому, что одного числового типа в яп недостаточно.
По коду проверки можно понять на что компилятор может расчитывать
а может и не понять. Интерфейс разных контейнеров же, например, должен быть примерно одинаковым.
В общем и целом статья строго философская, на тему «если бы слоны были бабочками». «Под капотом» у всего этого один фиг процессор с таки адресами памяти и набором примитивных операций. Вся «иммутабельность» и т.д. и т.п. — абстракция. При этом, в том виде, что предложен в статье, абстракция не имеет под собой основы и нарочь оторвана от реальности. Это все просто мат. теория.
Собственно, поиск по красно-черному дереву не имеет никакого математического обоснования. С точки зрения чистой математики поиск в дереве имеет бОльшую линейную сложность, чем элементарный перебор в лоб. Отсюда и проблема. В статье — красиво. На практике — это «горизонт», который, как мы помним, «удаляется по мере приближения».
Я говорил, что на уровне «чистой» математики, собственно, само понятие «сложности» алгоритма — достаточно странная штука.
Для «чистого» математика выборка из массива чисел выглядит как «для всех X бОльших Y». А вот трудоемкость поиска этих самых X, бОльших этого самого Y — это уже вопрос скорее прикладной математики, применительно к конкретно имеющейся системе. Т.е. я больше о том, что в отрыве от конкретной модели, с которой мы работаем, налагающей свои вполне определенные ограничения, расчет трудоемкости и прочего, мягко говоря, не имеет смысла.
С точки зрения математика result = result0 + 4 = result1 * 18 = 24 * 5 — вполне себе нормальная цепочка. Для программиста инстанцирование промежуточных result'ов — боль и разразаривание ресурса.
Даже в случае сортировки, выборки и т.д. Если программист уверен в том, что размер списка значений не превысит, допустим, 4 элементов, оптимальный алгоритм сортировки — пузырек, оптимальный поиск — линейный перебор.Поэтому призывы «а давайте абстрагируемся от всех деталей и выпилим любую возможность в них углубиться, чтобы не вызывать соблазна» мне лично кажутся злом.
Компилятор мне в лучшем случаем даёт директивы типа hot/cold.
Казалось бы идеальный инструмент — проще и функциональней дальше некуда.
А смотри же — и убить может.
То же самое будет и с идеальным языком программирования.
Кто-нибудь все равно поделит на ноль.
double a;
double b;
b = 0;
a = 2 / b;
MessageBox.Show(Convert.ToString(a));
Результат вывода: «бесконечность»
2. Если две переменных указывают на два разных блока памяти (пускай даже заполненных идентичными данными) — они не считаются равными. Это не интуитивно и порождает баги. Очень, очень субъективно.
3. Из-за существования полных по Тьюрингу языков программирования без нулевых указателей… мы знаем, что можем выразить любую валидную программу без данной концепции, а значит её устранение уберёт огромный пласт ошибок. И добавит серьезной головной боли тем, кто пытается подогнать существующие шаблоны проектирования под этот ЯП, что выльется в падение скорости разработки просто на ровном месте.
4. Согласно моему опыту, вы можете смоделировать всё, что угодно с помощью интерфейсов с одним методом, что автоматически означает возможность смоделировать то же самое и без самих интерфейсов. Отличная фраза. Вы можете, согласно моему опыту.
5. С ресурсами современного компьютера мы можем позволить себе язык программирования, дающий ровно один числовой тип. А современные компьютеры уже умеют вещественные числа произвольной точности, да со скоростью целых?
6. Однако, одно из последствий применения SOLID состоит в том, что вы должны предпочитать интерфейсы, обозначающие какую-то одну роль, а не интерфейсы, включающие наборы методов. Логический вывод из этого — каждый интерфейс должен иметь ровно один метод.
Вообще я так понимаю, парень открыл для себя функциональщину, и его так поперло, что он решил немедленно всех вокруг под это причесать.
В выводе, скорее всего, была допущена ошибка. В списке говорится, что идеален язык без рефлексии. Но, я думаю, логичнее было бы написать, что идеален гомоиконный язык, которому рефлексия не нужна.
А что в итоге? В итоге время компиляции и прожорливость приложения вырастает в разы в угоду понижения уровня вхождения в профессиональное программирование. Для ряда задач это хорошо, те же сайты-визитки или мелкие утилиты для бухгалтеров писать кому-то нужно. Но когда человек с подобным опытом неэффективного программирования попадает в энтерпрайз, джава сначала выжирает всю память, а потом пытается запуститься на остатках, юнити рисует всю сцену по 100500 раз за кадр вместо использования предрендерренной текстурки, а интернет эксплорер 7… Впрочем, не будем издеваться над инвалидами.
С ресурсами современного компьютера мы можем позволить себе язык программирования, дающий ровно один числовой тип.
Какие такие ресурсы? Ваш современный компьютер умеет считать целые и float одинаковым образом? Или сразу всё впихнуть в BigDecimal? Производительности не хватит, поверьте. Даже отсутствие unsigned в Java иногда является ощутимой проблемой.
Когда метод Map возвращает значение — был ли параметр rendition модифицирован?
Вот для этого в С++ есть const. Честно говоря, мне до сих пор неясно, почему эту замечательную фичу не перенесли в более поздние языки.
В наскальной живописи можно было сделать ошибку и нужно было искать новую скалу.
Потом появились языки высокого уровня, но они все не совершенны, ни английский, ни китайский, ни даже русский. Мы имеем много проблем. Я думаю, что нужно начать с того, чтобы убрать грамматику! Тогда мы сразу будем делать на 60% ошибок меньше. Потом пунктуацию, с ней тоже большинство не дружит, а если еще и семантику уберем, то получим вообще идеальный язык!
Даешь новый язык без пережитков старины!!!
В конце концов мы сделаем мыслепреступление попросту невозможным — для него не останется слов.
Оруэлл уже всё придумал до нас.
Сравнение ссылок
Cравнение по значению — это всегда очень медленно.
Так же есть довольно скользкий момент: иногда в объектах есть поля, которые не должны участвовать в сравнении. Например, какие-то служебные данные.
В первом случае количество ошибок не станет меньше, они просто будут другими. Во втором же случае для любого сравнения нужно будет писать код (тонны бессмысленного кода).
Числовые типы
Множество различных видов целого и вещественного чисел — это для динамических языков уже не нужная особенность.
Но отсутствие разделения на целые и вещественные числа — это дополнительные ошибки.
Вещественные типы страдают потерей точности, а это иногда очень критичный показатель. Целые, в свою очередь, страдают ограниченностью диапазона, что может привести к ошибкам переполнения.
Хорошим решением было бы выделить всего 2 класса: Int и Float, которые под капотом были бы представлены массивами. Возможно к этому когда-то придут, но сейчас живем с тем, что есть.
Но эта концепция имеет недостаток — она беспощадна к вычислительным ресурсам.
Имутабильность не уменьшит количество ошибок, она переведет их в другую плоскость. И в добавок заставит GC сгореть на работе.
Отсутствие деления целых и вещественных типов по размеру потребует использовать полиномиальные типы данных, а это медленно и очень много памяти. Уровень потребления памяти будет во много раз выше, чем у той же Java, которую за это обычно не любят.
Мне не нравится, что множества валидных (и не валидных) программ конечны.
Если мы сделаем эти множества бесконечными, то картинка сильно изменится. Дано: бесконечное множество валидных программ и бесконечное множество невалидных программ. Мы создаём язык програмирования, который исключает 999 из 1000 невалидных программ.
Ура! У нас образовалось бесконечное множество валидных программ и бесконечное множество невалидных программ.
Мы можем повторять эту операцию сколько угодно (конечное) число раз — соотношение валидных и невалидных программ от этого валидным не станет.
Количество программ неограничено в силу возможности всегда добавить к «последней программе» параметр, который позволит расширить множество.
Более того, даже в конечной памяти мы можем говорить о практической бесконечности: если у нас средний размер утилизации бинарного алфабита инструкциями составляет 50%, то двухгигабайтный бинарник (что не так уж много в свете динамических библиотек) даст нам 21073741824 вариантов программ, что примерно 10322122546, что в практических целях может считаться бесконечностью.
Лучшим подходом может быть использование некоторого композитного типа, собирающего в себе информацию об успехе выполнения некоторого блока кода, или ошибках в нём.
Это если вы пишете в функциональном стиле, где каждый участок кода что-то возвращает. Но это далеко не всегда так. И ваш язык должен требовать обязательной обработки всех вариантов tagged unions, иначе ошибки просто будут пропущены.
С ресурсами современного компьютера мы можем позволить себе язык программирования, дающий ровно один числовой тип.
Правда? Нет, серьезно? Как вы будете рационально рассуждать о потреблении памяти программой, написанной на таком языке?
Создайте языке без нулевых указателей и вам никогда не придётся задумываться о генерации или обработке ошибок доступа по нулевому указателю.
Тоже неверно. В языке без нулевых указателей просто появятся другие специальные значения, которые будут приводить к другим ошибкам, но такого же рода. В реальности нужен язык с контрактами и статической их проверкой (null-абилити — это всего лишь частный случай контракта).
Точно так же, если вы привыкли полагаться на концепцию изменяемых переменных, вам придётся научиться моделировать то же самое поведение без них.
А-га. Вы знаете, да, как различаются в производительности изменяемый и неизменяемый словарь?
Если две переменных указывают на два разных блока памяти (пускай даже заполненных идентичными данными) — они не считаются равными. Это не интуитивно и порождает баги.
Кому это не интуитивно?
Нет ничего такого, что вы можете сделать с помощью наследования и не можете сделать с помощью композиции и интерфейсов.
Нет ничего такого, что вы можете сделать с помощью языка программирования высокого уровня, и не можете сделать с помощью ассемблера.
Однако, одно из последствий применения SOLID состоит в том, что вы должны предпочитать интерфейсы, обозначающие какую-то одну роль, а не интерфейсы, включающие наборы методов. Логический вывод из этого — каждый интерфейс должен иметь ровно один метод.
Нет, это вывод не логичен. Роль не обязательно ограничивается отдельной операцией.
Если вы когда-нибудь занимались мета-программирование на .NET или Java, вы, скорее всего, знаете, что такое отражение. Это набор API и возможностей языка программирования или платформы, позволяющий вам извлекать информацию о коде и выполнять его.
Нет, reflection (по крайней мере, в .net) не является возможностью языка, это сугубо возможность платформы. Поменяйте CLR так, чтобы программы стали данными — ничего не изменится.
Но на самом деле, все сравнительно просто: Симан большой любитель функциональных языков и type-driven development. Это интересный подход, но он применим далеко не везде.
Кому это не интуитивно?
Судя по всему — абсолютным новичкам. По моему опыту люди, которые пытались войти в ИТ и при этом не имели какой-либо подготовки, часто очень долго не могли понять как два объекта одинаковые по содержанию не равны между собою по ==.
Описанная автором система программирования — это некий язык, который можно будет подсунуть даже обезьяне и она на нем сможет написать валидную программу. И этому примату тяжело будет понять различие между по-ссылке и по-значению.
Тоже неверно. В языке без нулевых указателей просто появятся другие специальные значения, которые будут приводить к другим ошибкам, но такого же рода. В реальности нужен язык с контрактами и статической их проверкой (null-абилити — это всего лишь частный случай контракта).
Явное указание в типе о том, что значения может не быть (Option) — это и есть решение. Но чтобы оно работало и код гарантированно был null-безопасным, надо убрать поддержку null из ЯП.
Недостаточно убрать поддержку null
.
Во-первых, в половине случаев вы просто замените NRE на ArgumentException
, или что у нас там Option.get
кидает. Стало лучше? Не особо.
Во-вторых, вот вы избавились от null
. Что там возвращает String.IndexOf
для ненайденного значения? Правильно, -1. Что будет, если написать smth.Substring(smth.IndexOf(Delimiter))
? Правильно, потенциально тот же ArgumentException
. Что-то изменилось по сравнению с тем, что repository.Find(x)
возвращает null
, и repository.Find(x).Process()
упадет с NRE? Фундаментально — нет, вы все так же получаете run-time ошибку.
Поэтому да, с одной стороны убирание (или явная типизация) null
делает программы немножко более очевидными. Но с другой стороны — языку и языковым средствам (в частности, статическим анализаторам контрактов) надо сильно эволюционировать, чтобы это было по-настоящему мощным (а не привело к банальной замене repository.Find(x).Process()
на repository.Find(x).Value.Process()
).
null размывает контракт, делая любое значение потенциально отсутствующим. Option определяет контракт явно, и компиятору не нужно никакого адского статического анализа, чтобы понять, что кто-то делает Option.get без проверки.
С NPE больше всего проблем в больших системах, когда где-то кем-то провалидированные данные с null'ами внутри, успешно пройдя транзитом через несколько границ подсистем и слоев абстракциии, используются в отрыве от их исходного контракта. Option в этом плане сложнее протащить через всю систему насквозь, его место — на границе, а внутри будут циркулировать значения без всяких сюрпризов, о чем явно будет свидетельствовать их тип.
Option определяет контракт явно, и компиятору не нужно никакого адского статического анализа, чтобы понять, что кто-то делает Option.get без проверки.
Так и с null
его не нужно. Проблема-то ровно в обратном. Смотрите:
smth |> Option.get
Надо выдавать предупреждение? Наверное, да. А теперь?
if (smth |> Option.isNone) throw;
//40 loc
smth |> Option.get
Вот и с null dereferencing то же самое. Понятно, что с явными null и не-null типами жить немножко легче, но это не фундаментальное изменение.
А главное, что для того, чтобы работали options, нужна сквозная поддержка монад, иначе код становится неудобоваримым. А где монады — там и ФП, и к нему все и приходит в итоге (я уже говорил, что Симан любит ФП).
С NPE больше всего проблем в больших системах, когда где-то кем-то провалидированные данные с null'ами внутри, успешно пройдя транзитом через несколько границ подсистем и слоев абстракциии, используются в отрыве от их исходного контракта.
Статический анализ справляется.
В Javascript есть только один числовой тип, что является отличной идеей. Жаль только, что это неправильный числовой типИнтересно. А какой с точки зрения Крокфорда правильный тип? Целое или там значение с тегом, или вообще объект?
Изменение значений переменныхЗапрет на изменение переменных в пределах текущей области видимости. Но…
Тогда прощай ООП и привет ФП, как я понимаю? Просто такую фичу я только в ФП встречал.
Волшебный, размером с char и размерностью с гугол.
> Тогда прощай ООП и привет ФП, как я понимаю?
А разве это не было очевидно изначально? Причём, часть претензий автора вызвана именно что усталостью от неверного или излишнего использования обыкновенных ООПшных и императивных инструментов. И этим грешат не только неопытные программисты, но и вполне весомые дядьки, у которых просто замылился глаз. То, что много чего на чистом ФП описать просто невозможно, статья плавно обходит, не о том она.
[5 абзацев ни о чем]
> мы уже выяснили, что использование GOTO — это плохо.
А ручки-то вот они.
И вот еще забавная статья, развенчивающая миф о простоте Хаскелла, на примере реализации quicksort:
Parallel generic quicksort in Haskell
развенчивающая миф о простоте Хаскелласильно!
Я вот могу взять какую-нибудь цитату Энштейна (из гугла) — он «чуть больше программист», чем Экзюпери, например
Господь Бог вычисляет дифференциалы эмпирическии начать колоть всё программирование как орех. Зачем вообще точные результаты и мат. модели, надо просто положить в железо нечеткие и нейроконтроллеры, а потом их комбинировать, настраивать и обучать — вот и всё программирование, никаких циклов-шмыклов. Ну это я конечно всё утрирую, но сам подход… наукообразный какой-то, ни туда ни сюда.
Вобщем неправильно так делать. Как-будто на автоматных компьютерах нельзя вот в ногу себе выстрелить. Холивар из разряда «дисциплина против дураков»…
Чем меньше, тем лучше — о возможностях языков программирования