?

Log in

No account? Create an account
для программистов - Поклонник деепричастий [entries|archive|friends|userinfo]
Anatoly Vorobey

[ website | Website ]
[ userinfo | livejournal userinfo ]
[ archive | journal archive ]

Links
[Links:| English-language weblog ]

для программистов [фев. 28, 2006|06:01 pm]
Anatoly Vorobey
Следующее будет интересно только программистам.

(вопрос: стоит ли мне создать отдельный журнал для записей на компьютерные/программистские темы? Я несколько раз об этом думал, но так ничего и не решил)

Один красивый хак, и две сырые мысли.

1. Tail-recursion в Питоне. Питон (Python) не поддерживает оптимизацию хвостовых вызовов. Если вы не знаете, что это такое, то вам следует об этом почитать, профессиональный программист должен об этом знать. Вот здесь понятным языком объясняется по-английски. В двух словах: если функция f заканчивает свою работу возвращением результата другой функции: return g(...) (неважен синтаксис, важна суть), то можно оптимизировать такой вызов, освободив место для аргументов и переменных функции f на стеке ещё до вызова функции g. Если g==f, т.е. f рекурсивная функция, то это позволяет делать сколь угодно глубокую рекурсию без лишней траты места на стеке. Это особенно важно в функциональных языках, но и в других удобно. Но не везде это можно сделать, зависит от языка.

В Питоне - теоретически можно, язык позволяет, а практически автор отказывается такими глупостями (с его точки зрения) заниматься. Так вот красивый трюк, который позволяет любую функцию в Питоне оптимизировать под хвостовую рекурсию, используя только возможности самого языка: "декораторы", позволяющие модифицировать поведение функций, и немного рефлексии. По-моему, этот код может понять и кто-то, кто не знает Питон (я, например, не знаю и понял). Суть его такая: из функции g, вызывающей самое себя, мы делаем func, которая при запуске проверяет свой стек, и смотрит, не является ли она своим собственным дедушкой по линии вызовов; если да, она кидает исключение, если нет, то вызывает g в блоке, ловящем такое исключение. Когда в выполнении g ("сына" func) дойдет дело до рекурсивного вызова g, вместо него подставляется вызов func опять (так работают "декораторы"), и она, обнаружив, что является своим дедушкой, кинет исключение; сама же в ипостаси дедушки его поймает и раскрутит стек на два вызова обратно; потом опять вызовет g, и так далее. В результате стек двигается на два фрейма вперед, а потом по исключению обратно на два фрейма назад, а рекурсия продолжается. Красиво!

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

Одна из главных причин, почему C++ плохой язык: для этого надо сначала понять, почему C хороший. В чем состоит то свойство C, из-за которого его называют "портабильным ассемблером"? Дело не в том, что "близко к машине", и всё низкого уровня. Дело в том, что почти всегда в C эффект любой строки кода локален и очевиден. Когда я что-то делаю в C, неважно что, я очень хорошо понимаю, что именно происходит. Если я пишу x=y, я знаю точно, что происходит. Если я пишу f(...), я знаю точно, какая конкретно функция будет вызвана, я могу указать на неё пальцем, и я знаю точно, что произойдёт в момент входа в неё и выхода из неё. Если я выделяю память, я знаю точно, что она не исчезнет, пока я её не освобожу. Итд. итп. Атомарные строки кода переходят в атомарные куски кода во время запуска, и никаких сюрпризов. Есть исключения: например, если я вызываю функцию через ссылку, я не знаю, что собственно я вызвал, до рантайма. Но этих исключений очень мало и они тоже "локализованы" и их легко понять.

Это необязательно хорошо. Но это - в C - выполняется последовательно, и то, что это последовательно - хорошо. Разные языки по-разному решают вопрос о том, как позволить программистам прятать информацию от самих себя. В объектно-ориентированных языках принцип полиморфизма, принципиального незнания мной того, объект какого класса я вызываю по ссылке (базового или наследника), является краеугольным; и это по-своему хорошо, если проведено последовательно.

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

Но, с другой стороны: полностью нарушен (я бы сказал, низвергнут с пьедестала и подвержен особо извращенному поруганию) этот самый принцип локальности поведения системы в ответ на строчку моего кода. Я всего лишь объявил переменную какого-то типа, написав "Typename varname;", но эта строчка может привести к вызову неизвестного мне конструктора, а за ним - кода сколь угодно, вообще говоря, сложности. Я всего лишь применяю известный мне оператор к переменной - а он, оказывает, overloaded у этого класса, и черт знает что на самом деле там произойдет. Я всего лишь вышел из функции, что может быть проще, написал }, а в рантайме на самом деле пошли плясать деструкторы всех автоматических объектов в этой функции. И даже и не буду начинать говорить про copy constructor и прочие подобные прелести.

