?

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: sartoris
2006-02-28 09:26 pm
Ну как бы получается так: Ява бежит на виртуальной машине. Правильно? Имеется байт-код, который без изменений должен бежать в любой "виртуальной машине". В идеале - всё правильно. На практике же, один и тот же код бежит совершенно по разному (разные версии виртуальных машин, разные модификации среды и так далее). Далее возникает вопрос: а давайте как-то этот код готовить к тому, что он может бежать на разных машинах... Только язык этого особо не позволяет к сожалению.
(Ответить) (Parent) (Thread)
[User Picture]From: dimrub
2006-02-28 09:32 pm
На практике же, один и тот же код бежит совершенно по разному (разные версии виртуальных машин, разные модификации среды и так далее).

Да нет, неправильно. Все виртуальные машины сохраняют backwards compatibility, что означает, что код, написанный для старых версий, продолжает, как правильно, бежать и в более новых - точно так же. Обратное, разумеется, неверно: код, написанный с использованием возможностей, введеных в новых версиях JVM, не может (да и не должен) бежать в более старых версиях. Что же касается модификаций среды, то существуют множество задач, которые с этим не сталкиваются, а даже если и сталкиваются - решение этих проблем уж никак не сложнее, чем на плюсах. Хотя, конечно, Ява не исключение: и на ней можно писать код на Фортране, было бы умение.
(Ответить) (Parent) (Thread)
[User Picture]From: sartoris
2006-02-28 09:43 pm
Так-то оно так, но только в теории. Потому что только в теории кодом исопльзуется исключительно "стандартные библиотеки" (native code для которых - часть "виртуальной машины" на которую, типа, можно бы и рассчитывать).

