?

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:
Страница 1 из 4
<<[1] [2] [3] [4] >>
[User Picture]From: bespechnoepero
2006-02-28 04:04 pm
Можно тэги ставить, а можно и отдельный журнал.
(Ответить) (Thread)
From: 9000
2006-02-28 04:13 pm
Давно нужна подписка по тегам %)
(Ответить) (Parent) (Thread) (Развернуть)
[User Picture]From: dimrub
2006-02-28 04:05 pm
По поводу С++ - согласен. По поводу монад - неужели ты их снова понял? :)))
(Ответить) (Thread)
From: 9000
2006-02-28 04:13 pm
liftM2(понял, монада) ;)
(Ответить) (Parent) (Thread)
From: 9000
2006-02-28 04:12 pm
1) любопытно :) но, возможно, медленновато?

2) истинно так! кстати, в некоторой части этот наезд касается и перла ;)

3)
>Монады и Хаскель. Эту часть записи допишу через час-два, надо убегать сейчас.
Вот это и называется lazy evaluation? ;)
(Ответить) (Thread)
[User Picture]From: avva
2006-02-28 04:28 pm
1) явно не более чем в два раза медленнее ;) а в зависимости от сложности g, обычно лучше
2) а что насчёт перла, распишите подробнее?
3) :)
(Ответить) (Parent) (Thread) (Развернуть)
(Удалённый комментарий)
[User Picture]From: yole
2006-02-28 04:19 pm
К вопросу о предсказуемости C++ есть такая хорошая статья (про использование C++ для написания Windows kernel drivers):
http://www.microsoft.com/whdc/driver/kernel/KMcode.mspx
(Ответить) (Thread)
[User Picture]From: avva
2006-02-28 08:57 pm
Да, очень характерно :)

The compiler automatically generates code for at least the following objects. These objects are put “out of line,” and the developer has no direct control over the section in which they are inserted, which means they could happen to be paged out when needed.

• Compiler-generated code such as constructors, destructors, casts, and assignment operators. (These can often be explicitly provided, but it requires taking care to recognize that they need to be provided.)

• Adjustor thunks, used to convert between various classes in a hierarchy.

• Virtual function thunks, used to implement calls to virtual function.

• Virtual function table thunks, used to manage base classes and polymorphism.

• Template code bodies, which are emitted at first use unless explicitly instantiated.

• The virtual function tables themselves.
(Ответить) (Parent) (Thread)
[User Picture]From: zyama_krendel
2006-02-28 04:21 pm

C++ рулез!

А я C++ люблю. ИМХО, то, что ты написал - это именно та красота, которая делает C++ по настояшему "расшираемым" (принимаются любые предложения по другому переводу слова "extandable"). Ты можешь просто использовать то, что есть (разумеется, изучив документацию и поняв, что делается автоматически, а что надо делать самому) и не беспокоиться о том, что происходит у ей внутре. А если беспокоиться приходится - значит, это плохая библиотека, найди или напиши сам другую, хорошую! :))
(Ответить) (Thread)
[User Picture]From: gaius_julius
2006-02-28 04:51 pm

Re: C++ рулез!

нельзя же всё собрать из stl шаблонов да библиотек...

проблемы из-за этой феерической расширяемости начинаются когда с твоим кодом начинает работать другой программист. Зачем делать людей шизофрениками? (-:
(Ответить) (Parent) (Thread) (Развернуть)
[User Picture]From: romanet
2006-02-28 04:24 pm
я не профессиональный програмист, поэтому может скажу чушь.
У меня ощущение, что эти притензии можно высказть по отношению к любому ОО языку. Это значит, что они направлены к ОО програмированию впринципе.
(Ответить) (Thread)
[User Picture]From: ilia_yasny
2006-02-28 04:34 pm
Да, именно. Я не знаком с другими ОО языками, кроме С++, но они тоже, вероятно, поддерживают полиморфизм и множественное наследование. А какова альтернатива?
Недавно прочел книгу "Паттерны проектирования", мне все эти паттерны показались очень полезными, и они, по-видимому, действительно повсеместно применяются, и большую пользу приносит осознанное их применение. Не представляю, как их воплотить на не-ОО языках так, чтобы код можно было бы хоть как-то читать и поддерживать.
(Ответить) (Parent) (Thread) (Развернуть)
[User Picture]From: zyama_krendel
2006-02-28 04:28 pm
Скажи честно: что у тебя упало? ;))
(Ответить) (Thread)
[User Picture]From: seann
2006-02-28 04:44 pm
Не надо отдельный журнал.