Так вот, поэтому C++ - плохой язык.

Он настолько много прячет за кулисами, чтобы навязать программисту режим работы "моя хата с краю": пиши свой код, не волнуйся насчёт того, что магически происходит вокруг него, всё хорошо, всё идёт по плану... И в то же время того же программиста заставляет следить за всеми malloc()'ами и new, рассчитывать ужасные иерархии наследования и дикие функции-"френды", не говоря уж о темплейтах. По сути дела, медленно и неумолимо превращает программиста в шизофреника.



3. Монады и Хаскель. Это ещё более сырая и невнятная мысль, но попробую всё же высказать.

(если вы не знаете, что такое Хаскель и монады, примите условно такие определения: Хаскель - язык, в который нелегко "въехать", но очень мощный и интересный; монады - способ программирования внутри Хаскеля, в который очень, очень нелегко въехать, но тем не менее он фундаментальный и без него реальные большие и полезные программы на Хаскеле не сделать. Если вам интересно почитать больше о Хаскеле, см. сообщество ru_lambda и, например, мою незаконченную серию записей в нем).

С одной стороны, мне сейчас полагается быть фанатом Хаскеля вообще и монадического программирования в частности. Т.к. я только что это изучил (собственно, продолжаю изучать), и оно действительно мощно, интересно, необычно, полезно. И с одной стороны, я действительно теперь фанат и мне всё очень нравится (не отменяя другие любимые языки). С другой, есть интересные сомнения. prosto_tak задал правильный вопрос, на который у меня нет хорошего ответа: если оно всё такое сложное, зачем оно надо?

Мысль, которую я хочу передать, она примерно вот какая. Казалось бы, этот вопрос неправилен вот по какой причине. Каждый раз, когда мы учим язык (технологию, идею, итп.), который очень отличается от уже знакомых нам, например, впервые учим объектно-ориентированный язык, или впервые функциональный, как Хаскель, или первый раз сталкиваемся с Лиспом, или с APL, итд. итп. - неизбежно некоторые вещи будет тяжело понять, потому что нужно "перестроить нейроны" на другой тип мышления. Именно такие языки и интересны, которые действительно заставляют программиста мыслить совсем по-другому, а следовательно другим образом смотреть на мир. И стоит ожидать, что какие-то вещи в языке будет трудно понять, пока не перестроишь нейроны. И поэтому нечего жаловаться на то, что трудно, скажем, понять и пользоваться тем, насколько в SmallTalk глубоко и последовательно лежит идея "всё-объект", или в Лиспе код==данные итд. - или в Хаскеле монады!

Но после того, как я более или менее разобрался всё же с монадами (ну по крайней мере мне кажется, что разобрался), я понял, что вышеописанная точка зрения, которой я придерживался до того, неверна. Монады объективно тяжело понять. Их тяжело понять не только потому, что это "новый" тип мышления - функциональное программирование - но и вообще, просто, с объективной интеллектуальной точки зрения их тяжело понять. Они требуют намного больше абстрактного и типично-математического мышления, чем сложные возможности в любых других известных мне языках. А меж тем в Хаскеле даже строку на вывод не послать без монад (ну, можно, конечно, это делать, не понимая, как это работает, но это ж неинтересно).

Я не то чтобы жалуюсь, нет - я же понял в конце концов, проникся, мне нравится итд. Но одновременно другая часть меня, расщеплённая, спрашивает: а нужно ли это на самом деле? Идея Хаскеля прекрасна и очень мощна - но не может ли быть так, что, заковав язык в эту прекрасную и действительно очень мощную идею, мы пришли к ситуации, когда для "обычного" вроде-как-но-не-совсем-императивного стиля программирования, без которого всё-таки не обойтись (и не только для ввода/вывода! нет, далеко не только!), нам приходится использовать столь сложные по своему уровню абстрактности и математической нетривиальности конструкции? Попросту говоря, не является ли энтузиазм по поводу Хаскеля и монад в определённой мере интеллектуальной мастурбацией, когда те, кто "проникся", радуются тому, как это умно и глубоко, и какие они крутые, что хорошо это понимают? А на деле для таких простых относительно вещей в языке не нужно было бы такой огород городить, если бы его получше построить, или снизить некоторую долю пуризма в его дизайне?

Это мысли по поводу Хаскеля; и реального ответа у меня нет пока, да может я завтра вообще решу, что полную чепуху написал (или пойму в очередной раз, что до сих пор не понимал монады). Но общая мысль, которую я из этого для себя вынес, пусть тоже тривиальна, но важно мне её для себя зафиксировать: не все трудности вызваны тем, что ты ещё не "привык к новому типу мышления". Не все (достаточно продвинутые, ясно) языки/способы программирования одинаково сложны, и дело лишь в том, что что-то тебе привычно, а что-то нет. Такое объяснение часто верно, и часто оказывается полезным и правильным, но оно не всегда верно, и об этом тоже надо помнить. Некоторые вещи просто действительно "объективно" тяжелее понять и выучить.


