— Сколько нужно архитекторов, чтобы реализовать язык программирования?
— Сто. Один будет писать реализацию, а 99 — говорить, что могут сделать лучше.
В этой статье я хочу затронуть не столько сам язык, сколько детали реализации CPython и его стандартной библиотеки, которые гарантируют, что у вас не будет никаких простых способов сделать приложение на питоне ни многопоточным, ни быстрым, ни легко поддерживаемым, и почему было создано столько альтернативных реализаций (PyPy, Cython, Jython, IronPython, Python for .NET, Parakeet, Nuitka, Stackless, Unladen Swallow), половина из которых уже умерла; и мало кто понял, почему у альтернатив не было шансов победить в борьбе за выживание против других языков. Да, есть GDScript, который призван решить проблемы с производительностью, есть Nim, который призван решить вообще все проблемы, не обязывая при этом пользователя чрезмерно явно объявлять типы. Однако, учитывая огромную инертность индустрии, я осознаю, что в ближайшие 10 лет новые языки точно не займут значимой ниши. Однако, я верю в то, что питон возможно сделать эффективным, изменив стиль написания кода, по большей части сохранив оригинальный синтаксис, и полностью сохраняя возможность взаимодействия кода нового и старого стиля. Я буду концентрироваться на проблемах CPython, а не ближайшего его конкурента, PyPy, поскольку PyPy на самом деле прыгает вокруг всё тех же проблем CPython.
Я — программист с семилетним стажем, в основном занимался разработкой десктопных приложений, с неким упором на веб и многопоточные базы данных. Вы спросите «погоди, но что общего имеет питон с пользовательским интерфейсом, веб-фронтендами, и многопоточностью?». А я отвечу «вот именно — ничего». Я использовал C, Delphi, JavaScript, и SQL для моих задач. Меня эта ситуация не сильно радовала, и энное время назад я попытался участвовать в проекте Эрика Сноу по реализации поддержки множественных интерпретаторов в CPython:
https://www.python.org/dev/peps/pep-0554/
https://github.com/ericsnowcurrently/multi-core-python
К сожалению, очень быстро пришло понимание того, что:
- CPython довольно слабо поддерживается для такого популярного проекта, и имеет кучу старых проблем, которые вылазят наружу при попытке перекроить реализацию. В итоге Эрик уже несколько лет ковыряет интерпретатор с переменным прогрессом;
- даже после успешной реализации множественных интерпретаторов не совсем ясно, как дальше организовывать параллельное выполнение. В PEP предлагается использовать простые каналы, но этот инструмент становится опасным по мере усложнения задачи, с угрозами зависания и непредсказуемого поведения;
- сам язык имеет большие проблемы, не дающие интерпретаторам непосредственно обмениваться данными и давать некую гарантию предсказуемости поведения.
Теперь более детально о проблемах.
Изменяемые определения классов
Да, я понимаю, что класс в питоне объявляется во время выполнения. Но, блин, зачем в него совать переменные? Зачем в старые объекты добавлять новые методы? В какой-нибудь Java нельзя объявлять функции и переменные вне классов, но в питоне такого ограничения нет (и питон создан до джавы). Причем, попрошу обратить внимание на то, как нужно нагибаться раком для того, чтобы добавить аналогичные методы в сам объект, а не в класс — для этого нужны types.MethodType, function.__get__, functools.partial, и так далее.
Я бы хотел для начала задать странный вопрос: а зачем вообще в питоне нужны методы? Не функции, как в близком JavaScript, а именно методы класса. Один из факторов: Гвидо не придумал лучше способов сделать короткие имена функций (чтобы не было сишного gtk_button_set_focus_on_click), поскольку не ясно, как выбирать из кучи похожих функций с коротким именем нужную под этот конкретный объект. Тем не менее, в питоне появились len, iter, next, isinstance, slice, dict, dir, str, repr, hash, type — сейчас это обертки над соответствующими методами классов с подчеркиваниями в имени, а когда-то встроенные простые типы не являлись классами и работали только через эти функции. Лично я не вижу особой разницы между записью method(object) и object.method — особенно если method является статичной функцией, которой, в общем-то, все равно, какой первый аргумент (self) принимать.
Динамические определения классов в общем случае:
- не дают модульно тестировать. Правильно отработавший в тесте кусок кода может выдать ошибку при работе целой системы, и никак вы от этого не защититесь в рамках CPython;
- создают большие сложности оптимизации. Объявление класса не дает вам гарантии по поводу фактической работы класса. По этой причине единственный успешный проект оптимизатора, PyPy, использует трассировку для обнаружения фактической последовательности выполняемых действий методом пробы;
- не состыковываются с параллельным выполнением кода. Например, тот же multiprocessing работает с копиями определений классов, и если вы не дай бог измените описание классов в одной из копий, то ваше приложение рискует развалиться.
Более тонкий вариант динамических классов — это переопределение доступа к атрибутам через __getattribute__, __getattr__ и прочие. Зачастую они используются в качестве обычных геттеров-сеттеров, для делегации функций объекту-полю, и изредка — для организации DSL. Все эти функции могут быть реализованы более цивилизованным способом, без превращения класса в свалку описаний, поведение которых порой тяжело гарантировать. К слову, в случае геттеров/сеттеров такой механизм уже есть — это дескрипторы атрибутов: https://www.python.org/dev/peps/pep-0252/#id5
Горячая замена классов нужна для целей отладки и банальной кодописи, но это все-таки должен быть специализированный инструмент для разработчика, а не артефакт во время выполнения, от которого нельзя избавиться.
Компилируемые классы — это только маленький шаг, который уже сделали в Cython и Nuitka, но одного этого шага без других изменений недостаточно для получения какого-то значимого эффекта даже в плане скорости выполнения, поскольку, например, питон широко использует динамическое связывание переменных, которое никуда не девается в откомпилированном коде.
Множественное наследование
Я думаю, это лидер хейт-парада. Его нет даже на уровне C-функций в реализации самого питона и его расширений. «Но как же интерфейсы?» — возразите вы. Интерфейсы в C++ и Java нужны в роли объявления протоколов вызова методов объекта с целью последующей статической проверки этих протоколов при компиляции, а также для формирования таблиц методов, которые во время выполнения будут использованы другим кодом, ничего не знающим об исходном объекте. Эти роли почти полностью потеряны в питоне, потому нет никакого оправдания их существованию. Мне нравится то, как сделаны интерфейсы в Go — это очень похоже на питоновые ABC: https://www.python.org/dev/peps/pep-3119
Множественное наследование не является прямой проблемой для параллелизации и оптимизации, но усложняет читаемость и поддерживаемость кода — это так называемый лазанья-код (по аналогии со спагетти-кодом).
Генераторы
Это прямо-таки запущенный случай GoTo, когда выполнение не просто бесконтрольно прыгает по коду — оно прыгает по стэкам. Особенно лютая дичь происходит, когда генераторы пересекаются с менеджерами контекста (привет PEP 567). Если в питоне есть общая тенденция запутывать приложение в тесный клубок связанных изменяемых состояний, не дающих пространства для маневров тестирования, параллелизации, и оптимизации программы, то генераторы — вишенка на этом торте.
Как вы думаете, какой будет результат выполнения программы:
import contextlib
@contextlib.contextmanager
def context_manager():
try:
print('Вход')
yield
finally:
print('Выход')
def gen_in_manager():
m = context_manager()
with m:
for i in range(5):
yield i
g1 = gen_in_manager()
next(g1)
print('Конец')
Это несколько переработанный пример со stackoverflow
Вход
Конец
Выход — не отобразится при выполнении в интерактивном режиме
Если этот пример показался вам слишком простым, то предлагаю с такой же легкостью предсказать выполнение немного измененной версии:
import contextlib
@contextlib.contextmanager
def context_manager():
try:
print('Вход')
yield
finally:
print('Выход')
def gen_in_manager():
m = context_manager()
with m:
for i in range(5):
yield i
def test():
g1 = gen_in_manager()
next(g1)
test()
print('Конец')
Вход
Выход
Конец
Вот так просто мы сделали выполнение нашей программы плохопредсказуемым.
Если вы думаете, что генераторы нужны только для возврата массива из функции, то спешу вас поправить: также генераторы используются в генераторных выражениях и async/await, а последнее нынче набрало популярность в веб разработке на питоне.
Интересный факт: транслятор RPython превращает генератор в класс-итератор. Это то, как генераторы и должны были быть сделаны. Ну а пока что наличие генераторов в коде не дает возможности для оптимизации, параллелизации, и тестирования.
Изменяемые значения
https://stackoverflow.com/questions/530530/python-2-x-gotchas-and-landmines
>>> a = ([42],)
>>> a[0] += [43, 44]
TypeError: 'tuple' object does not support item assignment
>>> a
([42, 43, 44],)
>>> a = ([42],)
>>> b = a[0]
>>> b += [43, 44]
>>> a
([42, 43, 44],)
>>> x = y = [1,2,3]
>>> x = x + [4]
>>> x == y
False
>>> x = y = [1,2,3]
>>> x += [4]
>>> x == y
True
>>> x = [[]]*5
>>> x
[[], [], [], [], []]
>>> x[0].append(0)
>>> x
[[0], [0], [0], [0], [0]]
Здесь мы приходим к некоему неожиданному выводу, на который особенно наводит ошибка «'tuple' object does not support item assignment» на фоне успешного выполнения операции: синтаксис питона слабо поддерживает работу с изменяемыми данными. Синтаксис адаптирован к тому, что результат операции всегда создается заново, как это сделано с числами, строками, кортежами. Однако же, по логике вещей, x[0].append(0) должно было бы создать новый или взять уникальный список из нулевого элемента и добавить в него нуль, но в CPython вместо этого неявно изменяется единственный экземпляр пустого списка, который хранится в нескольких местах. И если в трех строчках кода нам это очевидно, то в большом проекте можно очень долго вылавливать подобные проблемы.
Что же это за такой странный зверь тогда — списки [] питона? Это оптимизация кортежей, костыль, потому что операции с пересозданием кортежей на большом числе значений становятся очень медленными. К слову, создатель Clojure не согласился с таким подходом, и сделал быстрые неизменяемые списки с чем-то вроде partial-copy-on-write при выполнении операций над этими списками. Гвидо же сделал доживший до наших дней костыль, и не сделал адекватные средства работы с этими костылями.
Что делать с изменяемыми данными? К которым, кстати, также относятся и объекты в целом. Например, можно делать copy-on-write на присвоении, и явные операции изменения данных по месту без копирования: b.append.., b[].., b +=… Результат: устранение случайных связей между объектами, гарантия неизменяемости извне обладаемого объекта, а в итоге — упрощение параллелизации, оптимизации, тестирования. К сожалению, отваливается код, который опирался на старую логику работы списков, когда автору понадобилось, чтобы список/объект/ассоциативный массив таки менялся из разных мест, пусть это поведение и не очевидно из самого кода.
Почему Гвидо сразу не сделал язык с copy-on-write? Потому что та реализация оригинального интерпретатора не давала простой возможности отслеживать ссылки и копировать объекты, в частности, это требовало как минимум введения двойного указателя (вложенного), где указатель из функции на Си читает адрес объекта (который может меняться), а уже по этому адресу лежат сами данные, скопированные или общие неизмененные.
У меня все-таки не получается придумать лаконичных и совместимых способов добавить четкое разграничение изменяемых и неизменяемых объектов в рамках имеющегося языка. Ваши предложения? Например, можно было бы сделать все объекты copy-on-write, а явные ссылки делать через «({'first': 1, 'second': 2},)», то есть, такой себе боксинг, где роль контейнера выполняет кортеж, который неизменяем, и потому никогда не будет копироваться, оставаясь единой ссылкой на объект «{'first': 1, 'second': 2}».
Полиморфизм и множественная диспетчеризация
https://ru.wikipedia.org/wiki/Полиморфизм_(информатика)
«Полиморфизм в языках программирования и теории типов — способность функции обрабатывать данные разных типов»
Статья: https://en.wikipedia.org/wiki/Multiple_dispatch#Use_in_practice
Исследователи заявляют, что в исследованных системах 13–32% функций используют диспетчеризацию по одному аргументу (a.k.a. виртуальный метод), и 2.7–6.5% функций — по двум и более. Но я вам скажу ужасающую новость: когда вы просто используете арифметические операции над разными типами, или даже банально присваивание, то вы применяете операцию, полиморфную по одному и более аргументу. Полиморфизм намного ближе, чем кажется, и на самом деле все популярные языки очень активно употребляют полиморфные операции.
Например, в Си тип данных — это не сами данные, это способы работы с ними. По этой причине Си глубоко полиморфичен, то есть, позволяет однообразно обрабатывать разные типы данных. Например, вы можете превратить указатель на double в указатель на char, скопировать байты через последний указатель, потом взять эти байты как указатель на double и работать дальше с ними как с числом.
float a = 2.0;
float *src = &a;
char *dest = malloc(sizeof(float));
memcpy(dest, src, sizeof(float));
printf("%f", *(double*)dest);
При этом, язык способен сам совмещать в операциях (сложение, сравнение, присвоение) различные типы данных — эта фича давным давно возникла в самых разных ЯП и почему-то воспринимается как данность, но это весьма сложный и, порой, коварный механизм.
Питон же фундаментально полиморфичен, потому что большинство его конструкций применимы (в том числе с генерацией исключения) к произвольным типам. Язык сам по себе изначально не задавал никаких правил о реализации этого полиморфизма: у вас есть просто код «a = b + c», а вы дальше сами разбирайтесь, как вы будете это выполнять. Как же Гвидо решил это выполнять?
- явная диспетчеризация во время выполнения: «if (Py_TYPE(obj) == &PyLong_Type) {long val = PyLong_AsLong...}» на Си или аналогичное на питоне через type();
- приведение полиморфного аргумента к единому типу: «PyArg_ParseTuple()» на Си или конструкторы типов на самом питоне, вроде «this.number = int(param1); this.text = str(param2)» — в данном случае явная реализация диспетчеризации оказывается уже в конструкторе, а сам вызов конструктура является неявной диспетчеризацией. В стандартной библиотеке много подобных конструкций, правда, часто они вызваны банальной необходимостью сделать копию объект известного типа (напоминаю из прошлого раздела, что синтаксис питона слабо поддерживает операции с изменяемыми данными);
- методы/функции/поля объекта и его классов. Это уже не похоже на простые таблицы виртуальных методов в каноничных классовых ООП языках — это алгоритм поиска подходящего атрибута по сложному и не всегда ясному алгоритму.
Итак, возьмем банальную операцию сложения. Что делает питон для того, чтобы сложить два объекта разных типов? Я прочитал весь код реализации механизма сложения в CPython, но, честно говоря, до сих пор не пойму, что курил ее автор, и зачем было так это делать. Грубо говоря, опишу это так: будет вызван метод сложения одного из аргументов или встроенная быстрая функция сложения чисел. Пардон, а где здесь полиморфизм по двум аргументам? А нету его: здесь подразумевается, что оба метода у аргументов будут одинаковыми и/или эти методы будут выдавать ошибку, если обнаружат несовместимый метод сложения/тип у другого аргумента. Cложение «a = b + c» делается примерно так (в псевдокоде, краткий пересказ кода на Си):
def PyNumber_Add(b, c):
slotb = b.__add__
if not type(b) is type(c) and c.__add__:
slotc = c.__add__
if slotc is slotb:
slotc = None
if slotb:
if slotc and isinstance(c, b.__class__):
return slotc(b, c)
else:
return slotb(b, c)
else:
return slotb(b, c)
if isinstance(b, str) and isinstance(c, str):
a = unicode_concatenate(b, c)
else:
a = PyNumber_Add(b, c)
Провал здесь заключается в том, что на самом деле сложение логически не принадлежит числу, не является его методом. «Вариться в супе» не является методом собаки — это функция, которая использует собаку, плиту, котелок, и воду для реализации алгоритма. Но позиция питоничности, как и во многих других ООП языках, заключается в том, что каждая собака должна иметь методы «вариться в супе», «бежать», «лаять», и так далее; короче говоря — реализовывать все возможные и некоторые невозможные варианты взаимодействия со внешним миром.
Вернемся к нашим баранам: Гвидо не придумал, как сделать множественный полиморфизм. В языке, фундамент которого представляет собой полиформизм всех аргументов. Есть и утешительный вывод: он здесь не одинок, и куча создателей ООП языков тоже не придумали ничего годного. К сожалению, чисто классовая модель решительно провальна в этом плане, потому что дает исключительно одиночный полиморфизм — через виртуальный метод класса.
В тех же крестах и жаве для обхода ограничения существуют перегруженные методы, которые позволяют реализовывать один и тот же метод для разных аргументов типов. В этом решении есть проблема, которая привела к появлению такого уродливого подхода, как шаблон Посетитель. А именно, с перегрузкой методов класс опять-таки должен уметь всё на свете вместо того, чтобы нужные частные реализации принадлежали частными комбинациям типов.
Поскольку полиформизм на уровне ООП работает отвратительно, то целая куча функций из стандартной библиотеки питона представляют из себя отдельный интерпретатор, который самостоятельно читает программу (то есть тип и значение аргументов), и принимает решение о принимаемых действиях (приведение к единому типу или ветвление), а потом выбрасывает результат на вход следующему интерпретатору. И самое печальное здесь то, что теперь такой способ выполнения программы диктует расширениями питона и другим реализациям питона действовать точно так же, повторяя заморочки реализации всех мелких интерпретаторов (функций стандартной библиотеки). Помимо банальной производительности, это лишает предсказуемости, поскольку тип на выходе может неожидано измениться при неизвестных обстоятельствах — у нас же нет никакой гарантии типов, помните? Построение языка на мелких недостаточно предсказуемых интерпретаторах в итоге является одной из ключевых причин потери возможности оптимизации кода и статической проверки типов.
Еще один из костылей для решения проблемы множественного полиморфизма — это множественное наследование: мы засовываем два полиморфных параметра в один класс, представляющий собой комбинацию родительских классов из двух независимых иерархий. Я думаю, мне не нужно объяснять, насколько тяжело читать и поддерживать такой код. Причем, и здесь принцип оказался замаран заморочками реализации, потому что иногда иерархии все-таки пересекаются, и тогда начинается чад кутежа, читай: множественное наследование не работает в общем случае. Конкретно Гвидо серьезно поломал себе зубы об этот камень, пытаясь заставить работать то, что не может работать — в итоге мы имеем MRO:
https://www.python.org/download/releases/2.3/mro/
https://ru.wikipedia.org/wiki/C3-линеаризация
Справедливости ради нужно заметить, что начиная с 3.4 в питоне появилась так называемая «клиника аргументов» ( Argument — Monty Python ), которая призвана автоматически генерировать обертку обработки аргументов для функции с чисто сишными аргументами. Так сказать «не прошло и двадцать лет»… а не, уже прошло: 3.4 выпущен в 2014, а первый релиз питона был в 1991.
Но все эти попытки исправить горбатого питона представляют собой лишь полумеры. Питону нужна возможность распределять по модулям реализацию конкретных методов и дополнять эти реализации при появлении новых типов, примерно как это делают типажи (trait) в Rust (не важно, во время компиляции, исполнения статичного кода, интерпретации, или чего бы то ни было еще):
https://doc.rust-lang.org/1.8.0/book/traits.html
То есть, если я складываю объект А с объектом Б, то вызывается не метод объекта, и не встроенный в интерпретатор костыль, а функция, которая объявлена как функция сложения с аргументами типа А и Б. Проблемы возникают разве что с самим определением «тип». Поскольку в питоне очень распространена утиная типизация, а наследованию мы объявили войну, то класс объекта не может служить его типом в данном случае. Скорее, нужно что-то вроде интерфейса, протокола, типажей Rust-а. Например, объект имеет __iter__ — он становится автоматически обладателем типажа «итератор». Правда, если так широко загребать объекты под типажи, то очень быстро возникает ситуация, когда конфликтующие типажи представляются абсолютно одинаковыми атрибутами, с одинаковыми аргументами или типами переменных. Потому есть смысл делать явное объявление типажей, вроде «вот эти мои атрибуты принадлежат такому-то типажу, а никакому не другому». С другой стороны, не хотелось бы скатиться в крестовые обобщения с многоуровневыми объявлениями типов, как то в C++ ranges, где объявления шаблонов занимают примерно столько же, а то и больше, чем сама реализация алгоритма.
Приведу простой пример:
from collections.abc import Iterable, Container
from itertools import filterfalse
class MyList(Trait, Iterable, Container):
pass
def __sub__(a: MyList, b: object):
return list(filterfalse(lambda x: x == b, a))
def __sub__(a: MyList, b: Container):
return list(filterfalse(lambda x: x in b, a))
a = MyList([1, 2, 3, 4, 5])
print(a - [2, 5]) # То есть, print(__sub__(a, b))
# Вывод: [1, 3, 4]
print(a - 3)
# Вывод: [1, 2, 4, 5]
Здесь мы создаем оператор вычитания с типажем аргумента MyList, который как бы наследует Iterable (наличие метода __iter__) и Container (метод __contains__), которые оба поддерживаются list, и потому list можно как бы приводить к MyList, хотя ни MyList не является наследником list, ни list не является наследником MyList. Этот код в классическом питоне выглядел бы как:
from collections.abc import Container
from itertools import filterfalse
class MyList(list):
def __sub__(self, b):
if isinstance(b, Container):
return list(filterfalse(lambda x: x in b, a))
else:
return list(filterfalse(lambda x: x == b, a))
a = MyList([1, 2, 3, 4, 5])
print(a - [2, 5])
# Вывод: [1, 3, 4]
print(a - 3)
# Вывод: [1, 2, 4, 5]
Но есть большая разница: первый код можно скомпилировать статически с контролем типов, и он на самом деле не изменяет типа массива «a», а лишь оказывает влияние на методы работы с этим объектом. Это как бы «огрызочное наследование», когда наследование функционала происходит по отдельным частям-интерфейсам, а не по всей реализации в целом.
Конечно, я не предлагаю запрещать старые классы и быстро всем бежать переписывать код — скорее, я предлагаю основную толкотню отправить в стандартную библиотеку и в сам транслятор, где транслятор по правилам из библиотеки будет сидеть и лепить вызовы функций с типажами, аки конструктор лего. На самом деле, он и так сейчас их лепит, просто делает это беспорядочно и во время выполнения.
Оператор присвоения и статическая типизация
Давайте вспомним, что многие классы из стандартной библиотеки питона скомпилированы статически, в виде встроенных функций интерпретатора или расширений на Си. Очевидно, что они ждут вполне конкретный ограниченный набор входных параметров, для проверки которых применяют собственные велосипеды, и они хранят внутренние данные в фиксированном формате, который не может внезапно измениться, например, с массива на строку. Огромное количество проблем в программах на чистом питоне возникло из-за того, что присваивание питоне абсолютно слепо — оно просто берет любую ссылку-объект на вход, и присваивает эту ссылку левому выражению. Например:
>>> a = [1, 2, 3]
...
>>> a = '15'
...
>>> for i in map(lambda x: x*2, a):
>>> print(i)
11
55
Ожидается вывод
2
4
6
Удачи вам искать подобную ошибку в код на сотни тысяч строк.
Конечно, мы не можем заставлять программистов использовать только один тип в одной переменной, поскольку запихивание кучи типов в одну переменную стало уже сложившейся традицией. Даже банальный None — это уже другой тип, NoneType. Однако же, есть законченные куски кода — модули и объекты, которые в реализации на Си более-менее блюдут чистоту типов, но при написании на питоне внезапно эту чистоту теряют. Давайте рассматрим на простейшем примере теоретическую новую модель:
>>> class A():
>>> def __init__(self, value):
>>> self.val = value
>>>
>>> a = A('2')
>>> a.val = []
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only assign str (not "list") to str
>>> a.myattr = []
>>> a.myattr = 2
Здесь создается класс A с полиморфным типом атрибута val, который конкретизируется выводом типа из конструктора. Естественно, кто-то извне может захотеть добавить свой собственный атрибут в объект (myattr) — этот атрибут будет уже находиться на уровне экземпляра объекта, и дальше уже создающий и использующий экземпляр код будет разбираться, нужно ли проверять тип или нет.
Ожидаемо, такие финты ушами плохо состыковываются с динамически изменяемым описанием класса. Класс здесь должен быть не просто статичен — он является шаблоном, обобщением для частного класса, генерируемого в коде создания и использования объекта. Возьмем тот же класс, только теперь зададим ему несколько иной тип:
>>> class A():
>>> def __init__(self, value):
>>> self.val = value
>>>
>>> def func():
>>> a = A(None)
>>> a.val = 2
>>> print(a.__dict__)
>>>
>>> func()
{'val': 2}
Здесь вызывающий код создает как бы A<None or Int>, в результате чего функция успешно выполняется. Естественно, при строго построчной интерпретации проверить тип не представляется возможным, потому код создания и инициализации объекта завернут в функцию.
Я плавно перехожу к проблеме отсутствие времени компиляции: интерпретатор предполагает, что вы в любой момент имеете возможность перейти из любого состояния в почти любое: через двойную сплошную, овраг, сбив пасущуюся на лугу корову перетащите поезд с железной дороги на речку, и поплывете по речке всё с теми же звуками «чух-чух», которые были у класса «поезд». Глобальная изменяемость состояния — это важная особенность, которая пронизывает весь язык; из-за нее, к слову, питон отвратительно натягивается на функциональщину, потому что в своем фундаменте является языком глобального изменяемого состояния, в противовес чистым функциям без состояния. При всем при этом, однако, вы не можете банально заменить стандартный тип «list», который используется, в том числе, в list comprehension, а тот, в свою очередь, использует BUILD_LIST и LIST_APPEND (в CPython) — вы можете найти логичное оправдание такой негибкости на фоне гибкости? Как по мне, так это больше напоминает модель «груда беспорядочных фич», «лишь бы как-то заработало».
Еще замечу, что кто-то (я) может захотеть сделать приведение типа аргумента при присвоении, вроде «a.val = int(newval)». Собсна, стандартная библиотека часто делает приведение, но больше для аргументов и уже после выполнения самого присвоения, когда ссылка на значение неизвестного типа передается аргументом функции. Для перезаписи операций присваивания атрибутов в классах есть такие механизмы, как __setattr__ и __setattribute__, а начиная c 2.2 добавились еще и дескрипторы с их __set__ ( https://www.python.org/dev/peps/pep-0252/ ). Но давайте не забывать: намного проще оперировать простыми переменными, нежели создавать класс контекста и экземпляр этого класса просто для того, чтобы поработать с парой значений — в худших традициях C++/Java/C#. В принципе, можно было бы сделать через дескрипторы переопределение оператора присвоения с минимальными изменениями: достаточно начать уважать в обычных переменных методы __set__, __get__, __delete__, таким образом позволяя сделать проверку типа аргумента присвоения во время выполнения, например:
>>> a = StrictDict({'first': 1 })
>>> a = { 'dummy': 666 }
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StrictDictError: "first" key is missing in the assignment source
К слову, это один из вариантов решения двусмысленности операций со списками (из раздела «Изменяемые значения»): мы можем явно создать copy-on-write массив, который будет принимать новые значения, но не перезаписывать себя ссылкой на новый объект-массив, а делать copy-on-write ссылку на имеющиеся данные:
>>> a = COWList([1, 2, 3])
>>> b = a
>>> a.append(4)
>>> b.append(5)
>>> a
[1, 2, 3, 4]
>>> b
[1, 2, 3, 5]
Интересно, что в реализации CPython уже есть довольно обширный слой статичных атрибутов у классов, которые внутри называются «слоты»:
https://github.com/python/cpython/blob/master/Objects/typeobject.c#L5074
Это не те слоты, которые __slots__ у класса. Это сишная ссылка в структуре класса на методы, доступ к которым также можно получить через питон, и даже можно определить в новом классе на питоне методы с этим именем, и эти методы попадут в слот класса, то есть, код на Си сможет вызвать метод по указателю, методом лексического связывания, вместо запроса по имени метода, динамическим связыванием. Те же арифметические операции (сложение и вычитание) попадают в специальные слоты для операций с числами. В основном такой механизм применяется к методам, но есть также и данные: тот же __slots__ класса, который является слотом PyHeapTypeObject->ht_slots, или __dict__, который хранится в самом экземпляре объекта по смещению PyTypeObject->tp_dictoffset. Как вы видите, без статических структур реализовывать интерпретатор было не прикольно.
Вывод типов
Стоит помнить, что смысл питона был в том, чтобы избавить пользователя от мороки явного описания структуры объектов. Явные описания типов и атрибутов повсеместно в коде нужны только там, где у кода кишки торчат наружу, как то было у Си, где каждый модуль тусовался сам по себе, а потом линкер соединял их вместе, мол «это двенадцатиперстная, ее нужно будет подключать к тонким кишкам; а вот толстая кишка — она будет выводиться в тип "попа", не вздумай выводить ее в "рот"», и все типы этих подключений должны были быть описаны с обоих взаимодействующих сторон. Однако, если вы полностью контролируете алгоритмы, то вам обычно не нужны типы. Взять знаменитую строчку великих людей «Кукарек<кококо> кукарек = new Кукарек<кококо>()», которая в современной джаве приняла намного более приятную форму «var кукарек = new Кукарек<кококо>()». Также, из опыта людей по написанию фронтендов на ReasonML выяснилось, что практически единственное место, где нужны явные типы — это интерфейсы к JS либам, а во всех остальных местах работает вывод типов, который подбирает нужные типы по инициализации и доступу.
PyPy, а также аналогичные V8 для JavaScript и LuaJIT, испытывают проблемы с выводом типов до выполнения программы, потому они предпочитают конкретизировать типы уже после выполнения кода. Отсюда возникает проблема избыточного использования ресурсов из-за компиляции во время выполнения, и проблема параллелизации, которая не может происходить во время оптимизации и разоптимизации функций. По этой причине так активно развиваются проекты AOT компиляции, как то asm.js, WebAssembly, и почивший с миром PNaCl.
Давайте поверхностно пробежимся по истории развития идеи вывода типов:
- Bauer, A.M. and Saal, H.J. (1974). Does APL really need run-time checking? Software — Practice and Experience 4: 129–138.
- Kaplan, M.A. and Ullman, J.D. (1980). A scheme for the automatic inference of variable types. J. A CM 27(1): 128–145.
- Borning, A.H. and Ingalls, D.H.H. (1982). A type declaration and inference system for Smalltalk. In Conference Record of the Ninth Annual ACM Symposium on Principles of Programming Languages (pp. 133–141)
- https://ru.wikipedia.org/wiki/Standard_ML — 1984 год.
Насколько мне известно, Standard ML был первым языком, который полноценно опирался на вывод типов, а не использовал его в качестве дополнительного инструмента.
Конечно, вывод по Хиндли-Милнеру больше подходит для функциональных языков и достаточно простой системы типов, в которой полный вывод составных типов не приводит к безграничному числу справедливых сочетаний конкретных типов. К сожалению, обычно математики отвратительно программируют, а программисты — ничего не могут понять в математике, потому довольно долгое время математики прыгали на единорогах по радуге, выдавая бесполезные абстрактные модели (чем они до сих пор и занимаются), пока постепенно чудом не возникло локальное выведение типов:
- Frank Pfenning. (1988). Partial polymorphic type inference and higher-order unification. In Proceedings of the 1988 ACM Conference on Lisp and Functional Programming, pp. 153–163
- Cardelli, Luca; Martini, Simone; Mitchell, John C.; Scedrov, Andre (1994). An extension of system F with subtyping. Information and Computation, vol. 9. North Holland, Amsterdam. pp. 4–56
- Benjamin C. Pierce, and David N. Turner. (1997). Local type inference. Indiana University CSCI Technical Report #493, pp. 1-25
оно же в
(1998) Local type inference. POPL '98 Proceedings of the 25th ACM SIGPLAN-SIGACT symposium on Principles of programming languages, pp. 252-265
и
(2000). Local type inference. ACM Transactions on Programming Languages and Systems (TOPLAS). Vol. 22(1), pp. 1-44
Последний вариант вывода типов — это примерно то, вокруг чего возникла Scala, создание которой началось в 2001 году.
В 1991 году приходит Гвидо, в 1994 — Расмус, в 1995 — Юкихиро «Matz», и никто из них не слышал о выводе типов. Можно говорить о том, что отсутствие системы типов сделало обучение языку проще, благодаря чему они и получили такое распространение. Однако, сейчас пришло время, когда система типов нужна и без нее — никуда, потому что есть очень много кода на питоне, его тяжело поддерживать, он медленно работает, а для параллелизации выполнения приходится использовать внешние средства, вроде ZeroMQ, RabbitMQ, Kafka. Отсутствие же времени компиляции и статичных типов в питоне на фоне того, что полный код программы обычно известен до выполнения, в итоге отнимает очень много сил у разработчика, который потом в этом минном поле неопределенности пытается проверять ошибки в программе при помощи тестов и убогих статических анализаторов, и все равно пропускает гору простейших ошибок, потому что по мере роста количества кода все варианты ветвей выполения и типов данных удерживать под контролем становится всё сложнее и сложнее. Почему еще в начале двухтысячных язык не начал мигрировать в сторону вывода типов, когда гугл сделал на питон ставку на границе веков? Не знаю. Руби все-таки начал движение в эту сторону, которое известно как язык Crystal, но там с совместимостью дело печально обстоит, насколько мне известно.
Итог
Я — глупый и наивный максималист. У вас не составит труда показать мне моё место, или пояснить, как вы могли бы сделать лучше.
Комментарии (178)
sheknitrtch
24.12.2019 15:38+11Мне кажется, byko3y, вы неправильно позиционируете язык Python. Вы критикуете фичи языка, которые лежат в основе Python. Нельзя ругать динамически типизированный язык за то, что он динамически типизированный.
Однако же, по логике вещей,
Откуда такая логика? Потому что каком-то другом языке так? Не стоит по умолчанию тянуть концепции из других языков в Python.x[0].append(0)
должно было бы создать новый или взять уникальный список из нулевого элемента и добавить в него нуль
Оператор присвоения и статическая типизация… Ожидается вывод
С чего вы это решили? Если меняете значение переменной, то поменяется результат вычислений. Это работает во всех языках.
Стоит помнить, что смысл питона был в том, чтобы избавить пользователя от мороки явного описания структуры объектов.
Наверное, это самое странное утверждение в статье. Смысл Python зафиксирован в The Zen of Python. Всё остальное — ваши домыслы.byko3y Автор
24.12.2019 22:38Вы критикуете фичи языка, которые лежат в основе Python. Нельзя ругать динамически типизированный язык за то, что он динамически типизированный.
У питона динамическая строгая типизация. У питона вполне хорошо выражены типы, и обычно переменные хранят единственный тип в себе. По этой причине динамичность — это не приговор, как рак терминальной стадии.
Однако же, по логике вещей, x[0].append(0) должно было бы создать новый или взять уникальный список из нулевого элемента и добавить в него нуль
Откуда такая логика? Потому что каком-то другом языке так? Не стоит по умолчанию тянуть концепции из других языков в Python.
Наверное, это самое странное утверждение в статье. Смысл Python зафиксирован в The Zen of Python. Всё остальное — ваши домыслы.
Ну а как же "Explicit is better than implicit" и "practicality beats purity"? Ссылки на массивы формируют неявное поведение, которое непрактично. Я соглашусь разве что с тем, что нельзя менять поведение для старого кода — новые списки должны быть явно обозначены. Здесь фундаментальная проблема заключается даже не в списках, а в отсутствии явного разграничения значений и ссылок на значения.
С чего вы это решили? Если меняете значение переменной, то поменяется результат вычислений. Это работает во всех языках.
Я здесь затронул проблему опечаток, случайных ошибок программиста, которые неизбежны. Может быть он и неудачный, но в целом проблема актуальна. Конкретно в этом примере аннотация типа вместо со статическим валидатором помогли бы, но есть еще сто случаев, где они не помогут или выдадут ошибки там, где их нет.
arthuriantech
24.12.2019 23:05+2Ссылки на массивы формируют неявное поведение, которое непрактично. Я соглашусь разве что с тем, что нельзя менять поведение для старого кода — новые списки должны быть явно обозначены. Здесь фундаментальная проблема заключается даже не в списках, а в отсутствии явного разграничения значений и ссылок на значения.
В питоне все значения имеют ссылочный характер. Любая переменная является ссылкой на значение (указателем/ссылкой в терминах C/C++), передача по значению отсутствует. Однако существуют неизменяемые объекты (int, float, tuple, str, frozenset etc), когда при модификации с ссылкой связывается новое вычисленное значение вместо модификации "по месту". Именно поэтому x[0] += 1 с tuple не работает.
https://ru.wikipedia.org/wiki/Стратегия_вычисления#Вызов_посоиспользованию(call_by_sharing)
byko3y Автор
25.12.2019 04:29+1В питоне все значения имеют ссылочный характер
Кодеру, который пишет на питоне, обычно глубоко безразлично, то ли целочисленное значение переменной формально является ссылкой, то ли нет. Фактически он имеет значение, и это значение нельзя поменять даже если на него ссылается несколько переменных.
>>> a = 1 >>> b = a >>> b += 1 >>> print(a, b) 1 2
Это поведение передачи по значению, а не передачи по ссылке, пусть и реализовано внутри оно как ссылка на что-то там. Особенно если учесть, что в структурах CPython много самых разных ссылок на разные структуры, и эти ссылки неизвестны большинству сидящих здесь, даже если они много пишут на питоне.
В случае copy-on-write операций со значеним программист, опять же, видит только передачу по значению, и его не должен особо волнует сложный механизм, который отслеживает ссылки и копирует значения при изменениях.
Именно поэтому x[0] += 1 с tuple не работает
Но работает x += (1, ). Что есть нецелостность языка. Как мне создать новый кортеж, в котором нулевое значение увеличено на единицу?
Ответhttps://stackoverflow.com/questions/11458239/python-changing-value-in-a-tuple
t = ('275', '54000', '0.0', '5000.0', '0.0') lst = list(t) lst[0] = '300' t = tuple(lst)
arthuriantech
25.12.2019 08:52Но работает x += (1, ). Что есть нецелостность языка.
Ларчик открывается просто.
>>> x = () >>> id(x) 140362334412872 >>> x += (1,) >>> id(x) 140362217698360
Для иммутабельного tuple выражение
x += (1,)
эквивалентноx = x + (1,)
. Присваивается новое значение. Так работают все иммутабельные типы в Python. Мутабельные меняются "по месту".byko3y Автор
25.12.2019 20:53-1Для иммутабельного tuple выражение x += (1,) эквивалентно x = x + (1,). Присваивается новое значение. Так работают все иммутабельные типы в Python. Мутабельные меняются "по месту".
Да? Так а почему x[0] += 1 не создает новое значение? Не буду мучать вопросами: потому что стандартная реализация операции STORE_SUBSCR не может видеть ничего дальше своего носа: у нее есть аргумент, она может работать только с ним. Даже несмотря на то, что изменение указателя "x" нам доступно, и потому мы можем неограничено менять значение. В том числе, это следствие бедности синтаксиса питона, не разделяющего изменяемые и неизменяемые данные, значение и ссылку на значение.
arthuriantech
27.12.2019 03:09+1Да?
Да.
Так а почему x[0] += 1 не создает новое значение?
Потому что присваивание элемента запрещено для иммутабельного tuple. Выражение
x[0] += 1
не может создать новыйx[0]
, поскольку такое выражение делает следующее:
i = x[0] i += 1 x[0] = i
Операция
x[0] = i
не поддерживается tuple. Он никогда не меняет свои ссылки.byko3y Автор
27.12.2019 12:27Операция x[0] = i не поддерживается tuple. Он никогда не меняет свои ссылки.
Однако же, меняет в «x += (1,)». Меняет в будние дни с 8 до 17, кроме четверга, в остальные дни — не меняет.
arthuriantech
27.12.2019 12:39byko3y Автор
27.12.2019 13:01Ну что ж, тогда мне, наверное, придется придумывать выход. Выход начнется с примера:
>>> a = ([1, 2], ) >>> a[0].append(3) >>> a ([1, 2, 3],)
А тут изменилось. Всё это — просто следствие плохого проектирования языка, нецелостность реализации. Здесь у нас в реализации можно менять значение, а здесь — нельзя. Tuple — вроде неизменяемое значение, но одновременно изменяемое. По хорошему, нужно было либо запрещать хранить в кортеже изменяемые значения, либо делать копии. Первое не прокатывает, потому что смотри выше: «следствие плохого проектирования языка» — кортежи используются для передачи аргументов в функции и аргументы должны быть изменяемы, потому что как-то же нужно передавать ссылки на объекты в функции, когда нет деления на ссылки и значения. Но если котрежи, как отдельные типы данных, опасны в применении, то почему они остались? Смотри выше: «следствие плохого проектирования языка». На языке Гвидо это называет «питоничность», и формально записано в виде стихов про «будет у тебя счастье, будет у тебя и горе, будешь ты бедный, но и будет у тебя много денег».
arthuriantech
28.12.2019 15:49+1Кортеж поддерживает иммутабельность ссылок, а не значений. Это сильно облегченная версия массива для хранения и передачи последовательности элементов, а вовсе не наоборот, как ошибочно написано в статье.
По хорошему, нужно было либо запрещать хранить в кортеже изменяемые значения, либо делать копии.
Зачем? Если вам нужны иммутабельные элементы в контейнере, используйте для этого иммутабельные значения. Просто
((1, 2), 1)
, никаких проблем. Не думайте, что иммутабельность в Python должна быть эквивалентом константности из языка С.byko3y Автор
28.12.2019 05:23+1Кортеж поддерживает иммутабельность ссылок, а не значений. Это сильно облегченная версия массива для хранения и передачи последовательности элементов, а вовсе не наоборот, как ошибочно написано в статье
Уже в 1990 году в питоне были списки и кортежи — с тех пор они не изменились.
https://github.com/python/cpython/blob/85a5fbbdfea617f6cc8fae82c9e8c2b5c424436d/Objects/tupleobject.c
https://github.com/python/cpython/blob/85a5fbbdfea617f6cc8fae82c9e8c2b5c424436d/Objects/listobject.c
Если брать ABC, который Гвидо называл главным вдохновителем, то можно увидеть, что в языке вообще нет ссылок — это язык значений:
https://homepages.cwi.nl/~steven/abc/language.html
https://homepages.cwi.nl/~steven/abc/types.html
https://homepages.cwi.nl/~steven/abc/qr.html
В то же время, в Modula-3, с которой Гвидо в то время ознакомился, ссылки были вполне явные:
http://www.opencm3.net/doc/tutorial/m3/m3_22.html#SEC22
Да, можно считать, что кортежи появились после списков. Более того, можно считать, что списки питона появились после списков ABC, потому сами являются производными, как и кортежи — ничего подобного не было в ABC.
Если вам нужны иммутабельные элементы в контейнере, используйте для этого иммутабельные значения
Программисту не нужны неизменяемые значения — он хочет иметь возможность их менять. Может быть, он и не будет ею пользоваться, но он хочет ее иметь. Программист обычно не хочет работать со ссылками — его интересуют только значения. То есть, он пишет «config.db_cache.enabled = False», потом пишет «config.app_cache.enabled = True», и ожидает, что после этого «config.db_cache.enabled == False and config.app_cache.enabled == True», но с таким же успехом он может получить в результате «config.db_cache.enabled == True and config.app_cache.enabled == True», потому что где-то неявно связались config.db_cache и config.app_cache. Например:
class Config: pass class Cache: pass config = Config() default_cache = Cache() config.app_cache = default_cache config.db_cache = default_cache config.db_cache.enabled = False config.app_cache.enabled = True print(config.db_cache.enabled, config.app_cache.enabled)
Я понимаю, что современные продвинутые макаки научились обходить эти грабли, но это не причина для того, чтобы делать вид, будто граблей нет.
onegreyonewhite
28.12.2019 03:23Всё это — просто следствие плохого проектирования языка, нецелостность реализации. Здесь у нас в реализации можно менять значение, а здесь — нельзя. Tuple — вроде неизменяемое значение, но одновременно изменяемое.
В вашем утверждении банальная логическая ошибка. Tuple как состоял из одного массива, так и состоит. Этот массив всё тот же. Его состав изменился, но массив всё тот же.
Исходя из ваших домыслов вы делаете заключение, что язык плохо спроектирован, но больше похоже на то, что вы просто не хотите принимать того факта, что вы чего-то не поняли и просто пришли "со своим уставом в чужой монастырь".
Если в C++ классе объявить protected объект, то, насколько мне известно, в его инстансе можно вызывать любые public-методы из этого объекта, которые могут изменить внутренний состав. Это тоже плохо спроектированный язык получается по вашей логике.
Есть хорошая штука —__slots__
, которая определяет, что новые атрибуты не должны появляться внезапно, есть наработки у Instagram, которые действительно выглядят удобными и полезными (очень хотелось бы увидеть эту функциональность в CPython).
Но то, что вы предлагаете нарушает саму логику CPython из-за чего многим это и не понравилось, судя по комментам и минусам.
Вы где-то выше/ниже писали, что мало пишите на Python, но развивать язык, который по сути не знаешь, мне кажется заведомо плохой идеей: либо вас никто не поймёт, либо вы протолкнёте свои идеи и возможно тем самым убьете или сильно обновите community языка (что тоже не всегда хорошо).
Я согласен с вами в том, что CPython нужны перемены (хотя одного перехода с мажорной версии хватило по уши), но нужны перемены типа strict-модулей, более продуманных импортов. Но в остальном Python прекрасный язык. Когда не хватает статической типизации, мы используем Cython, когда нужно сделать что-то очень быстрое, то мы используем C API, когда нужно что-то очень быстро реализовать, то мы используем CPython. Это одна из причин, почему его любят.byko3y Автор
28.12.2019 05:52Tuple как состоял из одного массива, так и состоит. Этот массив всё тот же. Его состав изменился, но массив всё тот же.
У list тоже массив. Тоже один. Просто сделан через хранение указателя на массив в объекте, а не хранением самого массива в объекте. как в кортеже.
вы просто не хотите принимать того факта, что вы чего-то не поняли и просто пришли "со своим уставом в чужой монастырь"
Я просто не хочу принимать того факта, что питон плохо спроектирован и с этим ничего нельзя сделать. Я постоянно пытаюсь придумать, что можно сделать.
Если в C++ классе объявить protected объект, то, насколько мне известно, в его инстансе можно вызывать любые public-методы из этого объекта, которые могут изменить внутренний состав. Это тоже плохо спроектированный язык получается по вашей логике.
C++ — это квитнэссенция худших решений. Питон неплохо смотрится на этом фоне. По этой причине программирование на C++ сводится к тому, чтобы избежать 10 проблемных фич и использовать одну удачную. Я много лет назад взял за правило не использовать модификаторов доступа строже чем public. За эти годы я не могу вспомнить ни одной проблемы, которая бы после этого у меня возникла из-за того, что какой-то другой программист обратился к структурам, которые ему не стоит трогать — обычно просто проект переставал компилироваться и сразу указывал на проблемное место.
Есть хорошая штука — __slots__, которая определяет, что новые атрибуты не должны появляться внезапно
Очень странная интерпретация, которая отличается от официальной документации.
>>> class A(): >>> __slots__ = ['value'] >>> >>> a = A() >>> a.value = 1 >>> A.value2 = 'asd' >>> print(a.value2) asd
Что я делаю не так? У меня новые атрибуты внезапно появились.
есть наработки у Instagram
А вот за это спасибо, очень интересно.
arthuriantech
28.12.2019 07:22Что я делаю не так? У меня новые атрибуты внезапно появились.
Нарочно стреляете себе в ногу в доказательство якобы плохой архитектуры языка.
0xd34df00d
25.12.2019 05:35Вы критикуете фичи языка, которые лежат в основе Python. Нельзя ругать динамически типизированный язык за то, что он динамически типизированный.
Так, получается, никакие языки ругать нельзя.
Нельзя ругать язык с UB за то, что он с UB.
Нельзя ругать язык с тремя-пятью встроенными тьюринг-полными языками за то, что он… Ну вы поняли.
Есть мнение, что динамическая типизация — плохо (и вообще оксюморон). Непонятно, почему это мнение нельзя высказывать.
AlexBin
25.12.2019 18:46Так, получается, никакие языки ругать нельзя.
Почему же? Языки ругать можно и даже нужно за недостатки. Динамическая типизация это не недостаток. Лишь в ваших глазах это недостаток, но единого мирового консенсуса по этому поводу не существует.
Есть мнение, что динамическая типизация — плохо
Вот и ругайте динамическую типизацию, это даже полезно для сообщества, возможно однажды установится однозначная истина, и статическую типизацию признают единственно верным путем развития всех языков. А пока такого консенсуса нет, ругать языки за динамическую типизацию, это как прийти на концерт Гуфа с плакатом «рэп говно».0xd34df00d
25.12.2019 20:20Почему же? Языки ругать можно и даже нужно за недостатки. Лишь в ваших глазах это недостаток, но единого мирового консенсуса по этому поводу не существует.
А можно примеры каких-то деталей языков программирования, по которым существует мировой консенсус?
Кроме того, на мой взгляд, движение к добавлению статической типизации (не только в питон, но и в js, например) как-то намекает, что большие проекты таки проще писать с типизацией, чем без.
возможно однажды установится однозначная истина, и статическую типизацию признают единственно верным путем развития всех языков
Не, ну почему всех. Я вот на днях оформил некую последовательность команд как шелл-скрипт — там типизация не особо нужна, например (ибо нетривиальной логики никакой нет,
set -e
в начале хватает).AlexBin
25.12.2019 20:50А можно примеры каких-то деталей языков программирования, по которым существует мировой консенсус?
Так вот я о том и толкую. Если бы существовали какие-то серебряные пули по большинству вопросов, не было бы в мире столько языков.
Мне конечно на ум приходит «goto это плохо», но даже тут я не уверен, что консенсус достигнут, хоть перевес и очевиден. Кроме того, если рассматривать этот вопрос с точки зрения сферы применения, то я думаю для разных языков и разных людей goto в разной степени уместен или неуместен.
Если взять мое мнение, то я считаю статическую типизацию более правильной. Кроме того, я с нее начинал. Но так вышло, что на практике даже в динамическом языке я редко сталкиваюсь с проблемами типизации, и получается, что статическая типизация будет только вносить в мою работу дополнительные ограничения и бойлерплейт, ограждая меня от ошибок, которые я допускаю весьма редко, и которые до продакшена обычно не доходят. Плюс возможно влияет сфера деятельности.
Поэтому моя любовь к статической типизации плавно перетекла в платоническую.0xd34df00d
25.12.2019 21:33Так вот я о том и толкую. Если бы существовали какие-то серебряные пули по большинству вопросов, не было бы в мире столько языков.
Из этого, и из предыдущего вашего комментария тогда получается, что ругать языки таки нельзя. Ну, чисто формальной логикой.
Но так вышло, что на практике даже в динамическом языке я редко сталкиваюсь с проблемами типизации, и получается, что статическая типизация будет только вносить в мою работу дополнительные ограничения и бойлерплейт, ограждая меня от ошибок, которые я допускаю весьма редко, и которые до продакшена обычно не доходят.
Она просто разной бывает. Типизация в стиле джавы, да, будет мешать и вести к бойлерплейту. В стиле хаскеля — мм, нет, наоборот.
chapuza
25.12.2019 21:49В стиле хаскеля — мм, нет, наоборот.
Вы всерьез считаете, что в Хаскеле не нужен несуразных размеров бойлерплейт в более-менее серьезном проекте в каком-нибудь не очень изведанном домене?
На мой взгляд — разумный компромисс — это строгий паттерн матчинг везде, и статический анализатор кода, как в эрланге. Строгая типизация мне лично всегда только мешает, и в Хаскеле — особенно.
0xd34df00d
25.12.2019 21:53Вы всерьез считаете, что в Хаскеле не нужен несуразных размеров бойлерплейт в более-менее серьезном проекте в каком-нибудь не очень изведанном домене?
Бойлерплейт — нет.
А обмазаться типами побыстрее да посерьёзнее мне как раз помогает не допускать ошибок при последующем рефакторинге или изменении логики. Особенно если домен неизведанный.
Vilaine
25.12.2019 21:49бойлерплейт
Какой бесполезный бойлерплейт вам приходится писать?
Мне кажется, большинство людей недооценивают типизацию. Подразумевая не более, чем «тут я в любом случае передаю число, зачем мне проверки компилятора». Мой тезис в том, что типизация существует всегда — либо в голове программиста, либо в коде, и лучше бы уменьшить нагрузку на голову засчет кода.
Athari
26.12.2019 22:46Мне конечно на ум приходит «goto это плохо», но даже тут я не уверен, что консенсус достигнут, хоть перевес и очевиден.
До тех пор, пока остаются паттерны использования goto, которые не покрываются синтаксисом языков, консенсуса быть не может. Консенсус может быть, возможно, по "вермишельному" использованию goto, но так сейчас никто не пишет, а самые грубые использования goto (переход внутрь функции, обход инициализации и др.) запрещены всеми полноценными языками.
byko3y Автор
27.12.2019 01:42Осталось несколько красивых и эффективных паттернов, которые так и остаются во власти goto: быстрый выход из нескольких циклов, освобождение ресурсов при отстутвии RAII, переход по состояниям автомата и некоторые другие
- быстрый выход из нескольких циклов не делается при помощи goto. Нужно понимать, что условный переход и выход из цикла по структуре Дейкстры на уровне машинных кодов делается через те же переходы, но они фундаментально отличаются от перехода по goto структурированностью. Выход из нескольких блоков циклов-условий структурирован, потому что выходит из жестко заданного числа блоков. Ту же структуру можно было бы переписать на выходах одного уровня с дополнительными проверками флагов, но без этого код читается и пишется намного приятнее;
- переход по состояниям автомата. Конечные автоматы нынче всерьез применяют разве что в промышленных контроллерах, где исполняемые устройства представляют собой естественные автоматы, вроде «кран открыт, кран полуоткрыт, кран закрыт» или «активен инструмент 1, инструмент 2, инструмент 3, инструмент не активен». У конечных автоматов есть большой недостаток — они плохо наращиваются, поскольку роста числа состояний приводит к экспоненциальному росту числа переходов. Для бесконечных автоматов goto идет лесом, потому что никто не будет делать программу с бесконечным числом goto — для этого применяются другие способы. Ну и таблицы функций никто не отменял, в конце-концов — это вполне себе структурные способы оформления конечного автомата;
- освобождение ресурсов при отсутствии RAII. Я так понимаю, имеется в виду что-то вроде «goto cleanup» в функции после выделения ресурса. К сожалению, эта запись по сути является сахаром вокруг блочного оформления выделения-высвобождения.
Пример:
FILE *f1 = fopen("./file1", "r"); if (!f1) return; FILE f2 = fopen("./file2", "r+"); if (!f2) goto cleanup1; f3 = fopen("./file3", "a"); if (!f3) goto cleanup2; do(f1, f2, f3); fclose(f3); cleanup2: fclose(f2); cleanup1: fclose(f1);
Это сахар вокруг:
FILE *f1 = fopen("./file1", "r"); if (f1) { FILE f2 = fopen("./file2", "r+"); if (f2) { f3 = fopen("./file3", "a"); if (f3) do(f1, f2, f3); fclose(f3); } fclose(f2); } fclose(f1);
И да, во многих современных языках эта проблема учтена. Но она не имеет отношения к goto.
До тех пор, пока остаются паттерны использования goto, которые не покрываются синтаксисом языков, консенсуса быть не может. Консенсус может быть, возможно, по "вермишельному" использованию goto
Консенсус отсутствует, в основном, у людей, для которых нет развлечений в жизни, кроме как всю жизнь доказывать другим людям, что черное — это белое, а белое — это черное, и что именно они — это те самые люди, которые возглавляет элитную группу Знающих. Упомянутый выше Таненбаум — это именно такой кадр, но есть огромное количество таких мудаков: ими забиты институты и коммитеты стандартизации. Комитеты сделали Algol 68 — Вирт сделал Pascal.
Athari
27.12.2019 02:09быстрый выход из нескольких циклов не делается при помощи goto
Шта? Во всех языках, где нет "break 2" или "break fooFor", выход из циклов делается через "goto afterFor". Не знаю, в какие ваши определения что и как вписывается, но пишут в коде "goto". (Или, если начитались Вирта, добавляют флаги на каждый уровень.)
У конечных автоматов есть большой недостаток — они плохо наращиваются
ОК, нужно редко, ограничено и всё такое. Тем не менее, задача есть, и с помощью написания текста "goto" в исходнике задача решается красиво.
К сожалению, эта запись по сути является сахаром вокруг блочного оформления выделения-высвобождения
Любой код с goto можно переписать без goto введением флагов. Суть моего посыла в том, что код с goto в некоторых случаях выглядит лучше, а не то, что без goto прожить нельзя. Соответственно, позиция "goto вреден всегда" вредна — и имено это позиция современных кабинетных специалистов, а не наоборот.
Комитеты сделали Algol 68 — Вирт сделал Pascal.
Давайте, расскажите апологету шарпа — самого засахаренного и напичканного фичами мейнстримного языка, — какой он идеалист. =)
byko3y Автор
27.12.2019 04:40Во всех языках, где нет "break 2" или "break fooFor", выход из циклов делается через "goto afterFor". Не знаю, в какие ваши определения что и как вписывается, но пишут в коде "goto"
Что за «все языки»? Кобол? Фортран? Но даже в фортране уже был exit и cycle, которые аналогичны break и continue, а это 1957 год.
ОК, нужно редко, ограничено и всё такое. Тем не менее, задача есть, и с помощью написания текста "goto" в исходнике задача решается красиво.Какая задача? Я хочу ее увидеть.
Любой код с goto можно переписать без goto введением флагов. Суть моего посыла в том, что код с goto в некоторых случаях выглядит лучше, а не то, что без goto прожить нельзя.
Я привел единственный пример, где с goto код выглядел бы лучше, и то можно поспорить, потому что разница небольшая. В данном случае имеет место более общая проблема, характерная для фортрана, Си, паскаля, и еще кучи старых языков — это отсутствие механизмов высвобождения локально выделяемых ресурсов. Давным-давно оперативная память была на вес золота, а запись на накопители была очень медленной, потому программист тщательно обдумывал ввод-вывод и создание больших временных объектов. Уже к 80-м годам у программистов возникла возможность оперировать достаточно большими структурами, и ручное выделение-высвобождение стало проклятием, потому что приводило к ошибкам.
Потому правильный вариант функции выше выглядит так:
_cleanup_fclose_ FILE *f1 = fopen("./file1", "r"); if (!f1) return; _cleanup_fclose_ FILE *f2 = fopen("./file2", "r+"); if (!f2) return; _cleanup_fclose_ FILE *f3 = fopen("./file3", "a"); if (!f3) return; do(f1, f2, f3);
И никакое goto здесь рядом не валялось по лаконичности и чистоте кода.
Давайте, расскажите апологету шарпа — самого засахаренного и напичканного фичами мейнстримного языка, — какой он идеалист.
Что рассказать? Что люди пользуются неудобными технологиями просто потому, что они популярны? Так это мало для кого будет секретом. Вот MS Word — это отвратительный редактор текста, и, тем не менее, все им пользуются. А пользуются все потому, что все пользуются — вот такая вот простая истина. Люди ведь даже не ставят вопрос «должны ли мы использовать MS Word».
Можно осознанно махать кнутом на галере, можно целенаправленно разводить инвесторов стартапа, понимая, что все эти технологии — мусор, занавес, за которым можно грести бабло лопатой; а можно просто выдавать популярные идеи. Эта дихотомия отличается от противостояния с миром людей, которые придерживаются непопулярного мнения просто для того, чтобы придерживаться непопулярного мнения.Athari
28.12.2019 18:58Какая задача? Я хочу ее увидеть.
Конкретная прикладная задача? Не знаю, никогда не надо было.
Что за «все языки»? Кобол? Фортран? Но даже в фортране уже был exit и cycle, которые аналогичны break и continue, а это 1957 год.
Какой COBOL? Как красиво выйти из двух циклов в C, C++, C#, Objective-C, Pascal, Delphi, VB, VB.NET без goto? Да и Python, чего же там (прежде чем вы начнёте придираться к словам, raise-except == goto).
Потому правильный вариант функции выше выглядит так
Очень универсально и портабельно.
И никакое goto здесь рядом не валялось по лаконичности и чистоте кода.
Мы уже поняли, что все языки неправильные. Тем не менее, на них надо писать сейчас, а не в будущем в ваших фантазиях.
Так это мало для кого будет секретом. Вот MS Word — это отвратительный редактор текста
Так говорят те, кто не умеет им пользоваться. "Что, можно не пробелами выравнивать?" "Что, можно стили настраивать?" Да, у ворда есть проблемы, но все альтернативы ещё хуже для большинства ставящихся перед вордом задач.
byko3y Автор
28.12.2019 06:10Какой COBOL? Как красиво выйти из двух циклов в C, C++, C#, Objective-C, Pascal, Delphi, VB, VB.NET без goto? Да и Python, чего же там (прежде чем вы начнёте придираться к словам, raise-except == goto)
Return. Всегда так делаю. Очень удобно делать со вложенными функциями. И да, выхода из вложеного блока не хватает, не спорю.
Очень универсально и портабельно.
__cleanup__ поддерживается GCC и Clang. Ежели хотя бы 20 лет назад это сделали, то вопрос о портируемости нынче бы не стоял. У C++, например, есть огромные проблемы с портируемостью, но никто не жалуется почему-то.
Мы уже поняли, что все языки неправильные. Тем не менее, на них надо писать сейчас, а не в будущем в ваших фантазиях.
Мне сейчас не надо. Кому надо — пусть пишет.
Так говорят те, кто не умеет им пользоваться. "Что, можно не пробелами выравнивать?" "Что, можно стили настраивать?"
Вот именно это я и имел в виду. Когда пользователь создает форматирование на стилях, якорях, и явных разрывах строк, то внезапно от WYSIWYG ничего не остается, и возникает вопрос «а зачем нам тогда MS Word?». MS Word был создан для ровно противоположной задачи: чтобы пользователь никогда не пользовался стилями, якорями, разрывами строк — именно так создают документы большинство.
Да, у ворда есть проблемы, но все альтернативы ещё хуже для большинства ставящихся перед вордом задач
Скорее, ситуация выглядит как «все альтернативные решения вымерли». Хотя, LaTeX еще вполне жив и не планирует умирать, хоть и поживает не очень.
chapuza
27.12.2019 08:56+2Конечные автоматы нынче всерьез применяют разве что в промышленных контроллерах, где исполняемые устройства представляют собой естественные автоматы [...]
О, святой компилятор!
Вы [...] позволяете себе с развязностью совершенно невыносимой подавать какие-то советы космического масштаба и космической же глупости [...]
— М. А. Булгаков, «Собачье сердце»
У нас весь процессинг на FSM. Я не стану описывать, почему, но, подозреваю, что примерно во всем финтехе так.
У конечных автоматов есть большой недостаток — они плохо наращиваются, поскольку роста числа состояний приводит к экспоненциальному росту числа переходов.
Лолшта? Рост числа состояний может приводить к линейному росту числа переходов, к факториальному, и ко всем между.
byko3y Автор
27.12.2019 10:07У нас весь процессинг на FSM. Я не стану описывать, почему, но, подозреваю, что примерно во всем финтехе так.
Зачем тогда отвечать?
Рост числа состояний может приводить к линейному росту числа переходов, к факториальному, и ко всем между.
Если человеку нужно просто последовательно выполнить переходы между состояниями автомата, то он обычно не пользуется явными автоматами. А неявный автомат — это любой алгоритм, который выполняет процессор компьютера. Пусть современный высокопроизводительные процессор давно уже не автомат, а сложный набор автоматов, большая часть которых недоступна программисту — это не важно, я готов признать, что процессор — это автомат. Однако, уже на уровне ассемблера большая часть состояния и переходов этого автомата теряется, потому что состояние и переходы становятся неявными, они абстрагируется компилятором.
chapuza
27.12.2019 10:11Зачем тогда отвечать?
Потому что вы несете несусветную чушь и простой контрпример ее опровергает. Пояснения не требуются.
Если человеку нужно просто последовательно выполнить переходы между состояниями автомата, то он обычно не пользуется явными автоматами.
Да ну? А если переходы условные, зависимые от внешнего мира? Почему вы все время пытаетесь ваши неверные домыслы выдать за истину в последней инстанции?
[...] высокопроизводительные процессор давно уже не автомат [...]
Не знаю, при чем тут это.
byko3y Автор
27.12.2019 10:16+1Да ну? А если переходы условные, зависимые от внешнего мира? Почему вы все время пытаетесь ваши неверные домыслы выдать за истину в последней инстанции?
Не знаю, при чем тут это.Я тоже не знаю. И не вижу смысла продолжать разговор загадками и отвечать вопросами.
AlexBin
27.12.2019 13:08Конечные автоматы нынче всерьез применяют разве что в промышленных контроллерах, где исполняемые устройства представляют собой естественные автоматы [...]
О, святой компилятор!
Соглашусь.
Многие сетевые протоколы удобно делать на конечных автоматах.
BGP
DHCP-Server
TCP
Да и вообще любые протоколы, которые переключаются между состояниями (типа ESTABLISHED, LISTENING, HANDSHAKING), очень удобно делать на автоматах.
Регулярные выражения под капотом имеют автомат, да и вообще лексеры и парсеры делают на конечных автоматах.
В геймдев-анимации автоматами описывают переходы между состояниями «юнит бежит», «юнит в прыжке», «юнит танцует»
Так же в геймдеве можно описывать логику ИИ с помощью автоматов.
Это только то, с чем я лично работал или сталкивался. На деле же, думаю, применение автоматов шире.byko3y Автор
28.12.2019 03:21Многие сетевые протоколы удобно делать на конечных автоматах.
BGP
DHCP-Server
TCP
Да и вообще любые протоколы, которые переключаются между состояниями (типа ESTABLISHED, LISTENING, HANDSHAKING), очень удобно делать на автоматахСетевой протокол было бы удобно описывать описывать автоматом, если бы в нем только менялись режимы, но не передавались никакие данные. Неограниченное разобразие возможных данных и различных сценариев приводят к тому, что сетевые алгоритмы делают на бесконечных автоматах, читай «машина тьюринга». Взять банальный IP: этот протокол не ограничивает кол-во взаимодействующих узлов, для которых нужно хранить взаимосвязь MAC-IP адресов. Таким образом, алгоритму нужно бесконечное состояние, потому IP в общем случае невозможно реализовать на конечном автомате. Отсюда проистекает невозможность реализации TCP/IP на конечном автомате. По ссылке
http://www.tcpipguide.com/free/t_TCPOperationalOverviewandtheTCPFiniteStateMachineF-2.htm
описана воображаемая машина, которая ничего не передает по сети, а только переключает состояния единственного соединения.
Регулярные выражения под капотом имеют автомат, да и вообще лексеры и парсеры делают на конечных автоматах
C/C++ в общем случае невозможно распарсить на конечном автомате. По какой-то такой причине, например, yacc использует стэковую машину, являющуюся бесконечным автоматом, а последняя реализована на конечном автомате + стэк.
Полновесные регулярки perl способны разбирать нерегулярные языки и не могут быть реализованы на конечном автомате. Если ограничить эти регулярные выражения до разбора регулярных языков, то можно ограничиться конечным автоматом.
В геймдев-анимации автоматами описывают переходы между состояниями «юнит бежит», «юнит в прыжке», «юнит танцует»
Да, отдельные примитивы из системы можно назвать конечными автоматами.
Так же в геймдеве можно описывать логику ИИ с помощью автоматов
Только очень тупой ИИ можно реализовать на конечных автоматах.
GCU
24.12.2019 16:00В Python есть аннотация типов, а также механизмы приведения и проверки. Да, это не особо удобно или красиво, но почему «не даёт возможности» тестирования?
Возможности же есть!byko3y Автор
24.12.2019 22:55+1В Python есть аннотация типов, а также механизмы приведения и проверки. Да, это не особо удобно или красиво, но почему «не даёт возможности» тестирования?
Конкретно в этом случае вопрос не в тестировании, а в статической проверке кода. Только в 3.8 родили наконец понятие структурного подтипа: PEP 544 — Protocols: Structural subtyping (static duck typing)
Почему так поздно? Мне кажется, что создатели сами понимают, что и этот механизм очень далек от идеала, и скорее удовлетворяет хотелки людей, которым нравится жить в иллюзии, будто в питон вводится статическая типизация. Стоит понимать, что статическая типизация нынче много кому нужна, но ее нет и в ближайшее время не будет.AlexBin
24.12.2019 23:18которым нравится жить в иллюзии, будто в питон вводится статическая типизация.
Почему вы решили, что все мечтают о статической типизации?byko3y Автор
24.12.2019 23:22+1Почему вы решили, что все мечтают о статической типизации?
TypeScript. Под питон тоже много кандидатов, просто никто из них не дает достаточной пользы для того, чтобы стать мейнстримом.
AlexBin
24.12.2019 23:30TypeScript
Что тайп скрипт? Мой вопрос, почему вы решили, что ВСЕ мечтают. Я не говорил, что никто не мечтает, или что статическая типизация — это плохо. Кому хочется статическую типизацию ушли в тайпскрипт, ничего дурного не вижу. Кому нравится динамическая, пишут на питоне и JS.byko3y Автор
25.12.2019 04:33+1Что тайп скрипт? Мой вопрос, почему вы решили, что ВСЕ мечтают.
Конечно же не все мечтают. Кто-то пишет проекты на 10-20 тысяч строк, где можно наизусть запомнить все алгоритмы и структуры данных. Мечтают все или почти все, кто пишет крупные проекты, и кто постоянно спотыкается об «как мне перестать искать ошибки после каждой правки и начать писать код», а потом пишут в бложиках рассказы про «моя жизнь до и после тайпскрипта».
AlexBin
25.12.2019 08:24Конечно же не все мечтают. Кто-то пишет проекты на 10-20 тысяч строк, где можно наизусть запомнить все алгоритмы и структуры данных. Мечтают все или почти все, кто пишет крупные проекты, и кто постоянно спотыкается об «как мне перестать искать ошибки после каждой правки и начать писать код», а потом пишут в бложиках рассказы про «моя жизнь до и после тайпскрипта».
— Почему вы решили, что все мечтают бросить вредную привычку есть мясо?
— Ну конечно же не все мечтают. Ведь кто-то ест мясо каждый день и не планирует доживать даже до 60, и кто постоянно после каждого употребления мяса клизму ставит себе и другу. А потом в бложиках пишут рассказы «моя жизнь до и после принятия веганства»
chapuza
25.12.2019 09:51-1как мне перестать искать ошибки после каждой правки и начать писать код
Вот только статическая типизация тут поможет хорошо, если процентов на пять. Много хайпа, очень много бойлерплейта, мало настоящей практической пользы, насколько я могу судить, исходя из личного опыта (и опыта команды).
И да, у нас проекты не только на десять строчек.
byko3y Автор
25.12.2019 20:59Вот только статическая типизация тут поможет хорошо, если процентов на пять. Много хайпа, очень много бойлерплейта, мало настоящей практической пользы, насколько я могу судить, исходя из личного опыта (и опыта команды).
И да, у нас проекты не только на десять строчек.Если речь идет про один лишь питон, то я ни сколько не сомневаюсь, что в нем статическая типизация в ее нынешней форме приносит больше вреда, чем пользы. Статью я писал про свершенно другую статическую типизацию, которой не нужны классы и в большинстве случаев не нужны явные объявления типов.
arheops
24.12.2019 20:54+4Что-то не понял из этого потока сознания, зачем автор программирует на Питоне, если ему не нравятся основы языка?
Может, вам просто взять другой язык, C++ или Go например?
В Питоне самый смак в том, что можно запустить консоль и в реальном времени переопределять классы и переменные. Смысл это в нем убирать? Не нужно это? Ну так транслируйте программу(или сразу пишите) в C/C++.
Нужны проверки типов? Используйте сеттеры с проверкой типов, делов то. Нужен запрет переопределения классов? Используйте функцию проверки в конструкторе.
Не нравятся генераторы и множественное наследование? А кто вас заставляет его использовать то? А мне вот нужны, они ложатся в мое мышление хорошо.
Искать ошибку в коде на 1000строк? Так множественное наследование и динамическое выполнение на то и направлено, чтоб локализировать эту ошибку в 30 строках.byko3y Автор
24.12.2019 23:21-1Что-то не понял из этого потока сознания, зачем автор программирует на Питоне, если ему не нравятся основы языка?
Может, вам просто взять другой язык, C++ или Go например?Очень мало пишу на питоне. Я написал в статье, что питон плохо ложится в привычные мне сферы. И даже Go плохо ложится. Очень хорошо ложится C++, но как с молодости не подружились.
Меня устраивают основы питона как языка — я не согласен с нынешней популярной реализацией. А она таки много кого не устраивает — просто, мало кто об этом говорит вслух, мало кто идет дальше «да нужно просто GIL убрать и все проблемы решатся» или «вот, аннотации расставим, и тогда заживем». Не просто так было создано столько альтернативных проектов.
Ну так транслируйте программу(или сразу пишите) в C/C++.
Nuitka и Cython уже есть, но они не особо помогают.
В Питоне самый смак в том, что можно запустить консоль и в реальном времени переопределять классы и переменные. Смысл это в нем убирать? Не нужно это?
Цитируя великих и скромных: «Горячая замена классов нужна для целей отладки и банальной кодописи, но это все-таки должен быть специализированный инструмент для разработчика, а не артефакт во время выполнения, от которого нельзя избавиться».
Нужны проверки типов? Используйте сеттеры с проверкой типов, делов то
Да, вопрос применения дескрипторов атрибутов я затронул в статье. В нынешней реализации они решают лишь одну проблему из дюжины. Что делать с локальными переменными и аргументами функций? Что делать со вложенными структурами?
Нужен запрет переопределения классов? Используйте функцию проверки в конструкторе.
Вот эта магия мне еще не подвластна. Каким образом проверка в конструкторе поможет избежать переопределения класса?
Искать ошибку в коде на 1000строк? Так множественное наследование и динамическое выполнение на то и направлено, чтоб локализировать эту ошибку в 30 строках.
В последнее время не работаю с проектами меньше 500 тыс строк. Самый отвратительный код — это когда единый функционал разбит на много-много файлов и классов, по которым приходится прыгать, чтобы собрать картину воедино. Это не проблема на масштабе в тысячу строк, это проблема на масштабе 100+ тысяч. Именно по этой причине столько людей используют прием "божественный объект", который считается антипаттерном, однако же, при всех его ограничениях и проблемах, он дает замечательную локализацию кода и возможность прочитать единый функционал в одном месте.
arheops
25.12.2019 00:08Вот эта магия мне еще не подвластна. Каким образом проверка в конструкторе поможет избежать переопределения класса?
храните класс в другом классе, который при присвоении проверяет, идет ли переопределение или нет.byko3y Автор
25.12.2019 04:38храните класс в другом классе, который при присвоении проверяет, идет ли переопределение или нет
«Вот два петуха, которые будят того пастуха, который бранится с коровницей строгою, которая доит корову безрогую, лягнувшую старого пса без хвоста, который за шиворот треплет кота, который пугает и ловит синицу, которая часто ворует пшеницу, которая в темном чулане хранится» — в коде, который написал джэк.
Vilaine
25.12.2019 00:59+1когда единый функционал разбит на много-много файлов и классов, по которым приходится прыгать, чтобы собрать картину воедино
Единая картина более менее сложного проекта обеспечивается как раз разбитием кода по определенным правилам. Детали реализации мешают обозревать структуру. God object невозможно эффективно поддерживать из-за ограниченности оперативной памяти. И да, типизация на уровне сигнатур хорошо согласуется с разбиением. Я могу допустить, что без типизированных сигнатур действительно чтение структурированного модульного кода — боль.byko3y Автор
25.12.2019 04:47Детали реализации мешают обозревать структуру.
Если кодер видит только интерфейс без деталей реализации, то ему решительно все равно, как там этот код внутри устроен. Если же ему нужно изменить реализацию, то ему нужно будет найти, какой из пятисот классов отвечает за нужный ему функционал, и с какими двадцатью классами нужно будет наладить взаимодействие методу этого класса для того, чтобы новый функционал правильно работал.
Я люблю к таким разговорам приводить пример паттерна Посетитель, в котором в десять классов пишется тот же код, который в норм языках пишется в десять строчек. Из-за чего некоторые программисты пребывают в иллюзии по поводу того, какова истинная сложность системы, с которой они работают, вроде «вау, я всего-лишь неделю потратил на написание hello world. Боже, какая у нее безупречная архитектура». Примерно тем же занимаются хаскелисты, только в сфере чистой функциональщины.
0xd34df00d
25.12.2019 05:42Примерно тем же занимаются хаскелисты, только в сфере чистой функциональщины.
Там этим занимаются исключительно по желанию, чтобы потом писать смишнявки типа этой.
byko3y Автор
25.12.2019 06:05-1Там этим занимаются исключительно по желанию, чтобы потом писать смишнявки типа этой.
Когда на хаскеле приходится писать императивную программу, то хошь-не хошь, а приходится. Вот эти вот «IO STM Maybe a», unboxed клинопись, и прочее.
0xd34df00d
25.12.2019 06:10+1Всяко лучше, чем «ой, а у меня в STM-транзакции printf случайно, и компилятор вырезал нахрен весь мой код как UB, что делать-то».
А клинопись… Я вот
[x for x in xs]
читать не могу, у меня мозг ломается на четвёртом токене.byko3y Автор
25.12.2019 06:33-1Всяко лучше, чем «ой, а у меня в STM-транзакции printf случайно, и компилятор вырезал нахрен весь мой код как UB, что делать-то».
Речь о том, что для проведения простых императивных операций в хаскеле нужно использовать много конструкций. Он вполне умышленно поощряет чистый функциональный код простыми конструкциями, и не поощряет императивный чрезмерно сложными. Только вот беда — прикладная программа обычно производит огромное кол-во императивных действий. По этой причине цитадель функционального программирования — Lisp, создавалась и десятками лет развивалась как смешанный инструмент.
Я вот [x for x in xs] читать не могу, у меня мозг ломается на четвёртом токене.
Не, это очень простой пример. Намного прикольнее читать вот такой код — здесь даже подсветка синтаксиса не поможет:
https://spapas.github.io/2016/04/27/python-nested-list-comprehensions/
strings = [ ['foo', 'bar'], ['baz', 'taz'], ['w', 'koko'] ] [ (letter, idx) for idx, lst in enumerate(strings) for word in lst if len(word)>2 for letter in word]
И да, list comprehension — это рак. List comprehension в питоне ввели не потому, что он так хорош — дело в том, что альтернативные записи ещё хуже.
Why List Comprehension is Bad
Vilaine
25.12.2019 21:30Если же ему нужно изменить реализацию, то ему нужно будет найти, какой из пятисот классов отвечает за нужный ему функционал, и с какими двадцатью классами нужно будет наладить взаимодействие методу этого класса
Получается, ему нужно читать и все ваши God objects, чтобы разобраться, что к чему? Но ведь с этим и боремся, чтобы нужный интерфейс было легко найти, легко понять его эффекты и изменение логики было локализовано. Так что хоть 500 000 классов, нас они не касаются.
И вообще, я сильно сомневаюсь, что вы пишете весь код в одном файле/классе. Так что и у вас есть какие-то принципы разделения, так что лучше обсуждать, какие именно практики вы поддерживаете, а против каких выступаете.
пример паттерна Посетитель
Рискую показать свою некомпетентность, но я не знаю, зачем нужен этот паттерн, а примеры его использования мне хочется переписать. =) Но с сохранением разделения ответственности.
Ни о какой неделе на hello world речь никогда не идёт, и Enterprise Fizzbuzz — это пример очень плохой архитектуры. Единственная цель архитектуры — это облегчить жизнь разработчику, и каждый принцип этому служит и имеет объяснение «почему это упрощает разработку». Мне кажется, у вас немножко информационный беспорядок, поэтому не стоит разговаривать обобщенно.byko3y Автор
26.12.2019 01:12-1чтобы нужный интерфейс было легко найти, легко понять его эффекты и изменение логики было локализовано
Вот, именно для этого и нужен God object — чтобы нужную реализацию было легко найти и изменение логики было локализовано, а не размазоно тонким слоем по 20 файлам, как это обычно бывает в том же Java. Именно из-за отсутствия локализации кода реализации и возникла инструментальная работа с кодом и инструменты рефакторинга, которые в хороших языках не нужны или нужны очень-очень редко.
я сильно сомневаюсь, что вы пишете весь код в одном файле/классе
Конечно. Напомню определение: «Боже?ственный объе?кт (англ. God object) — антипаттерн объектно-ориентированного программирования, описывающий объект, который хранит в себе «слишком много» или делает «слишком много».»
Оно не обязывает иметь единственный объект — оно просто говорит про много/мало.
Поясню на примере. Вот монолит, стена текста, фу-фу костыли:
a = 1 b = 2 print(a+b)
А вот — грамотно спроектированная архитектура:
class Value: def accept(self, v): raise Exception('Not implemented') class IntValue(Value): def __init__(self, value): self.value = int(value) def accept(self, v): return v.visit(self.value) class Visitor: def visit(self, value): raise Exception('Not implemented') class IntVisitor(Visitor): def visit(self, value): return int(value) class UnaryOperation: def op(self, value): raise Exception('Not implemented') class BinaryOperation: def op(self, value1, value2): raise Exception('Not implemented') class AdditionOperation(BinaryOperation): def op(self, value1, value2): return value1 + value2 class PrintOperation(UnaryOperation): def op(self, value1): print(value1) a = IntValue(1) b = IntValue(2) addition = AdditionOperation() tmp = addition.op(a.accept(IntVisitor()), b.accept(IntVisitor())) PrintOperation().op(tmp)
Мне уже не так смешно, как может быть вам, потому что я видел подобное воочию.
Ни о какой неделе на hello world речь никогда не идёт, и Enterprise Fizzbuzz — это пример очень плохой архитектуры.
Тем не менее, сейчас в проекте, за который мне дают еду, до 8 уровней вложенности папок в модуле на 60 тыс строк. И это JavaScript.
Vilaine
26.12.2019 02:04+1Вы забыли вынести эту операцию в микросервис в Docker, управляемом Kubernetis, который запущен на кластере эмуляторов калькулятора Texas Instruments. Если вы думаете, что этим примером что-то показали, то нам стоит прекратить дискуссию.
byko3y Автор
26.12.2019 02:56-1Вы забыли вынести эту операцию в микросервис в Docker, управляемом Kubernetis, который запущен на кластере эмуляторов калькулятора Texas Instruments
Да, у меня есть знакомые, которые занимаются организацией хелло ворлдов в кластерах докера. Только не на Kubernetes, а на Docker Swarm. Конечно, я преувеличиваю. Но не намного.
tmteam
27.12.2019 13:11God object почти всегда используют из-за сложности развязывания зависимостей, и сложностей с соблюдением с SRP. Осознанных GO я помню от силы процента 3
0xd34df00d
25.12.2019 05:39Нужны проверки типов? Используйте сеттеры с проверкой типов, делов то.
Это поздно, эти проверки уже в рантайме.
В Питоне самый смак в том, что можно запустить консоль и в реальном времени переопределять классы и переменные.
А можно, кстати, запустить консоль, загрузить файл и сделать доступными все определения из него, но не выполнять код?
А можно загрузить файл, поменять его, а потом перезагрузить так, чтобы результат был эквивалентен чистому интерпретатору с единожды загруженным файлом?
Искать ошибку в коде на 1000строк? Так множественное наследование и динамическое выполнение на то и направлено, чтоб локализировать эту ошибку в 30 строках.
Сильные выразительные статические типы для этого подходят лучше.
AlexBin
25.12.2019 18:49Сильные выразительные статические типы для этого подходят лучше
Они лучше подходят для локализации ошибок типизации, но не для любых ошибок.
А для локализации любых ошибок (включая ошибки типизации), как раз подходит хорошая архитектура. Я только не согласен с тем, что это обязательно должно быть множественное наследование, или наследование как таковое, или вообще ООП. Есть же разные методы архитектурить, ООП — лишь один из них.0xd34df00d
25.12.2019 20:21Они лучше подходят для локализации ошибок типизации, но не для любых ошибок.
Безусловно. Только системы типов бывают разные, и более сильные позволяют выражать больше вещей на уровне типов, и сводить больше вещей к проверкам типов.
AlexBin
25.12.2019 20:55Безусловно. Только системы типов бывают разные, и более сильные позволяют выражать больше вещей на уровне типов, и сводить больше вещей к проверкам типов.
Тут не могу не согласиться. Но факторов много, и сложно их всех объективно проанализировать.
arthuriantech
24.12.2019 22:32+3не дают модульно тестировать. Правильно отработавший в тесте кусок кода может выдать ошибку при работе целой системы, и никак вы от этого не защититесь в рамках CPython;
Это проблема модульного тестирования, которая решается интеграционным тестированием системы в целом. Причем тут питон?
создают большие сложности оптимизации. Объявление класса не дает вам гарантии по поводу фактической работы класса. По этой причине единственный успешный проект оптимизатора, PyPy, использует трассировку для обнаружения фактической последовательности выполняемых действий методом пробы;
Это следствие динамизма и утиной типизации, основных фич питона. Разумеется, что класс (как и почти любая вещь в питоне) может быть изменен во время выполнения. Даже байкод.
не состыковываются с параллельным выполнением кода. Например, тот же multiprocessing работает с копиями определений классов, и если вы не дай бог измените описание классов в одной из копий, то ваше приложение рискует развалиться.
Потому что multiprocessing работает с разными процессами, у которых внезапно разные адресные пространства. Причем тут питон?
Более тонкий вариант динамических классов — это переопределение доступа к атрибутам через getattribute, getattr и прочие. Зачастую они используются в качестве обычных геттеров-сеттеров, для делегации функций объекту-полю, и изредка — для организации DSL.
И что из этого следует? Эти магические методы обычно используются в библиотеках, фреймворках, и там, где необходимо переопредлить логику поиска атрибутов. В обычном коде встречается редко.
Горячая замена классов нужна для целей отладки и банальной кодописи, но это все-таки должен быть специализированный инструмент для разработчика, а не артефакт во время выполнения, от которого нельзя избавиться.
(См. выше) К слову, почему "это все-таки должен быть специализированный инструмент"? Что подразумевается под этим? Он должен быть статическим?
Это прямо-таки запущенный случай GoTo, когда выполнение не просто бесконтрольно прыгает по коду — оно прыгает по стэкам.
Я не понимаю каким образом сопрограммы и корутины являются запущенным случаем goto. Исключения, continue, break, with, return тоже можно рассматривать как частный случай goto.
float a = 2.0; float *src = &a; char *dest = malloc(sizeof(float)); memcpy(dest, src, sizeof(float)); printf("%f", *(double*)dest);
Вы описали так называемый type punning, который может быть использован для реализации рудиментарного полморфизма, но сам по себе им не является.
В статье много рациональных зёрен, но они размазаны по стене текста с неясными претензиями.
byko3y Автор
25.12.2019 00:15Это проблема модульного тестирования, которая решается интеграционным тестированием системы в целом. Причем тут питон?
При том, что много языков проблемы систематической поломки тщательно оттестированного модуля не имеют. Да, есть проблемы с повреждением памяти у C/C++, но для того и придумали Java/C#.
Это следствие динамизма и утиной типизации, основных фич питона. Разумеется, что класс (как и почти любая вещь в питоне) может быть изменен во время выполнения. Даже байкод.
Утиная типизация есть у интерфейсов Go: если у структуры имя метода совпадает с именем метода в интерфейсе, то считается, что для структуры реализован метод для этого интерфейса, даже несмотря на то, что интерфейс не упомянут в структуре и вообще мог быть написан после написания структуры. Например:
https://putridparrot.com/blog/interfaces-and-duck-typing-in-go/
Что не помешало Go быть статически типизированным и статически компилированным.
Разумеется, что класс (как и почти любая вещь в питоне) может быть изменен во время выполнения
Много чего в CPython нельзя изменить во время выполнения: огромные слои стаднартной реализации неподвластны динамичности, я упомянул лишь один пример в статье — невозможность переопределять стандартные списки. Как там было… «вы можете выбрать автомобиль любого цвета, если этот цвет — чёрный».
Потому что multiprocessing работает с разными процессами, у которых внезапно разные адресные пространства. Причем тут питон?
Pickle же ж. Схоронили один класс, а достали уже в другой. Изменяемые определения классов — это почти всегда зло. Они не зло только при написании и отладке. Конкретно в случае параллельного выполнения задач изменяемые определения классов становятся злом в квадрате.
переопределение доступа к атрибутам через getattribute, getattr и прочие
И что из этого следует? Эти магические методы обычно используются в библиотеках, фреймворках, и там, где необходимо переопредлить логику поиска атрибутов. В обычном коде встречается редко.
Потенциальная возможность динамического изменения класса при выполнении и возможность изменять доступ к атрибутам во время выполнения сами по себе закрывают целый пласт возможностей параллелизации и оптимизации, поскольку в общем случае нужно всегда держать наготове второй интерператор, а для частного случая можно просто написать расширение на Си.
Исключения, continue, break, with, return тоже можно рассматривать как частный случай goto.
По порядку:
- исключения, действительно, проблематичны, особенно на фоне того, что нынче в CPython принято использовать их штатно, например, для остановки итераций, а не только лишь в исключительных случаях, как можно предположить из названия;
- continue, break, return — это справедливые переходы для программы в форме иерархии Косарайю, которая считается модификацией оригинального структурного программирования Дейкстры. Эти конструкции не нарушают иерархию кода, давая выходы по конкретным логическим блокам, в отличие от исключений, обработка которых прыгает как попало. К слову, в Go исключения убрали, а continue, break, return — оставили;
- with — не понимаю претензии, где тут goto? Вызов функции, как и вызов вложенного блока в нотации with, не нарушает структурированности кода.
Вы описали так называемый type punning, который может быть использован для реализации рудиментарного полморфизма, но сам по себе им не является.
Приведение типов считается некой разновидностью полиморфизма, и лишь от определений и конкретных критериев зависит, будем ли мы называть приведение «настоящим полиморфизмом» или же плюшевым.
В статье много рациональных зёрен, но они размазаны по стене текста с неясными претензиями
К сожалению, у меня нет конкретных конечных целей, и я не хотел ограничивать возможность действий чем-то вроде «завтра же начинаем делать транслятор питона с такими и такими функциями».
arthuriantech
25.12.2019 01:45Pickle же ж. Схоронили один класс, а достали уже в другой. Изменяемые определения классов — это почти всегда зло. Они не зло только при написании и отладке. Конкретно в случае параллельного выполнения задач изменяемые определения классов становятся злом в квадрате.
Cпойлерbyko3y Автор
25.12.2019 05:09Почему Python должен быть статически типизированным и статически компилируемым?
А почему нет? Статически он прекрасно компилируется, между прочим. Но на фоне сложных динамических структур данных выхлоп от этого получается смешной.
Встроенные типы являются исключением. Они не подлежат изменению по соображениям производительности
Типы из расширений питона — это тоже «встроенные типы», которые «не подлежат изменению»? А они таки не подлежат.
можно унаследоваться от встроенного типа и переопределить или дополнить его поведение, а из-за утиной типизации в общем случае нет нужды ожидать именно list или dict — достаточно объекта, который реализует набор методов.
Да, только код обрастает уродливыми конструкциями, вроде «a = MyDict({'first': 1, 'second': 2})». Никто ведь не заставляет весь сишный код интерпретатора изменять тип создаваемых объектов — я просто хочу поменять смысл конструкций «{...}» и «[...]» в моем собственном коде.
Такая гибкость является намеренной фишкой Python, которая отличает его от других языков.
Плохая архитектура реализации не является какой-то отличительной чертой питона — много чего было плохо реализовано в других языках. У PHP не было классов вообще: ни динамических, ни статических — и, тем не менее, он был популярнее питона. Как так?
Там же, где try/except/finally, только под другим соусом.
Если говорить о том, что with является неявным блоком try...finally — да, согласен. Но у него есть основной, линейный алгоритм выполнения.
Вы же не нарекаете на то, что вытесняющая многозадачность средствами ОС нарушает структурированность исходного кода?
Многопоточный код нарушает структурированность алгоритма, но это нарушение дает вкусные плюшки — возможность работы на нескольких процессорах. По этой причине однопоточные программы так долго существуют на наших устройствах. Сопрограммы async/await и генераторы нарушают структурированность алгоритма, но не дают сравнимых плюшек, а лишь дают альтернативную запись для алгоритмов с функциями-callback-ами.
arthuriantech
25.12.2019 09:33+2Честно говоря, мне сложно с вами спорить из-за сильной расплывчатости формулировок и пространных рассуждений. Какое нарушение? Какого алгоритма? Что?
Да, типы, методы и функции из расширений тоже являются built-in, и нет, определенно не нужно менять смысл выражений (...), [...] и {...} в Python. Все, пощадите мой мозг)
byko3y Автор
25.12.2019 21:24-1Честно говоря, мне сложно с вами спорить из-за сильной расплывчатости формулировок и пространных рассуждений. Какое нарушение? Какого алгоритма? Что?
Многопоточную программу нельзя представить как алгоритм иерархической структуры. Именно это я и называю «нарушает структурированность алгоритма».
https://ru.wikipedia.org/wiki/Структурное_программирование
По этой причине так медленно многопоточные приложения входят в индустрию — их по прежнему крайне сложно писать.
Да, типы, методы и функции из расширений тоже являются built-in, и нет, определенно не нужно менять смысл выражений (...), [...] и {...} в Python
Мне нужно в моем коде создать мою структуру данных. Если при это страдает читаемость, то можете написать Гвидо письмо о том, что читаемость программы в питоне на самом деле на важна — пусть он будет в курсе.
AlexBin
25.12.2019 13:26+1я просто хочу поменять смысл конструкций «{...}» и «[...]» в моем собственном коде
Ого! Да минует меня участь читать ваш код после этого или работать с вами в одной команде в проектах где свыше 10-20 килострок кода.byko3y Автор
25.12.2019 21:26Ого! Да минует меня участь читать ваш код после этого или работать с вами в одной команде в проектах где свыше 10-20 килострок кода.
А вот теперь представьте себе, что в JavaScript переопределение стандартного Array считается нормой. Не то, чтоб я был согласен с этой ситуацией — просто для справки, чтобы было ясно, что потребность есть, и ее довольно много.
AlexBin
25.12.2019 18:50+1У PHP не было классов вообще: ни динамических, ни статических — и, тем не менее, он был популярнее питона. Как так?
Могу поделиться сокральной тайной. Язык программирования любят/ненавидят не за один-два критерия, а за совокупность и согласованность целого множества критериев разного веса и за их комбинации.
Иначе получится картина:
— Алёша, за что ты любишь Машу?
— У нее платье красивое
tsukanov-as
24.12.2019 22:38+1Лично я не вижу особой разницы между записью method(object) и object.method — особенно если method является статичной функцией, которой, в общем-то, все равно, какой первый аргумент (self) принимать.
Разница в том, что реализация method у разных object разная )
Ну т.е. если даже вот совсем отказаться от классов и использовать только функции первого класса, то все равно придется писать так:object.method(object)
ps Но возможно я недопонял о чем речь.byko3y Автор
24.12.2019 23:28-1Разница в том, что реализация method у разных object разная
Тебя покалечили ООП. Функция — это функция, данные — это данные. Разные функции работают с разными данными, они сами по себе не принадлежат каким-то объектам. В питоне можно вполне законно вызвать метод объекта с произвольным значением вместо self:
class Test(): value = "hello" def test(self): print(self.value) class A(): pass a = A() a.value = "world" Test.test(a)
Механизм принадлежности к объекту или классу — это лишь некоторые варианты установления соответствия. Один из, но далеко не единственный.
tsukanov-as
24.12.2019 23:39Ну ok, попробую иначе объяснить )
def foo(x): x.bar()
Как в данном случае сделать вызов аналогичный x.bar() без методов и их аналогов на функциях первого класса? Ведь очевидно что мы не знаем заранее какую функцию вызвать. Как отвечать на сообщение «bar» знает только объект x.
ps В том сообщении я возможно внес путаницу примером: object.method(object). Это не всегда нужно, да. Я хотел сделать акцент на форме вызова: object.method()byko3y Автор
25.12.2019 05:20def foo(x):
x.bar()
Как в данном случае сделать вызов аналогичный x.bar() без методов и их аналогов на функциях первого класса?Например:
def foo(x): bar(x)
Не то, чтоб я предлагал именно эту форму записи — все-таки, методы в питоне принято записывать через точку, а bar() обычно ссылается на некоторую простую функцию bar в глобальном пространстве имен.
Как отвечать на сообщение «bar» знает только объект x.
Обычно никому не нужно такое условие, потому что метод объявлен в классе и диспетчеризация идет по классу. Я подозреваю, что сложность создания методов у экземпляра объекта была создана вполне целенаправлено, чтобы поменьше использовали monkey patching. Я же предлагаю привязать сложные случаи диспетчеризации к отдельным методам, а не к сложной иерархии классов.
tsukanov-as
25.12.2019 12:41+1def foo(x): bar(x)
Можно внутри bar сделать if по типам, да. Вполне законно и это будет такая же диспетчеризация. Но есть одно но. Придется bar дописывать под каждый новый тип.
Мне кажется что полиморфизм все таки удобнее )byko3y Автор
25.12.2019 21:31Можно внутри bar сделать if по типам, да. Вполне законно и это будет такая же диспетчеризация. Но есть одно но. Придется bar дописывать под каждый новый тип
Если "if" будет дописывать и выполнять компилятор во время компиляции, то проблем для писателя софта не возникнет.
arthuriantech
25.12.2019 00:12+1Попробуй сделать
int.__hash__()
вместо hash(int) и многое прояснится само собой.
https://docs.python.org/3/reference/datamodel.html#special-method-lookupbyko3y Автор
25.12.2019 03:06Попробуй сделать int.__hash__() вместо hash(int) и многое прояснится само собой.
Я не совсем понимаю, при чем тут хэш от класса к вопросам диспетчеризации. Описанное по ссылке — это некоторые детали костыльности реализации слотов в CPython, а именно: в слоты класса не попадают атрибуты экземпляра объекта, и встроенные функции используют именно слоты класса. hash(int) делает то же, что и type.__hash__(int), поскольку именно type является классом для int.
Zanak
24.12.2019 23:01-1Спасибо автору за статью. Хотя я и пишу на нем уже лет 10, я как та домохозяйка, что не задумывается об устройстве автомобиля, она просто садиться и едет за покупками. Было интересно.
bormotov
25.12.2019 03:22нужно не забыть поплнить запасы попкорна к моменту, когда будут аналогичный «разбор» хаскеля. Или эрланга.
byko3y Автор
25.12.2019 05:39-1нужно не забыть поплнить запасы попкорна к моменту, когда будут аналогичный «разбор» хаскеля
Я мог бы по хаскелю сделать разбор, но это совершенно безполезно: хаскель изначально спроектирован как игрушечный язык для защиты диссертаций. Речь идет про ленивость вычислений по умолчанию и диспетчеризацию по типу возвращаемого значения (монады-монадки) — оба они создают большие трудности в определении точной последовательности выполняемых программой действий, что, если и не полностью отрезает возможность, сильно усложняет написание и отладку императивного кода. А императивный код — это, например, весь ввод-вывод.
Все-таки, статью я писал не просто для болтовни, а для того, чтобы получить какие-то конкретные идеи по внесению минимальных правок в имеющийся питон. Для той питоновой либы, которую я сейчас пишу, но еще не опубликовал, позарез нужен новый стиль программ, поскольку ни генераторы, ни изменяемые классы туда ну вообще никак не вписываются, желательно было бы иметь какие-то конкретные структуры вместо аморфной динамики, а вопросы изменяемости значений оказывают огромное влияние на результат. Все эти проблемы не сильно мешают в однопотоке, и я бы, наверное, пять раз подумал, прежде чем начинать проект, если бы заранее знал все проблемы, которые описал в статье.
0xd34df00d
25.12.2019 06:08+1хаскель изначально спроектирован как игрушечный язык для защиты диссертаций
[citation needed]
Он изначально спроектирован как открытая замена проприетарной миранде.
Речь идет про ленивость вычислений по умолчанию
У неё есть проблемы, но не те, что вы упоминаете дальше: а именно, например, использование одной и той же структуры как для данных/рекурсии, так и для коданных/корекурсии мешает иметь некоторые хорошие формально доказуемые свойства. Но большинству программистов на это плевать, и это, наверное, ожидаемо.
Ну и вы можете говорить о трудности оценки потребления памяти, но, глядя на современные креативы на обычных императивных языках, не могу сказать, что эта задача как-то особенно специфична для хаскеля. А вот написать агрегацию и подсчёт статистики логов в O(1) на хаскеле как раз тривиально за счёт ленивости. Успехов на каком-нибудь C++ обмазываться всякими итераторами-ренджами.
и диспетчеризацию по типу возвращаемого значения (монады-монадки)
Какое отношение диспетчеризация имеет к монадам?
В любом случае, любой тайпкласс-констрейнт из Haskell2010 вы можете заменить на явно переданный словарь функций (что, тащем, в нутрях компилятора и происходит), получив что-то типа vtbl из вашего любимого ООП-языка, только лучше (потому что компилятору доступно больше информации, и он может специализировать код почти всегда — я даже не могу придумать случая кроме existentials, где вы явно просите стирание типа, где он это бы не мог).
— оба они создают большие трудности в определении точной последовательности выполняемых программой действий
Опять же, в этом они трудности не создают.
что, если и не полностью отрезает возможность, сильно усложняет написание и отладку императивного кода.
А для описания, в том числе, императивного кода как раз и придумали монады. С контролем эффектов.
А императивный код — это, например, весь ввод-вывод.
Которого у вас в программах 99%, конечно же.
А вообще, не спорю, удобно, когда монадок и контроля за эффектами нет: можно невозбранно внедрять бекдоры в функцию сравнения строк (которая, конечно, одна из тех функций, кому позарез нужен IO). Если я вдруг захочу этим заниматься, то я, конечно же, выберу питон или жс, а хаскель не выберу, там это неудобно делать.
byko3y Автор
25.12.2019 07:05Он изначально спроектирован как открытая замена проприетарной миранде.
Да. Но Миранда была спроектирована именно как абстрактная фантазия. А значит, хаскель тоже стал абстрактной фантазией.
У неё есть проблемы, но не те, что вы упоминаете дальше: а именно, например, использование одной и той же структуры как для данных/рекурсии, так и для коданных/корекурсии мешает иметь некоторые хорошие формально доказуемые свойства…
Ну и вы можете говорить о трудности оценки потребления памяти...Написав программу на хаскеле, я не могу ответить на вопрос «что делает эта программа?». У меня вызвалась func1, а потом func2, или же func2, а потом func1? Или и вовсе вызвалась только func1? Даже если допустить, что формально побочных эффектов у них нет — есть такое побочный эффект, как время выполнения и потребляемая память, которые не всегда получается вынести за скобки. Из-за этой проблемы, например, минимальные правки кода дают разницу производительности выполнения на порядки. У человека, пишущего диссертацию, есть условно неограниченное время на пердолинг, но у программиста, пишущего прикладную программу, есть ограничения и по времени разработки, и по времени выполнения. Прикладному программисту не нужно иметь хорошо формально доказуемые свойства, потому что он не будет эти свойства доказывать. Для него лучшее доказательство — это фактическая работа программы и удовлетворенность пользователя.
Какое отношение диспетчеризация имеет к монадам?
Во время компиляции же ж. По канону это именуется полиморфизмом: компилятор по типу (заранее известному) возвращаемого контейнера выбирает конкретную монадическую функцию. Это именно ad-hoc вариация, которая близка по духу к диспетчеризации, даже если конкретная функция сама полиморфна.
А для описания, в том числе, императивного кода как раз и придумали монады. С контролем эффектов.
Монады — это сложный инструмент для простых действий. Подтверждение этому — число статей в интернете «что такое монада?», в которых лично я без продвинутого знания хаскеля ничерта не смог понять. И зачем нужна такая сложная конструкция? Чтобы написать «выполни мне одну команду вслед за другой».
Которого у вас в программах 99%, конечно же.
Весь GUI, внезапно, состоит из побочных эффектов. Почему на хаскеле его и не пишут.
А вообще, не спорю, удобно, когда монадок и контроля за эффектами нет: можно невозбранно внедрять бекдоры в функцию сравнения строк
Это разговор про неуловимого Джо: программа не может нанести вред, если она ничего не делает и ей никто не пользуется.
mayorovp
25.12.2019 08:44Написав программу на хаскеле, я не могу ответить на вопрос «что делает эта программа?». У меня вызвалась func1, а потом func2, или же func2, а потом func1? Или и вовсе вызвалась только func1?
А нахера это знать?
Даже если допустить, что формально побочных эффектов у них нет — есть такое побочный эффект, как время выполнения и потребляемая память, которые не всегда получается вынести за скобки.
Для достаточно сложной программы это так на любом языке будет. Профайлеры человечество не просто так придумало, и впервые их начали применять вовсе не для Хаскеля.
Во время компиляции же ж. По канону это именуется полиморфизмом: компилятор по типу (заранее известному) возвращаемого контейнера выбирает конкретную монадическую функцию. Это именно ad-hoc вариация, которая близка по духу к диспетчеризации, даже если конкретная функция сама полиморфна.
Не могу разгадать смысл этого абзаца. Кажется, вы используете термины не в том значении, в котором их обычно используют.
byko3y Автор
25.12.2019 22:02Написав программу на хаскеле, я не могу ответить на вопрос «что делает эта программа?». У меня вызвалась func1, а потом func2, или же func2, а потом func1? Или и вовсе вызвалась только func1?
А нахера это знать?
Есть большое число вещей. которым важен порядок: файлы, сетевые протоколы, текстовый и графический интерфейс, общие структуры данных. То есть, практически всё, что связано с прикладными задачами, а не абстрактными моделями. В RTL хаскеля огромное кол-во императивного кода написано просто для того, чтобы компенсировать непредсказуемость программы на хаскеле, чтобы правильно инициализировать ресурс и высвободить его независимо от порядка, в который взбредет компилятору скомпилировать программу. И в итоге получается, что программист пишет программу не столько на хаскеле, сколько на RTL. Примерно как на Racket пишут, используя Scheme в качестве вспомогательного языка.
Для достаточно сложной программы это так на любом языке будет. Профайлеры человечество не просто так придумало, и впервые их начали применять вовсе не для Хаскеля.
Да, только в других языках профилированием можно достичь предсказуемой производительности кода.
Не могу разгадать смысл этого абзаца. Кажется, вы используете термины не в том значении, в котором их обычно используют.
Понятия диспетчеризации, полиморфизма, и приведения типов близки друг к другу. Если полиморфизм из времени компиляции переходит во время интерпретации, то он становится диспетчеризацией или приведением типа. Я не вижу смысла искать четкой границы между ними — ее тяжело найти, да и нет особой разницы в конечном итоге — это детали конкретной реализации. В общем случае это называется «полиморфизм». Я просто хотел написать, что приведение типов — это тоже частный случай полиморфизма.
mayorovp
28.12.2019 15:04Файлу важен порядок работы с файлом. Но это не то же самое, что порядок вызова функций.
Я просто хотел написать, что приведение типов — это тоже частный случай полиморфизма.
Хорошо, а какое отношение это всё имеет к монадам?
byko3y Автор
28.12.2019 06:35Файлу важен порядок работы с файлом. Но это не то же самое, что порядок вызова функций
Элементарный пример:
https://stackoverflow.com/questions/31342012/read-and-writing-to-file-in-haskell
«resource busy (file is locked)» — типичная для хаскеля проблема, вызванная ленивостью. И это только работа с целым файлом — если пытаться читать/писать его по частями, то там проще сразу застрелиться.
Хорошо, а какое отношение это всё имеет к монадам?
—
диспетчеризацию по типу возвращаемого значения (монады-монадки)
Основной механизм в хаскеле для выбора конкретных «функций для реализации последовательного выполнения» (монадических функций) — это тип возвращаемого значения на каждом шаге. То есть, все функции в рамках одной монады должны иметь один и тот же тип-контейнер — благо, этот тип автоматически подставляется там, где не задан жестко.
Страшны не столько сами монады, сколько этот механизм выбора конкретной функции по типу возвращаемого значения, который заставляет всегда делать приведение к типу монады в рамках единого конвеера. И в итоге программисту приходится сильно прогибаться для выполнения банальных операций, вроде «выполни действие 1, потом выполни действие 2, потом выполни действие 3», потому что работа в монадах отличается от обычных вызовов функций, и программист должен помнить «вот здесь я в монаде, а тут — нет» — и этот ком растет с ростом сложности приложения, потому что одни монады-контейнеры накладываются на другие. Поэтому, к слову, нет ни одного крупного проекта, написанного на хаскеле — где-то в районе десятков тысяч строк сложность становится неподъемной для хомо сапиенс.
bormotov
26.12.2019 02:54хаскель изначально спроектирован как игрушечный язык для защиты диссертаций
а питон изначально для чего?
а потом, как он развивался?
какие-то конкретные идеи по внесению минимальных правок в имеющийся питон
Кажется, процесс развития питона устроен так, что любые идеи можно оформлять в PEP, выносить на суд общественности, и есть шансы, что их реализация будет в следующих версиях.
позарез нужен новый стиль программ, поскольку ни генераторы, ни изменяемые классы туда ну вообще никак не вписываются
когда-то давно, в моём кругу общения была ходовая фраза «хорошо быть молодым и сумасшедшим»
Еще «байка из прошлого»: в одно время, был человек (лет через пять после того я вспомнил, пытался найти — никаких не смог), у которого был сайт, и был опубликован прототип интерпретатора proton. Запускался сам, имел какой-то не очень сложный способ, как портировать исходники питоновской stdlib в его протон. На уровне базовых концепций были очень близоки. Автор утверждал, что вот это результат работы, примерно месяца fulltime, с нуля.
С тех пор, средства разработки очень далеко шагнули вперед, не только по удобству, но даже и «по ширине» — больше всего уже написано готового. Не говоря уже о прям «новой жизни» целого направления «транспайлеров» (трансляторов, препроцессоров, итд — как не назови). Это еще проще, не нужно возиться с реализацией низкого уровня.
Добрый совет: попробуйте вашу бодрость духа, направить в русло получения результата, который можно ощутить.byko3y Автор
26.12.2019 03:14а питон изначально для чего?
а потом, как он развивался?Питон был языком для написания системных скриптов. Его предок, ABC, был языком для обучения и для непрограммистов. Потом они развивались плохо.
Кажется, процесс развития питона устроен так, что любые идеи можно оформлять в PEP, выносить на суд общественности, и есть шансы, что их реализация будет в следующих версиях
Не кажется — это реальность. Есть конкретные идеи? Если придерживаться идей из моей статьи в полном объеме, то PyPy мне видится более приятным фундаментом, поскольку там уже много чего из стандартной библиотеке переписано на питоне (вместо Си).
bormotov
26.12.2019 21:44PyPy так PyPy.
У меня никаких идей нет, этап «хочется сделать инструмент который лучше тем есть» у меня прошел уже давно. Все ваши тезисы для меня совсем другая вселенная. Но это, конечно, не значит, что эта ваша вселенная хуже.
Вот то, что совершено точно повторяется и в моей вселенной, что тратится больше сил на обсуждение, чем требуется для начала получения результата.
Проектировать, конечно нужно, но (в моей вселенной) если речь идет об инструменте, важно соблюдать баланс, между затратами на проект-реализация, и тем, что новый инструмент позволит сэкономить.byko3y Автор
27.12.2019 01:50-1Вот то, что совершено точно повторяется и в моей вселенной, что тратится больше сил на обсуждение, чем требуется для начала получения результата
Нужно быстро заработать деньги — торгуй наркотой. Конкретно я пошел в программирование потому, что мне нравится писать программы. И мне не нравится, когда мне не нравится писать программы. Раньше программисты день писали программу, которая за секунду сделает вычисления, которые вручную делаются за два часа — сейчас тенденция идет обратная, и люди предпочтут помахать веслами пару часов, чем решить задачу красиво. Одно из следствий этой тенденции — активное накопление технического долга, когда веслами махать приходится месяц на фоне того, что первый программист всё так же тратит день, чтобы потом программа работала секунду.
bormotov
27.12.2019 12:00Не все задачи нужно решать красиво. Очень часто, решить быстро, важнее, чем решить красиво. Самый простой маркер — это решение нужно всего один раз.
p.s. и да, не все задачи вообще нужно решать. Лучший код — это код, который не написан.byko3y Автор
27.12.2019 12:37Очень часто, решить быстро, важнее, чем решить красиво. Самый простой маркер — это решение нужно всего один раз
Ну тогда кодер и вовсе не нужен. Я все-таки пытался намекнуть куда-то в сторону technical debt, которую нынче принято наращивать до небес в свете удешевления железа.
bormotov
28.12.2019 21:16Не понимаю, что значит «кодер» во фразе «кодер не нужен». Кто это?
Ближайшие лет 20, точно, для 90% задач, железо дешевле чем грамотный человек. Мы, раз в несколько лет проверяем.
Очень много кода, может быть написано один раз, работать годами, и «держать нагрузку», просто добавлением железа. К тому времени, как в этот код возникает необходимость смотреть глазами и править, очень высокая вероятность, что исходные требования поменялись настолько, что экономически выгоднее решить эту новую задачу заново.
За пару-тройку лет инструменты делают заметные шаги вперед.
За лет 5-6, драматические.
Берем любой живой язык программирования (у которого в пределах этого года выходили новые сборки), и смотрим историю изменений за 5 лет.byko3y Автор
28.12.2019 06:43Ближайшие лет 20, точно, для 90% задач, железо дешевле чем грамотный человек
Да. 90% задач дешевле выполнять без человека. Часто люди придумывают себе проблемы, которые вообще не нужно было решать. И именно наличие достаточно дешевых кодеров дает возможность попытаться решить задачу человеческими руками. У меня есть знакомый из Москвы, который для Рено в Экселе делает расчеты по экономике/бухгалтерии. Какой 1С, о чем вы?
chapuza
27.12.2019 09:03этап «хочется сделать инструмент который лучше тем есть» у меня прошел уже давно
Если это не краеугольный камень языка / библиотеки, вытащи который — и все развалится, я всегда отправляю пулл реквесты. Почему нет, зачем отказываться от удобств, которые прямо тут, за углом, стоит только руку протянуть?
Но, разумеется, я не сотрясаю воздух на хабре, предлагая все переписать, в таких случаях :)
bormotov
27.12.2019 11:57видимо не удалось сразу сразу донести мысль:
1. практическая оценка (выгода-усилия), очень редко бывает положительной
2. уже есть очень много разных удобных инструментов.
Чем более масштабный проект (типа язык питон), тем больше цена за отправку «пулл-реквеста». Попробуйте прикинуть, сколько потребуется вложить сил, что бы пройти через процедуру PEP.
При оценке, скажем, в деньгах, вот эти «удобства», тоже почти всегда остаются на уровне восприятия отдельных людей.
Если уйти немного в философию — можно изменять мир под себя, а можно жить в гармонии с миром. Реальность, разумеется, всегда некий компромисс, и моя текущая точка баланса сдвигается в сторону «жить в гармонии с миром».
Это позволяет, например, лучше слышать других людей, лучше понимать, что вокруг происходит.
byko3y Автор
25.12.2019 08:05-1Про numba не слышали?
Не слышал. Я узкоспециализированными оптимизаторами как-то не интересовался.
axi1
25.12.2019 13:25+1А вы посмотрите. Не особо-то он и узкоспециализированный. Там многие вещи, на которые вы сетуете, решены.
byko3y Автор
25.12.2019 18:42А вы посмотрите. Не особо-то он и узкоспециализированный. Там многие вещи, на которые вы сетуете, решены.
Язык Numba всего-лишь не поддерживает классы питона. Почти питон, ага.
axi1
25.12.2019 22:53Классы там есть. Синтаксис не весь, правда, поддерживается. Множественного наследования, в частности, нет. Но вам оно и не надо.
«Официант, во-первых, что за дрянь вы мне принесли, во-вторых, почему так мало?»
byko3y Автор
26.12.2019 01:28Классы там есть. Синтаксис не весь, правда, поддерживается. Множественного наследования, в частности, нет. Но вам оно и не надо.
from numba import jitclass, types dict_ty = types.DictType(types.int64, types.unicode_type) @jitclass([('d', dict_ty)]) class NotInitilisingContainer(object): def __init__(self): self.d[10] = "apple" # this is invalid, `d` is not initialized NotInitilisingContainer() # segmentation fault/memory access violation
Проблемы решены? Это фактически структуры Cи, просто синтаксис их определения подражает питоновым классам. Питоновые класы он не поддерживает.
axi1
27.12.2019 08:00Вы же сами писали, что вам сишные классы нравятся, а питоновские — нет. Ну вот здесь и есть сишные классы, а всё остальное — питоновское. Ровно как вам надо. numba активно развивается. И классы там полноценные будут со временем. Если вам надо прямо сейчас — возьмите и допишите. Всё опенсорсное.
byko3y Автор
27.12.2019 08:04Вы же сами писали, что вам сишные классы нравятся, а питоновские — нет. Ну вот здесь и есть сишные классы
Вообще не помню, чтобы я писал что-то подобное когда-то. Особенно учитывая то, что в Си нет понятия классов.
axi1
27.12.2019 10:33Ну разумеется я имел в виду c++.
Статью я писал про свершенно другую статическую типизацию, которой не нужны классы и в большинстве случаев не нужны явные объявления типов.
Зачем вам тогда понадобились классы в numba?
byko3y Автор
27.12.2019 12:21Зачем вам тогда понадобились классы в numba?
В статье упоминалась типизация через типажи/traits, в стиле утиной типизации. Это совсем не похоже на классы в C++.
Проблема классов в Numba заключается в том, что nopython режим не умеет взаимодействовать с каноничными классами питона, на которых много чего в питоне написано и с которыми привыкли работать макаки. Я не исключаю, что в каком-то будущем Numba доработают, сделают конфеткой, но пока что сыро, очень сыро. Как я писал «либо быстро, либо питон».
axi1
28.12.2019 14:15Ну нету серебрянной пули, как говорил Брукс. У каждого языка есть свои плюсы, свои минусы. numba закрывает большую часть проблем, которые вы здесь обозначили (в частности, удобство тестирования, оптимизации и параллельного выполнения кода). Если вы любого человека, пишущего на питоне, считаете макакой, зачем вы вообще за него взялись? Или вы вообще всех программистов считаете макаками, и себя в том числе? ;)
byko3y Автор
28.12.2019 06:46numba закрывает большую часть проблем, которые вы здесь обозначили (в частности, удобство тестирования, оптимизации и параллельного выполнения кода).
В Numba параллелизация еще более ограничена, чем классы.
Если вы любого человека, пишущего на питоне, считаете макакой, зачем вы вообще за него взялись? Или вы вообще всех программистов считаете макаками, и себя в том числе?
Можно и так сказать. Меня не особо волнует, кто там макака, а кто — нет.
Dorsay
25.12.2019 18:15+2Судя из прочитанной статьи и комментов вы как раз один из 99. Потому как знаете как должно быть, но не знаете что для этого надо сделать.)
На мой взгляд, все вертится вокруг проблемы, что тяжело разобраться в ранее кем-то написанном коде. Но проблема тогда не в Питоне, а в написании кода и желании программиста написать код, который будет понятен всем его (программиста) последователям без разницы в размере LoC
"… Впрочем… разве что чудо? Хорошо, я готов принять на веру. Хороши ваши стихи, скажите сами?" (с)
В общем и целом на мой взгляд такие статьи можно написать про любой язык программирования а «Разруха не в клозетах, а в головах. ...» (с) :)
С уважением
vvvvvvv
25.12.2019 18:17Зачем в начале упоминается GDScript? Он с питоном не связан никак кроме похожего синтаксиса и не является языком общего назначения.
byko3y Автор
25.12.2019 19:09Зачем в начале упоминается GDScript? Он с питоном не связан никак кроме похожего синтаксиса и не является языком общего назначения
В этом абзаце я упоминал питоноподобные языки, которые могли бы быть перспективными в качестве замены устаревшему питону, но не смогли. Общее назначение GDScript мог бы приобрести, но излишняя многословность убивает весь смысл питоновости.
Обычно игрострой выбирает Lua, но мой опыт написания логики для игр на Lua говорит, что без типизации контролировать корректность программы тяжело. Это одна из причин, почему так долго мучали Stalker и не могли выпустить его. По этой причине порядочный создатель движка должен придумать свой скриптовый язык, как Bethesda сделала свой Papyrus, как Blizzard свою модификацию Lua со строгой типизацией. Точно так же создатели Godot посмотрели на имеющиеся open source языки, и обнаружили, что не существует достаточно простого языка с приемлимой производительностью выполнения, который, в отличие от Lua, давал бы какие-то гарантии по поводу стабильности работы скрипта.
https://docs.godotengine.org/ru/latest/getting_started/scripting/gdscript/gdscript_basics.html#historyvvvvvvv
26.12.2019 10:20И какие гарантии дает GDScript? Он такой же динамический как и Lua. По ссылке сказано что его придумали только потому что задолбались писать байндинги внутренностей движка к языку общего назначения.
byko3y Автор
26.12.2019 19:43И какие гарантии дает GDScript? Он такой же динамический как и Lua
Если посмотреть на тезисы моей статьи и на отличия GDScript, то можно увидеть, что они прошлись почти по всем ним, то есть:
- убрали из языка генераторы и менеджеры контекстов;
- убрали исключения. Этого тезиса не было в моей статье, но он был в комментариях к ней. Также, смотри обсуждение исключений в GDScript: https://github.com/godotengine/godot/issues/7643. Исключения рушат структуру кода, создают проблемы в параллелизации и оптимизации, которые очень важны в GDScript, в отличие от CPython;
- переработали классы. Нельзя сказать, что теперь их нельзя изменить во время выполнения, потому что классы уже совсем не похожи на оригинальыне классы питона — это, скорее, модули, чем классы. Но да, их нельзя изменять во время выполнения, и множественного наследования нет;
- сделали вывод типов и опциональную строгую типизацию в дополнение к динамике.
По ссылке сказано что его придумали только потому что задолбались писать байндинги внутренностей движка к языку общего назначения
Это общая проблема Lua, а не конкретно проблема Godot: в Lua для написания любых привязок нужно нагибаться раком.
AlexBin
25.12.2019 19:31+1По этой причине порядочный создатель движка должен придумать свой скриптовый язык, как Bethesda сделала свой Papyrus, как Blizzard свою модификацию Lua со строгой типизацией.
Может в Lua от Blizzard типизация и строгая (я не проверял), но точно динамическая. Ну и в питоне в принципе тоже: строгая и динамическая.
Lua любят не за типизацию, а за простое и эффективное встраивание. Если бы питоновская виртуальная машина могла потягаться с Lua, то скорее всего везде бы пихали питон.byko3y Автор
25.12.2019 22:35Lua любят не за типизацию, а за простое и эффективное встраивание. Если бы питоновская виртуальная машина могла потягаться с Lua, то скорее всего везде бы пихали питон.
Питон по скорости выполнения не имеет шансов, но транслятор плюс базовая библиотека у него не такие уж и большие сами по себе. Проблема в том, что транслятор питона тяжело встаривать куда бы то ни было, потому что он изначально сделан как отдельная программа с кучей глобальных переменных и допущений об окружении, и трудоемкость встраивания этого такова, что проще создать свой язык заново. По этой причине до сих пор не получилось сделать множественные интерпретаторы для питона — даже такую задачу тяжело выполнить с нынешней реализацией CPython, куда там еще и встраивать.
axi1
25.12.2019 22:53Если бы вы удосужились попробовать numba, вы бы с такой уверенностью не говорили про шансы у скорости выполнения.
byko3y Автор
26.12.2019 01:35Если бы вы удосужились попробовать numba, вы бы с такой уверенностью не говорили про шансы у скорости выполнения
Я уже пробовал Cython, но вот беда: оно либо быстрое, либо питон. Два вместе не бывает — создатели GDScript поняли это. Да, на примитивных примерах достаточно несколько строчек типов написать в отдельном файле, и будет летать, но по мере роста сложности программы растет и сложность описания типов.
axi1
27.12.2019 07:58В numba есть вывод типов.
byko3y Автор
27.12.2019 08:06В numba есть вывод типов
Да. И в GDScript есть вывод типов. Да только вот беда: ни там, ни там классы не имеют почти ничего общего с классами CPython.
axi1
27.12.2019 10:33+1Если вам классы CPython так не нравятся, может, это и к лучшему?
byko3y Автор
27.12.2019 11:39Если вам классы CPython так не нравятся, может, это и к лучшему?
Да, к лучшему. Но изначальная цель была — сохранение совместимости с имеющимся наследием. Если отбросить это условие, то и Cython убог, и GDScript убог, и Numba бедна фичами.
axi1
28.12.2019 14:04У нумбы как таковой (конструкции языка) фич вполне достаточно (ну, кроме классов). Но как вы справедливо заметили, пока что она не очень хорошо сопрягается с существующими библиотеками. Если numpy достаточно хорошо интегрирована в numba, то что касается всего остального, для каждой функции надо выбирать — или совместимость или скорость: разогнанные функции не могут вызывать неразогнанные (хотя неразогнанные могут вызывать разогнанные). Но во всяком случае это гораздо удобнее чем каждый раз изобретать интерфейс между c++ и python.
Но в любом случае в 2019 году писать про оптимизацию питона и говорить, что pypy – единственная успешная библиотека по его оптимизации это по меньшей мере странно. Посмотрите бенчмарки.
byko3y Автор
28.12.2019 06:50У нумбы как таковой (конструкции языка) фич вполне достаточно (ну, кроме классов)
Ну представьте, если я скажу «я сделал язык, который почти Си, только указатели не поддерживаются» — мне же засмеют. А почему-то питон без классов никого не смущает.
Но в любом случае в 2019 году писать про оптимизацию питона и говорить, что pypy – единственная успешная библиотека по его оптимизации это по меньшей мере странно
У PyPy нет конкурентов. Никто не может сравнимо быстро исполнять питон.
mayorovp
А что тут общего с goto? Конструкция goto передает управление навсегда, а тут идёт обычный вызов сопрограммы. С гарантией, что управление вернется обратно.
Но я решил оба примера правильно. Они ведут себя совершенно логично. Где тут плохая предсказуемость?
Генератор — это уже оптимизация
Ну да, последовательные вычисления нельзя так просто взять и сделать параллельными. Но это верно для любого последовательного кода, причём тут генераторы?
Пишите код так, чтобы его можно было протестировать. Генератор вовсе не обязательно тестировать совместно с остальным кодом, его можно протестировать отдельно.
byko3y Автор
Конструкции goto использовались как для продолженния работы с обходом ненужных инструкций, так и для цикличного/подчиненного выполнения. Если придираться к деталям, то генераторы — это самые что ни на есть зеленые потоки, а вызов next/yield переключает контекст. Это не вызов сопрограммы, потому что точка входа неопределена — вызывающая функция не знает, что вызывает. Примерно так в древности вызывали функций при помощи goto/jmp, когда вызывающий код прыгает куда-то в середину блока кода, и последний должен по каким-то правилам вернуть управление/возобновить выполнение вышестоящего кода. Это рушит структурированность кода и создает проблемы как минимум в понимании алгоритма работы человеком. Кроме того, это создает трудности в понимании работы еще и для машины, читай "для тестов и оптимизаторов".
Поздравляю. Мое первое возражение описывается так: я жму 120 кг от груди; можно ли сказать, что это детский вес и любой ребенок с ним справится? Мое второе возражение описано в статье: минимальное изменение структуры кода меняет порядок выполнения. То есть, для предсказания поведения кода программиста досконально должен знать весь код в генераторов с контекстом, даже если этот код разбросан по нескольким модулям и часть из этих модулей хорошо оттестированы.
Да. Код на итераторах в CPython выполняется медленнее. "Сломал ногу — учись ходить на руках".
В статье я слабо прошелся по матчасти о параллелизации кода, о том, почему одни алгоритмы очень легко параллелизуются, а другие — нет. Один из ключевых факторов параллелизуемости — возможность изменить порядок выполнения команд. Те же Go и Haskell свои зеленые потоки могут спокойно разбрасывать по потокам ОС, потому что контексты, изменяемые состояния у потоков изолированы, их обработка независима, порядок выполняемых операций в разных потоках может меняться. Генераторы питона, как зеленые потоки, наоборот, отличаются тесной связанностью состояний потоков, недопустимостью изменения порядка команд, и повышением сложности контекста, который теперь хранит неявный объект фрейма стэка со всеми объектами в этом стэке, и эти объекты, по идее, не должны меняться во время замирания потока-генератора. Напоминаю, что это происходит на фоне отсутствия механизмов работы со значениями, из-за чего, даже при воображаемом наличии реализации многопоточности, нам пришлось бы прикладывать большие усилия для того, чтобы случайно не выстрелить себе в ногу, приняв локальной переменной в генераторе ссылку на изменяемый объект.
И где гарантия того, что протестированный отдельно генератор будет работать так же в составе целой системы? Её нет.
mayorovp
Приведите примеры. Лично я не наблюдал указанных вами явлений.
Ну да, конечно же! Если переставить две строки кода местами, то порядок их выполнения поменяется! Такая неожиданность...
Давайте я перепишу ваш пример без генераторов и менеджеров контекста, а вы скажете стало ли понятнее?
А если не писать print в генераторах, то и предсказывать ничего не придётся.
Питону мешает это делать GIL, а вовсе не генераторы.
Между прочим, ваш пример с "вход-выходом" можно запросто переписать хоть на Го, хоть на Хаскель. Сможете ли вы угадать, в каком порядке будут выводиться строки у "независимо" обрабатываемых потоков?
Или для потоков играть в угадайку программисту необязательно, а для генераторов без этого никак потому что гладиолус?
Ну а зеленый поток Го хранит не просто неявный фрейм стека, а аж целый "неявный" стек! Почему первое преподносится как нечто ужасное, а второе вы считаете чем-то хорошим?
К слову, в Го неизменяемые объекты вообще признаны неидеоматичными, и как-то с этим они живут… Наверное, они просто не принимают в локальные переменные случайные ссылки.
byko3y Автор
Проблема наблюдается при усложнении кода, когда в коде кроме генераторов есть состояния и контексты. Простой код можно написать и без генераторов, и без классов-итераторов — там уже нет разницы. По вопросам менеджеров контекстов в генераторах было уже пара утвержденных PEP, а вот эти ребята, например, хотят ввести возможность блокировать генераторы внутри менеджеров контекста (там же и примеры):
https://discuss.python.org/t/preventing-yield-inside-certain-context-managers/1091
Ничего не переставлялось местами, порядок выполнения команд в обоих примерах из статьи одинаков: «gen_in_manager(); next(g1); print('Конец')» — разница только в том, что во втором примере «gen_in_manager(); next(g1)» вызваны в виде функции. Мне кажется, что ты привираешь про «я решил оба примера правильно».
Код не имеет отношения к проблеме менеджеров контекста с генераторами. А проблема заключалась в том, что контекст остается в активном состоянии, потому что не освобождается после выхода из вызова функции-генератора, что противоречит интуитивному пониманию контекста.
Да, если не обращать внимания на проблему, то она исчезнет.
Смогу: в произвольном. Но там будут настоящие потоки, которые можно выполнить на нескольких процессорах.
Стэк — это абстрактное хранилище, которое само по себе ничего не значит. Значат данные которые хранятся в стэке, значат данные, которые передаются между потоками. В Go взаимодействие потоков выполняется через канал и по умолчанию не имеет общих данных, в питоне же всегда есть как минимум общее состояние в виде объекта-генератора.
Это намного тяжелее делать в питоне, где общее состояние — это, например, класс объекта данных, это переданные в функцию генератора параметры.
mayorovp
А с чего вы взяли, что вы в своем коде "вышли" из функции-генератора?
Нет, разница в том, что во втором примере итератор выходит из области видимости и удаляется перед
print('Конец')
. Если это вам кажется непонятным — значит, вам на самом деле непонятна работа деструкторов в Питоне, а генераторы тут ни при чём.Во-первых, не в произвольном: примитивы синхронизации не просто так придуманы. Во-вторых, если вас устраивает произвольный порядок исполнения на других языках, почему вы ищите конкретный порядок в Питоне? Что мешает также сказать, что он произвольный?
А чем "объект-генератор" принципиально отличается от канала? Почему объект-генератор у вас стал общим состоянием, а канал общим состоянием не является?
byko3y Автор
Вот именно! Откуда я знаю, где я вышел из генератора? Я не знаю. Пока я досконально не вычитал каждую строчку кода каждой вызываемой функции — я не могу быть уверен, что смогу предсказать, вышел ли я из контекста в генераторе или нет.
Да, это правильный ответ. А никого не смущает тот факт, что выход из контекста неявный и определяется областью видимости в функции, вышестоящей по отношению к вызываемой? Норм структурированность?
Тот факт, что питон не многопоточен, мешает мне увидеть пользу в бесполезных конструкциях, которые не дают языку развиваться.
Типа «чем MS DOS отличается от линукса? То ОС, и то ОС». На уровне языка канал не является общим состоянием, потому у потоков нет общих данных в рамках канала. Может быть, в самой реализации канала и используются какие-то общие данные, но это скрыто от программиста. Объект-генератор, например, не может выдать следующее значение до того, как это значение будет запрошено, а когда оно будет запрошено — оно станет опять тем самым общим состоянием. За исключением неизменяемых примитивных типов, конечно же — потому хелло ворлды так сладко писать на генераторах.
mayorovp
А зачем вам знать, вышли ли вы из контекста в генераторе? Почему бы просто не писать такой код, которому будут не важны детали реализации генератора?
Норм. Именно так и должно работать иерархическое владение ресурсами.
Так вы ищете пользу конструкций или проблемы в них? Мне почему-то кажется, что второе.
С пользой-то всё просто: генераторы нужно использовать для разделения двух совместно работающих алгоритмов. Именно двух алгоритмов, а не одного. Если вы задаетесь вопросом как будет выглядеть общий алгоритм — генераторы не для вашего случая, вот и всё.
Реализация возвращенного вам итератора ("объекта-генератора") точно так же скрыта от программиста, не вижу разницы.
Канал тоже.
И в Го это происходит точно так же.
byko3y Автор
Ага, "почему бы всем людям на Земле не жить в мире и любви?". Потому что по мере роста населения возникает борьба за ресурсы. Конечно, есть вариант дать каждой сопрограмме отдельную копию, но все равно останутся какие-то естественные монополии, вроде открытых файлов, под которые придется делать слой абстракции, чтобы скрыть асинхронность доступа.
Иерархическое? Откуда такая гарантия? Что мешает передать ресурс (объект-генератор) мимо иерархии? Почему я и пишу про необходимость досконального изучения каждой строчки вызываемых фукнций.
Разные функции — разные алгоритмы. Это очевидно. Проблема генераторов заключается в том, что они заставляют разные алгоритмы выполняться совместно — это значительно повышает сложность понимания программы, не давая значимых преимуществ. Да, есть программы, которые изначально выполняют в ядре ОС или во внешних потоках несколько операций параллельно, и управляют ими в event-driven режиме — алгоритм уже изначально сложный, там разница будет лишь в форме записи кода. Но если такой острой необходимости в асинхронности нет, то и генераторы не нужны.
Там идет вполне целенаправленное движение в сторону того, чтобы кишки торчали в обе стороны: send, close, gi_frame, gi_yieldfrom. Да, это вполне себе взаимодействие подчинения, когда породивший генератор код владеет фреймом генератора и может создавать в генераторе исключения. Но независимые алгоритмы? Нет.
Только в частном случае. Например, «c := make(chan int, 1)» уже работает по-другому, заполняя канал без ожидания потребителя.
В отличие от питона, у Go есть явное разделение на значение и ссылку. Канал передает значение, которое существует в один момент времени только с одной стороны канала.
mayorovp
Во-первых, здравый смысл. Когда я пишу программу, я стараюсь написать её как можно проще, а не как можно запутаннее. Если вы пишете программы по-другому — это не проблема языка.
Во-вторых, даже если считать это проблемой языка, это точно не является проблемой генераторов, поскольку такое может случиться с любым ресурсом.
Это не проблема, а возможность. Для того сопрограммы и придуманы.
В Питоне тоже так можно, надо только подходящую библиотеку найти. Или написать.
Вот только у значения внутри может быть ссылка. Часть ваших примеров из поста именно про это — и в Го тоже так можно накосячить.
byko3y Автор
Если я явно выделил ресурс и явно высвободил его — нет проблем. Но это же не я писал про то, что вызывающий код не должен знать деталей реализации генератора, правильно? А генератор взял, и выделил ресурс после итерации (не путать с "в итерации"). А откуда я знаю, что он выделил? А я не знаю, мне же не нужно вычитывать каждую его строчку реализации, правильно? Вот я засунул указатель на итератор в долгоживущее поле, и пошел по своим делам. А генератор, тем временем, живет и держит какой-то важный или большой ресурс — но я-то думаю, что просто держу один фрейм генератора, и больше ничего.
Проблема заключается не в сопрограммах, а в том, что совместное выполнение алгоритмов резко повышает сложность их написания и понимания, как человеком, так и машиной.
На двух ядрах вычисления делать можно? Сомневаюсь.
Об этом и была вся моя статья: питон провоцирует плохой код, Go — хороший.
AlexBin
Эм, нет, проблема заключается в неуместном применении инструментов, что с любым инструментом ведет к беде. Вообще с любым. У некоторых инструментов есть очень узкое но эффективное применение, и когда его делают золотым молотком, инструмент превращается в антипаттерн. Если вы предлагаете такие инструменты в принципе убирать, то можете их не использовать, а другим не мешайте. Я генераторы использую редко, но эффективно, где они действительно упрощают понимание алгоритма.
Вообще, все ваши кейсы с генераторами касаются проблем чего угодно, только не генераторов, но ваше рвение впечатляет.
byko3y Автор
В случае класса-итератора я могу выделить ресурс явно и передать его в генератор — это самый правильный подход к данному вопросу, вызывающий код полностью контролирует вызываемый. Я могу точно знать, когда цикл итератора начал работу и закончил работу, он не зависнет в промежуточном состоянии, завися от ресурса, который я решил закрыть, как может сделать генератор. Да, может оказаться, что требуемого для итерации ресурса нет, а итератор еще есть — какая разница, итерация не будет вызвана все равно. В генераторах же считается нормой управление ресурсом в самом генераторе. Особенно это актуально для сопрограмм на async/await.
Если копировать поведение итератора в генератор, то внезапно оказывается, что генератор и не нужен вовсе. Генератор нужен только тогда, когда структурность поведения нужно нарушать, задерживая ресурсы между итерациями, в противовес доступа к ресурсу только внутри одной итерации.
Существуют еще блокировки. отображения памяти, и прочие более легковесные вещи, который тоже умеют стерлять в ногу.
В многозадачном коде генераторы категорически неприемлимы. Они должны быть оформлены отдельной задачей, либо переработаны в итераторы — это основная моя претензия. Да, можно взвешивать за и против по поводу тестирования и оптимизации, но по параллелизации нечего взвешивать, потому что кол-во неявных ресурсов и проблем с ними еще больше возрастает, и при этом нет никакой гарантии, что итератор или вызвавший поток вернутся в то же состояние — это потенциально может превратить отладку программы с генераторами в ад.
Позиция защитников генераторов исходит из того, что «нож в печень, CPython вечен». Я же исхожу из некоего воображаемого транслятора, который умеет хорошо оптимизировать, параллелизировать, и давать гарантии по поводу надежности работы программы, и я вижу, что генераторы прямо-таки вставляют палки в колеса, заставляя сильно прогибаться транслятор и накладывая ограничения на выполнение, которые не дают просто говорить "да, пусть будет, оно никому не мешает" — потому что мешает.
chapuza
Довольно странно приводить Го как пример удачной архитектуры для асинхронных вычислений. Одной корпоративной многозадачности достаточно, чтобы никогда с ним не связываться.
mayorovp
Так это же не я его первым в такой роли привёл.
chapuza
Ну я в конец треда просто привык реплики добавлять :)
Если честно, я не верю в удачную архитектуру с concurrency + mutability. Что-то одно, вместе они не работают.
byko3y Автор
Корпоративной? Я так понимаю, речь идет про кооперативную. Го полностью поддерживает потоки на уровне ОС, но не отображает го-программы в потоки ОС 1 к 1. Причина лежит в слабой поддержке многопроцессорного железа в современных ОС, и в ближайшее время ситуация не планирует меняться. Например, я хочу из одной го-программы вызвать вторую, чтобы вторая го-программа обработала параметр и выдала результат, вернув управление первой го-программе. В случае нынешней реализации Go я не переключая контекста ОС сразу передаю управление другой го-программе, тратя на это порядка 200 циклов процессора. Если же у меня есть многопроцессорная система и моя программа не привязана к конкретному процессору, то на потоках обычно такая передача управления приводит к добавлению второй го-программы в планировщик другого ядра и запуском потока, с последующей остановкой потока первой го-программы и исключением ее из очереди планировщика первого ядра. Второй сценарий занимает примерно 5000-10000 циклов процессора. Вызвал функцию, называется.
По этой причине в pthreads есть/была поддержка кооперативной многозадачности, в windows fibers есть до сих пор, потому в Haskell и Go для сопрограмм используются именно кооперативность, которая может быть натянута на потоки ОС. В конце-концов, по этой причине возник nginx, который компенсирует неспособность ОС эффективно работать с большим числом легковесных задач.
chapuza
Да, опечатался. Я знаю, что такое грин треды, спасибо. Виртуальная машина эрланга умеет запускать миллионы эрланг-процессов в режиме вытесняющей многозадачности. Что не дает возможности человеку запустить на восьмиядерной машине девять грин тредов, считающих факториалы и повесить нахрен всю систему. Поэтому я и говорю, что с Го связываться разумный человек не станет.
byko3y Автор
Вообще, над этим работают в Go:
https://codereview.appspot.com/10264044
https://github.com/golang/go/issues/10958
Однако, нужно понимать, что для высокопроизводительного си-подобного кода не так просто сделать подобное не потеряв производительности.
В то же время, никто не запрещает запускать избыточное число потоков в Go, чтобы гарантировать выполнение фоновых задач даже при тяжелых вычислениях. Там вон на C++/Java/JS и вовсе однопоток лепят сплошь и рядом — претензия к тому, что «программа зависает» на этом фоне мне кажутся преувеличенными.
chapuza
А мне — нет. Го появился чпустя 20 с лишним лет после эрланга, можно было бы обойтись без таких нелепых родовых травм.
Да, Гугл может вытащить своей «поддержкой» любое поделие, что и объясняет популярность Го. Но когда начинаются разговоры об адекватности языка в вакууме, мне становится смешно. Го выглядел бы прорывом в восьмидесятые годы прошлого века, но сейчас — это просто язык, разоаботанный для того, чтобы нанимать пачками программистов среднего уровня и они бы не накосячили сверх положенного.
byko3y Автор
Вот примерно так 20 лет назад Таненбаум с Линусом спорил о том, что только лохи могут писать монолитную систему. Ну и где сейчас Таненбаум? Го написан матерыми практиками, он написан так, что работать, и работать быстро. Да, они могли бы начать пытаться вводить в ядро линукса какие-то правки для того, чтобы позволять переключать контекст в обработчиках сигналов без UB — они уже пытались ввести правки для кооперативного переключения контекстов при вызове функции между потоками, но так в ядро эти правки и не попали. Даже если бы у них получилось — они бы получили linux-only решение.
Реальность же такова, что ОС-и не дают никакой возможности сделать вытесняющую многозадачность в user-space без огромной потери производительности. Потому я и пишу, что поддержка многопроцессорности по прежнему медленно и неохотно развивается, находясь на достаточно низком уровне в среднем по палате, который ограничивается реализацией равенства процессоров и примитивными мелкогранулярными блокировками структур ядра, не способными эффективно разрешать значительные трения между процессорами. По этой причине в современных SMP системах ты ограничен независимыми задачами/потоками. Захотел взаимодействия потоков? Сам себе злобный буратина.
chapuza
О, да. Роб Пайк, автор всемирно известных операционных систем Plan 9 и Inferno, а также языка программирования Limbo. Матёрее практика и не найти, пожалуй.
Ну да, ну да. А пацаны-то и не в курсе.
byko3y Автор
Серьезно, что ли? Я думаю, здесь сидит немало людей, которые скажут, что Plan 9 была одной из лучших ОС-ей своего времени. Но победил Windows — наверное, это и есть самая лучшая в мире ось.
И да, Go является прямым наследником Limbo.
Есть что возразить? Или мне нужно показывать бенчи?
https://stressgrid.com/blog/benchmarking_go_vs_node_vs_elixir/ — в среднем Erlang в 4 раза больше потребляет ресурсов процессора, чем Go;
https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/erlang.html — так и быть, соберу сам таблицу по Erlang vs Go:
Go: 25 s, 361 Mb;
Erlang: 8 s, 464 Mb;
Go: 4 s, 3 Mb;
Erlang: 21 s, 33 Mb;
Go: 2 s, 8 Mb;
Erlang: 16 s, 29 Mb;
Go: 7 s, 340 Mb;
Erlang: 56 s, 2000 Mb;
Go: 18 s, 1.5 Mb;
Erlang: 85 s, 24 Mb;
Go: 21 s, 1.6 Mb;
Erlang: 194 s, 24 Mb;
Go: 4 s, 1800 Mb;
Erlang: 42 s, 2700 Mb;
Go: 12 s, 160 Mb;
Erlang: 140 s, 750 Mb;
Go: 5.5 s, 31 Mb;
Erlang: 110 s, 52 Mb;
Go: 2 s, 3.5 Mb;
Erlang: 50 s, 24 Mb.
Erlang в данном случае — это скомпилированный HiPE код.
Как мы видим, в среднем Erlang проигрывает на порядок, только на бинарных деревьях хорошие результаты показывает — судя по всему, там есть какие-то родные инструменты для этих структур.
chapuza
Это синтетические тесты, которые показывают чуть меньше, чем ничего. В реальном мире надо смотреть на что-нибудь, отражающее реальный мир.
Например, вот на это: https://www.phoenixframework.org/blog/the-road-to-2-million-websocket-connections
byko3y Автор
Я вообще не вижу здесь измерения производительности — они только меряют максимальное число подключений за условно бесконечный период времени.
chapuza
Они меряют число одновременных (simultaneous) подключений, которые сервер обслуживает. Одна машина (довольно крутая, но одна) обслуживает 2М соединений.
Если это не производительность, то продолжайте считать, что бизнесу нужно добиваться увеличения мандельбротов в единицу времени.
byko3y Автор
«Производительность устройства — величина действия устройства, то есть отношение количества произведённой работы (выпущенного продукта) ко времени их выполнения (выпуска)»
«Вычислительная мощность компьютера (производительность компьютера) — это количественная характеристика скорости выполнения определённых операций на компьютере»
Число одновременных подключений не является мерой производительности.
chapuza
Вы издеваетесь, что ли? Число обслуживаемых одновременных соединений — это не количество произведённой работы? А что тогда?
byko3y Автор
Нет, они не обслуживаются — они просто висят. Работа равна нулю.
tmteam
Число обслуживаемых одновременных соединений — это скорее о библиотеке а не о языке и компиляторе. И да, есть люди и задачи, которым количество подключений — до фонаря, а вот факториалы с бинарными деревьями крайне важны.
chapuza
Тут, во-первых, никакой библиотеки нет: это OTP запускает миллион эрланг-процессов. Во-вторых, не вижу противоречий. Количество соединений в данном примере — не про сокеты, а про способность языка, компилячтора и виртуальной машины справляться с миллирнами процессов. Там же вебсокеты, поэтому они не просто висят, а шлют туда-сюда хендшейки, то есть, нагружают процессор (не сильно, конечно, но все же).
Факториалы несколько веков назад научились считать через золотое сечение. С деревьями как раз все хорошо из-за TCO.
Есть целый ряд задач, в которых эрланг — не лучший выбор (тяжелая арифметика, например), ну так — сюрприз — там и C до сих пор проигрывает фортрану :)
Athari
Вопрос в основном в том, насколько вы готовы испортить сишный код, чтобы он был оптимальнее (restrict и пр.).
byko3y Автор
Handshake — это процедура инициализации соединения. Поддерживается в живом состоянии соединение при помощи keep-alive механизма TCP, то есть, на уровне ядра ОС.
Benchmarking game мало затрагивает тяжелую арифметику. Советую почитать описание тестов.
chapuza
Что-то мне в общении с вами вообще внутренний спеллчекер отказывает. Hearbeat, конечно же.
byko3y Автор
Они привели конфиг Tsung, там нет никакого heartbeat-а.
arthuriantech
Генераторы и корутины являются сопрограммами. Я не представляю, откуда вы берете все эти идеи.
byko3y Автор
Являются. Зеленые потоки — это разновидность выполнения сопрограмм. Но yield и next — это не вызовы сопрограммы, а команды переключения контекста, которые могут переключать его на весьма неожиданные команды — отсюда и аналогия с goto. Моя претензия состояла в нарушении структуры программы генераторами, а не в необходимости обговорить определения терминов.
chapuza
Основная опасность в том, что мы в итоге получаем zero fault tolerance.
Вот я отладил, выотладил и переотладил свой код, содержащий
yield
. А Вася его неаккуратно использовал, и теперь внутриyield
закрывается открытый мной ресурс. В результате мой (я умею в защитное программирование :)finally
блок пытается его закрыть еще раз и voila?.mayorovp
Это вообще как?
chapuza
Я не понял вопрос. Что именно — как?
Если вызывающий код позволяет вызываемому навредить ему — дело швах.
mayorovp
Как можно "внутри yield" закрыть открытый вами ресурс, если у вызывающего кода просто нет к этому ресурсу доступа?
chapuza
yield resource
В таком утрированном виде это выглядит как «сам дурак, не нужно ресурсы отдавать», а в реальном мире это иногда необходимо, да и не столь прозрачно.
AlexBin
так?
Если да, то подобное можно повторить многими механизмами. Это проблема архитектуры, а не генераторов.
chapuza
Да, именно это я и имел в виду. Если плохой архитектурой можно отстрелить себе ногу, это не говорит в пользу языка.
AlexBin
chapuza
Генераторы спроектированы так, что они способствуют созданию плохой архитектуры, ведущей к отстрелу ног.
Впрочем да, я согласен, генераторы тут вряд ли основной виновник.
goodbear
Тут проблема в том, что на таком коде это все понятно, но когда это вдруг вылазит внутри большого проекта, где куча вызовов и все это перемешано, не каждый может сообразить где истинная причина того что вот сейчас что-то не сделалось или наоборот — что-то произошло без видимой причины.
AlexBin
Ну так подобное можно про большинство механизмов сказать, только потом окажется, что дело то не в них. В принципе можно сказать «Пишите код хорошо, а плохо не пишите».
goodbear
Так а кто против-то — не у всех получается, плюс приходится использовать сторонние библиотеки и многие написаны не идеально.
byko3y Автор
Дай угадаю: ты на ассемблере программы пишешь. И написанные программы почти всегда работают как надо, не преподнося неожиданных проблем.
mayorovp
Судя по желанию знать абсолютно всё, на ассемблере пишете программы как раз вы.