Есть такое замечательное развлечение - не знаю, насколько оно распространено - следить за чужой мыслью (логикой, скачками интуиции), причем предмет разговора не важен. Чем он темней, тем увлекательнее подглядыванье за чужими мозгами.
(Ответить) (Thread)
[User Picture]From: _noss
2006-02-28 06:09 pm
+1
(Ответить) (Parent) (Thread)
[User Picture]From: nayname
2006-02-28 04:47 pm
С++ хорош для индустриального программирования, и все. Есть язык журналистский, а есть писательский. Писатель любит смотреть на свое произведение как на законченную картину, а журналисту нужно набросать слов чтобы место в полосе закрыть. При всем богатсве и мастерстве языка первого нужнее второй, и плох тот писатель который в наши времена не "встает в строй". И это хорошо по-моему. Потому что язык всегда - просто язык, тем более язык программирования. ОН инструмент, инструмент общения. НЕ делаейте из напильника скрипку страдивари)))
(Ответить) (Thread)
[User Picture]From: zyama_krendel
2006-02-28 04:50 pm
"Ну, не скажите, батенька, не скажите..." (с)

Решите вот эту проблему, причём сами, не заглядывая в решение (т.е. самому написать класс auto_ptr), и после этого скажите ещё, что это не Высокое Искусство! :))
(Ответить) (Parent) (Thread) (Развернуть)
From: ex_ex_zhuzh
2006-02-28 05:02 pm
По поводу C++ категорически не согласен, все не так. Объявление переменной всегда вызывает конструктор (и понятно, где его искать); никаких известных операторов нет, их никто никому не обещал; деструкторы вызываются известно где — там, где нужно, а не там, где захочет сборщик мусора; и т.д. Все известно и понятно, просто все происходит не там, где в C, а в совершенно других местах — у меня с этим никаких проблем нет, но я понимаю, почему это может не нравиться людям, выросшим на C. C++, конечно, плохой язык, но совсем по другим причинам :)
(Ответить) (Thread)
[User Picture]From: jbaruch
2006-02-28 05:38 pm
деструкторы вызываются известно где — там, где нужно, а не там, где захочет сборщик мусора
Хе-хе.. Вообще-то у него (в отличие от программиста) есть известный и несложный алгоритм. Где он чего делает прекрасно известно. А вот где программист чего сделает, точнее, где забудет чего сделать, если повезет, выяснит QA.
(Ответить) (Parent) (Thread) (Развернуть)
[User Picture]From: jbaruch
2006-02-28 05:36 pm
2) ППКС
(Ответить) (Thread)
[User Picture]From: d_m_
2006-02-28 09:38 pm
ППКС под Вашим ППКС :)
(Ответить) (Parent) (Thread)
[User Picture]From: dorjechang
2006-02-28 05:36 pm
1. Гвидо занимается языком, а не оптимизатором. Это во-первых. Во-вторых, при всей "божественности" рекурсии использование её оправдано лишь там, где присутствует рекурсивное же определение (да и то не во всех случаях). Это я к тому, что нет смысла при наличии ещё многих задач заморачиваться на поддержке того, что не является необходимым, или даже очень уж интенсивно используемым. И в-третьих, Python - это не Perl (за что и люблю первого, ибо со второго откровенно хочется блевать), и откровенно идиотского подхода "Если что-то можно сделать сотней способов, то извратись и придумай сто первый" (а это один из идеологических принципов Perl, хотя и в моём свободном изложении) в нём нет. А есть даже почти обратный. Так вот, зачем городить что-то в реализации (что может вызвать и сложности на "более других" платформах), если это можно сделать средствами языка (изящный пример чему ты и сам нашёл).