Как обычно, критика и комментарии приветствуются.
СсылкаОтветить

Comments:
[User Picture]From: d_m_
2006-03-01 07:51 am
Ура! Теперь я знаю, что такое переносимость:

 #ifdef OS1
   //do code1
 #elseif OS2
   //do code2
 #else ...

Да, в Java так действительно нельзя :))
(Ответить) (Parent) (Thread)
[User Picture]From: sartoris
2006-03-01 07:59 am
Если код написан ХОРОШО, то таких ifdef требуется очень мало.
(Ответить) (Parent) (Thread)
[User Picture]From: d_m_
2006-03-01 08:32 am
Да Вы же сами себе противоречите! :)
Только что тут рассказывали про "совершенно другие сокеты".
И, похоже, не уловили самого главного: говорите о портировании исходного кода, в то время как в Java переносим откомпилированный проект.
(Ответить) (Parent) (Thread)
[User Picture]From: sartoris
2006-03-01 09:11 am
Только что тут рассказывали про "совершенно другие сокеты".

Проанализировав КОНКРЕТНЫЕ задачи КОНКРЕТНОГО приложения (а не проблему глобального парсинга) можно построить весьма и весьма переносимый код, в котором будет минмум ifdef. Если вас интересует конкретно КАК это делается - приезжайте. Десять дней, пара-тройка тысячь евро и вы будете в курсе:))))

И, похоже, не уловили самого главного: говорите о портировании исходного кода, в то время как в Java переносим откомпилированный проект.

Понимаете ли... С практической точки зрения "универсальный байткод" Явы никому не нужен. Любая система пишется для той или иной среды, для того или иного набора подручных средств и так далее. Совершенно ясно, что какой-нибудь анализ данных работающий с Oracle через ODBC никогда не будет запускаться на мобильном телефоне. И вряд ли вы станете на своём десктопе играть в джава-игры для мобильных телефонов (которым, кроме всего прочего, нужна целая симулятивная надстройка, для того чтобы работать на обычном компьютере). Поэтому универсальность кода пусть и приятная, но лишенная смысла особенность.

Я вообще-то пытаюсь высказать довольно простую мысль: Ява не пускает программиста на тот уровень, на котором можно так или иначе обходить и решать проблемы совместимости и различные use-cases связаные со средой. Одни говорят, что это хорошо. Другие - что это плохо. Я отношусь ко вторым.
(Ответить) (Parent) (Thread)
[User Picture]From: d_m_
2006-03-01 05:12 pm
Проанализировав КОНКРЕТНЫЕ задачи КОНКРЕТНОГО приложения (а не проблему глобального парсинга) можно построить весьма и весьма переносимый код, в котором будет минмум ifdef. Если вас интересует конкретно КАК это делается - приезжайте. Десять дней, пара-тройка тысячь евро и вы будете в курсе:))))

Желаю Вам успехов в анализе КОНКРЕТНЫХ задач КОНКРЕТНОГО приложения! Не забудьте также учесть особенности компиляторов на каждой из платформ. А я уж как-нибудь попишу на Java, которая портируется безо всяких "конкретных анализов" и даже без единого #ifdef. Заодно сэкономлю "десять дней и пару-тройку тысяч евро".

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

Неплохая, однако, эволюция Вашей точки зрения. Сперва Java не портировалась вообще, теперь же портируемость на уровне готового приложения оказалась не нужна.
Что, и апплеты, работающие в любом браузере на любой платформе не нужны?
И МИДлеты, запускаемые на любом телефоне с поддержкой MIDP?
А я вот, например, разрабатываю свой проект на Windows x64 + mySQL, а один заказчик пользует его под Linux+Oracle, а второй собирается использовать Solaris-SPARC + DB2-AS/400. И я даже не знаю, какой кнопкой на Solaris запускают компилиляцию :)

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

MIDP и J2SE действительно не очень совместимы (по библиотекам), а вот .NET Compact Framework (мобильный вариант .NET) является подмножеством обычной .NET. Следовательно, всё, что идёт на PocketPC или смарфоне должо работать и на десктопе.

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

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

Другим важным достоинством Java (кроме портируемости), о котором, собственно, и была речь изначально является автоматический контроль за распределением памяти. Ни ошибки, связанные с неверными указателями, ни утечка памяти, ни двойной вызов деструктора тут в принципе невозможны.

(Ответить) (Parent) (Thread)