На практике тот же jODBC на Windows, FreeBSD и Linux работает по разному...:( А то каким месивом оказывается многозадачность в разных системах - вообще молчу... Ну и об "апплетах" - тоже молчу. Тут, надо признаться, что Macromedia всех сделала....
(Ответить) (Parent) (Thread)
[User Picture]From: d_m_
2006-02-28 09:36 pm
А Вас никогда не пугало, что код, сгенерённый С++ для Celeron'а, кто-то запустит на Athlon 64? Вот и с Java байт-кодом та же штука.
(Ответить) (Parent) (Thread)
[User Picture]From: sartoris
2006-02-28 09:44 pm
Если бы... Уровень абстракции не тот.

Хотя обратите внимание: новые комьютеры Apple на Intel процессорах не работают с целым рядом приложений. Причина: несовместимость...
(Ответить) (Parent) (Thread)
[User Picture]From: d_m_
2006-02-28 10:03 pm
Если бы... Уровень абстракции не тот.

Почему нет тот? Часть системы команд, с расчётом на которую компилился наш воображаемый проект, у Celeron и Athlon 64 совпадает. При этом процессоры производятся разными компаниями и имеют совершенно разную архитектуру. Именно такая ситуация и с JVM.
(Ответить) (Parent) (Thread)
[User Picture]From: sartoris
2006-02-28 10:21 pm
Понимаете ли... Если бы всё оставалось на одном лишь уровне базовых операций, которые так элегантно в байткоде записываются, то было бы очень здорово. Но к сожалению на каком-то месте обязательно возникнет какая-то измена с native code, которая к тому же рознится от версии к версии, по разному взрывается и не совместима с параллельными библиотеками в других системах. Это что-то вроде различий между дистрибутивами линукса... Только линукс ходит с чемоданом инструментов позволяющих так или иначе различия между дистрибутивами (а также подходами к ведению системы) разрешать. А Ява, в теории, должна без компиляции бежать всегда и везде. Как только это последнее условие нарушается - возникает неприятность и, что хуже, непредсказуемость.

А язык, да, язык очень красивый. Мне в своё время так нравился, что я чуть из-за него работу не потерял...
(Ответить) (Parent) (Thread)
[User Picture]From: d_m_
2006-02-28 10:25 pm
А Ява, в теории, должна без компиляции бежать всегда и везде

IMHO, практика с теорией не слишком расходится.
Native code, разумеется, работать не станет, если затащить его на чужую платформу. Но при чём тут Java?
(Ответить) (Parent) (Thread)
[User Picture]From: sartoris
2006-02-28 10:35 pm
Хмм... Попробую объяснить на примере:

Представим себе, что есть некий класс с неким методом, который на каждой платформе вызывает свой native code. Допустим, что этот native code относится к подсистеме TCP/IP sockets.

Когда с этим классом общается Java код - класс для него прозрачен. И значит тот же самый код должен на всех платформах работать.

Теперь вспомним насколько различаются принципы работы с TCP/IP sockets в Windows и Linux (для примера) и задумаемся над тем, насколько по разному этот native code будет реагировать на разных платформах.

Если добавить сюда многозадачность, то получится такой коктейль, что лучше сразу застрелиться. Разве что если гений-программист, писавший native code предусмотрел все хитрости возможных функциональных совмещений с другими native code segments в других платформах...

Это, кстати, возвращает нас к изначальному тезису Толи о "спрятаном коде", с которым я в некотором роде согласен. Другой разговор, что я не согласен с выводом о том, что прятанье кода более хитрым образом - решение проблемы...
(Ответить) (Parent) (Thread)
[User Picture]From: d_m_
2006-03-01 12:28 am
Так... что мы обсуждаем?

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

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

Далее Вы заявили, что с переносом native code имеются проблемы. Конечно! На то он и native, тем и отличается от Java. И чем виновата JVM, если java-класс вызывает неудачно портированный native-code? Так не пишите native code вовсе - и будет Вам счастье!

Пример с сокетами я вообще не понял. Во-первых, что за "различные принципы", разве в Linux не берклиевские сокеты? Во-вторых, Java - это не только язык и среда выполнения, это ещё и Java API - набор библиотек, являющихся неотемлимой частью технологии. В частности, если некто решит написать программу, работающую с TCP, он абстрагируется от тонкостей, присущих конкретным операционным системам и воспользуется пакетом java.net, а не станет кропать некий [непереносимый] код на C/C++ с дальнейшим вызовом его из Java. А реализация Java API, поверьте, сделана качественно. Во-первых, львиная доля Java API реализована на Java, и тем переносима. А native code, вызываемый Java API, аккуратно написан 3 раза: для Windows, Solaris и Linux (см.исходники).
(Ответить) (Parent) (Thread)
[User Picture]From: d_ohrenelli
2006-03-01 07:10 am

Berkley sockets

Est' sushestvennaja raznica v podderzhivaemyh
get/setsockopt.

Get/SetSockOpt na raznye layers po raznomu rabotaet.
Timeout v selecte ochen' o raznomu sebja vedet - vplot' do neobhodimosti ispol'zobat' pselect vmesto selecta v nekotoryh mestah
(Ответить) (Parent) (Thread) (Развернуть)
[User Picture]From: sartoris
2006-03-01 08:00 am
А native code, вызываемый Java API, аккуратно написан 3 раза: для Windows, Solaris и Linux (см.исходники).

Вот этому-то я и боюсь верить по уже высказаным причинам:

Разве что если гений-программист, писавший native code предусмотрел все хитрости возможных функциональных совмещений с другими native code segments в других платформах...
(Ответить) (Parent) (Thread) (Развернуть)
[User Picture]From: d_m_
2006-02-28 10:11 pm
С Apple/Intel ситуацию я не знаю, но если мне не изменяет память, до сих пор в Apple использовалась система команд PowerPC (к тому же RISC), а в Intel Core - классическая x86. Зачем мы тогда рассматриваем этот случай?
(Ответить) (Parent) (Thread)
[User Picture]From: gaius_julius
2006-03-01 04:48 am
Apple недавно решила перейти на интеловское железо.
(Ответить) (Parent) (Thread)
[User Picture]From: d_m_
2006-03-01 07:37 am
Я в курсе, я же написал про Intel Core.
(Ответить) (Parent) (Thread)