2. Согласен с теми, кто говорит, что это наезд не на C++, а на все языки, поддерживающие ООП. И на Python в том числе. Идея очень проста - не надо писать на языке, который ты не очень хорошо знаешь. А если даже и знаешь его хорошо, то не надо инстанцировать объекты классов, поведения которых ты не знаешь. Сначала изучи документацию или/и реализацию конкретного класса. Чтобы прикрыть себе задницу от неожиданностей типа тех, что ты описываешь.

Беда C++ вовсе не в том, что он мультпарадигменный. На мой взгляд, она в том, о чём Страуструп изначально предупреждал (только вот мало кто прислушивается) - не надо сравнивать C++ с C. И он же писал о том, что заматерелым сишникам придётся тяжело, может, лучше и остаться чистым сишником. Это принципиально другой язык, и сравнения неуместны.

Резюме: большинство (но не все) из того, что массы считают бедами C++ - беда конкретного представителя этих самых масс.
(Ответить) (Thread)
[User Picture]From: quappa
2006-03-01 02:16 pm
1. BTW, в Перле есть встроенный механизм tail-call optimization. Не в компиляторе, а средство языка. Правда, всего один :)
(Ответить) (Parent) (Thread)
[User Picture]From: freeborn
2006-02-28 05:38 pm

не согласен

1. возможно, красиво. но коэффициент полезного действия падает - большую часть времени программа ползает по стеку, вместо выполнения полезного кода. в С++ как раз принцип обратный - все красивости (templates, inline) компилируются в эффективный код.

2. Абстракция - лучший, если не единственный способ справиться с возрастающей сложностью проектов. чистый С неудобен именно трудностью масштабирования, в отличие от С++. Достаточно один раз понять (или реализовать) -и потом не нужно задумываться о том, чьи там конструкторы и overloaded методы выполняются.

(вынужден признаться, что большую часть рабочего времени пишу не на С++, а на C#, хотя и с generics - рынок, знаете ли.)
(Ответить) (Thread)
[User Picture]From: zyama_krendel
2006-02-28 05:47 pm

Про Python

...Все так набросились на фразу про C++ (и правильно сделали), что про Python забыли напрочь. :))

А по мне - так "хвостовая оптимизация рекурсии" есть средство реализации языка; если это реализовано, то стоит его использовать - писать рекурсивные функции.

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

Правда, один возможный ответ я знаю сам: "зато красиво" (с) :)) (как-нибудь напишу у себя в журнале, откуда у меня эта фраза).
(Ответить) (Thread)
[User Picture]From: avva
2006-02-28 08:41 pm

Re: Про Python

Нет, погоди, всё не так просто. Есть языки, где реализовать хвостовую оптимизацию в общем случае невозможно. Возьми C++: у тебя после выхода из этого рекурсивного вызова ещё деструкторы твоих объектов должны по-вызываться; если ты их вызовешь до рекурсии, порядок будет другой и может что-то испортить. Поэтому если у тебя есть местные переменные с деструкторами -> всё, кирдык оптимизации.

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

Переписать функцию, которая естественным образом пишется рекурсией, без неё - в общем случае очень нетривиальное занятие, которое часто приведет тебя к очень уродливому коду. На практике просто редко встречаются столь глубокие рекурсии, чтобы угрожать стеку, и это спасает. Но с оптимизацей всё равно значительно быстрее, кроме того факта, что стек не растёт неограниченно.
(Ответить) (Parent) (Thread) (Развернуть)
[User Picture]From: baccara
2006-02-28 05:48 pm
“If C gives you enough rope to hang yourself, then C++ gives you
enough rope to bind and gag your neighborhood, rig the sails on a
small ship, and still have enough rope to hang yourself from the
yardarm”
—Anonymous

(C) The UNIX-HATERS Handbook
ссылку не дам, мне её в PDF дали
ругают там среди прочего и С++ (более эмоционально нежели конструктивно, на мой взгляд), мысли выказывают сходные.
(Ответить) (Thread)
[User Picture]From: avva
2006-02-28 05:52 pm
Я когда-то давно её читал, да. Там всё же в основном на эмоциях выезжают, действительно ;) но смешно.
(Ответить) (Parent) (Thread)
Страница 1 из 4
<<[1] [2] [3] [4] >>