дженерики (программистское)
May. 22nd, 2010 12:26 am(эта запись может быть интересна разве что программистам)
Прочитал Java Generics FAQ. 500 страниц, между прочим, в PDF-версии. 500 страниц. Когда вы в последний раз читали FAQ на 500 страниц? Ну вот и я тогда же. С трудом верится даже теперь, по прочтении. Правда, там много места занимают бесконечные индексы и оглавления и заголовки, так что реально "мяса" страниц на 350. Но и этого довольно.
Теперь я понимаю, зачем в Джаве класс Enum определяется, как Enum<E extends Enum<E>>, что это в точности значит, и зачем это нужно. Не уверен, что мне нравится, что я это теперь понимаю.
Многие объяснения в этом фэке читаются, как один сплошной WTF. Ты понимаешь, в процессе чтения, почему это сделано так, а не иначе. Почему и тут исключение, и тут, и это надо делать через задницу, а то даже через задницу не сделать. Почему - один пример наугад из сотни - все обычные вещи с параметризованными типами можно делать, а вот создавать массивы из них нельзя. Кроме случая, когда они параметризованы неограниченными вопросиками. Ты понимаешь, почему это так получается, но не оставляет ощущение глубокого WTF на тему того, как мучительно и болезненно эти дженерики врастают в плоть языка. Сказать, что это leaky abstraction - ничего не сказать; эта лодка не протекает, она буквально состоит из воды.
Вот один из прекрасных вопросов из этого FAQ:
По-моему, внесение дженериков в Джаву было огромной ошибкой. Главная польза от них - строгая типизация коллекций - не оправдывает той огромной цены, которую пришлось заплатить: сложностью языка, читабельностью и понимабельностью кода. Учитывая то, что динамическая безопасность у коллекций и так была, необходимость безопасности на уровне компиляции была кем-то, по-видимому, сильно преувеличена. Да, конечно, намного удобнее и проще написать ArrayList<String>, и знать, что попытка всунуть туда что-то другое вообще не скомпилируется. Это удобнее, чем всунуть что-то не то в ArrayList и получить исключение в рантайме, когда это взяли и попытались привести к String. Баги ловятся быстрее. Но насколько быстрее, как часто на практике это оказывалось существенным, и оправдывает ли эта польза введение архитектуры, приводящей к Enum<E extends Enum<E>> или вопросам, подобным процитированному выше?
Одним из главных преимуществ Джавы по сравнению с C++ было именно grokability исходного кода средним программистом: посмотрев на исходник какого-то класса, который он раньше не видел, средний программист на Джаве - не гуру, не хватающий звезды с неба итд. - мог сказать, что делает каждая строка и зачем это нужно. Дженерики это свойство языка с помпой похоронили. Кто были люди, которые приняли решение так поступить, и радовались ли они красивому трюку само-референтных типов? И это только ущерб понятности кода, не говоря о всем этом огромном числе исключений и плохой совместимости с существующими частями языка - массивами, рефлекцией, исключениями, итд. итд. итд.
Можно ли было добиться той же пользы, что дали дженерики, менее разрушительными способами? Пусть не той же, но главной ее части, мне кажется, можно было. Скажем, какая-нибудь аннотация в момент создания коллекции, подсказывающая компилятору, что там должно быть. Просто сказать: в эту коллекцию я хочу класть строки, или такие-то пары, итп. И пусть компилятор проверяет, что сможет, и вставляет эксплицитное приведение к строкам или парам, когда мы из коллекции что-то достаем. И все. Без extends, без wildcards, для всех мест, где это действительно надо, пусть программист продолжает приводить эксплицитно. Конечно, такая аннотация не покрыла бы все случаи, но самые простые и важные, думаю, покрыла бы. Главное ведь то, что у дженериков почти нулевой эффект на рантайм, поэтому необязательно было вносить их глубоко в ткань языка, so to speak; подсказки компилятору достигают схожей цели. Простая (простая!) и удобная подсказка захватила бы, мне кажется, большинство багов с типами в коллекцях, которые до дженериков проявлялись только в рантайме.
(disclaimer: я редко и мало пишу на Джаве, и не эксперт в ней)
Прочитал Java Generics FAQ. 500 страниц, между прочим, в PDF-версии. 500 страниц. Когда вы в последний раз читали FAQ на 500 страниц? Ну вот и я тогда же. С трудом верится даже теперь, по прочтении. Правда, там много места занимают бесконечные индексы и оглавления и заголовки, так что реально "мяса" страниц на 350. Но и этого довольно.
Теперь я понимаю, зачем в Джаве класс Enum определяется, как Enum<E extends Enum<E>>, что это в точности значит, и зачем это нужно. Не уверен, что мне нравится, что я это теперь понимаю.
Многие объяснения в этом фэке читаются, как один сплошной WTF. Ты понимаешь, в процессе чтения, почему это сделано так, а не иначе. Почему и тут исключение, и тут, и это надо делать через задницу, а то даже через задницу не сделать. Почему - один пример наугад из сотни - все обычные вещи с параметризованными типами можно делать, а вот создавать массивы из них нельзя. Кроме случая, когда они параметризованы неограниченными вопросиками. Ты понимаешь, почему это так получается, но не оставляет ощущение глубокого WTF на тему того, как мучительно и болезненно эти дженерики врастают в плоть языка. Сказать, что это leaky abstraction - ничего не сказать; эта лодка не протекает, она буквально состоит из воды.
Вот один из прекрасных вопросов из этого FAQ:
What is the difference between a Collection<Pair<String,Object>>, a Collection<Pair<String,?>> and a Collection<? extends Pair<String,?>>?Ответ начинается так: "All three types refer to collections that hold pairs where the first part is a String and the second part is of an arbitrary type. The differences are subtle." Потом на двух страницах объясняются эти subtle differences.
По-моему, внесение дженериков в Джаву было огромной ошибкой. Главная польза от них - строгая типизация коллекций - не оправдывает той огромной цены, которую пришлось заплатить: сложностью языка, читабельностью и понимабельностью кода. Учитывая то, что динамическая безопасность у коллекций и так была, необходимость безопасности на уровне компиляции была кем-то, по-видимому, сильно преувеличена. Да, конечно, намного удобнее и проще написать ArrayList<String>, и знать, что попытка всунуть туда что-то другое вообще не скомпилируется. Это удобнее, чем всунуть что-то не то в ArrayList и получить исключение в рантайме, когда это взяли и попытались привести к String. Баги ловятся быстрее. Но насколько быстрее, как часто на практике это оказывалось существенным, и оправдывает ли эта польза введение архитектуры, приводящей к Enum<E extends Enum<E>> или вопросам, подобным процитированному выше?
Одним из главных преимуществ Джавы по сравнению с C++ было именно grokability исходного кода средним программистом: посмотрев на исходник какого-то класса, который он раньше не видел, средний программист на Джаве - не гуру, не хватающий звезды с неба итд. - мог сказать, что делает каждая строка и зачем это нужно. Дженерики это свойство языка с помпой похоронили. Кто были люди, которые приняли решение так поступить, и радовались ли они красивому трюку само-референтных типов? И это только ущерб понятности кода, не говоря о всем этом огромном числе исключений и плохой совместимости с существующими частями языка - массивами, рефлекцией, исключениями, итд. итд. итд.
Можно ли было добиться той же пользы, что дали дженерики, менее разрушительными способами? Пусть не той же, но главной ее части, мне кажется, можно было. Скажем, какая-нибудь аннотация в момент создания коллекции, подсказывающая компилятору, что там должно быть. Просто сказать: в эту коллекцию я хочу класть строки, или такие-то пары, итп. И пусть компилятор проверяет, что сможет, и вставляет эксплицитное приведение к строкам или парам, когда мы из коллекции что-то достаем. И все. Без extends, без wildcards, для всех мест, где это действительно надо, пусть программист продолжает приводить эксплицитно. Конечно, такая аннотация не покрыла бы все случаи, но самые простые и важные, думаю, покрыла бы. Главное ведь то, что у дженериков почти нулевой эффект на рантайм, поэтому необязательно было вносить их глубоко в ткань языка, so to speak; подсказки компилятору достигают схожей цели. Простая (простая!) и удобная подсказка захватила бы, мне кажется, большинство багов с типами в коллекцях, которые до дженериков проявлялись только в рантайме.
(disclaimer: я редко и мало пишу на Джаве, и не эксперт в ней)
no subject
Date: 2010-05-21 09:39 pm (UTC)no subject
Date: 2010-05-21 09:43 pm (UTC)Наверное, мой узус гораздо уже того, что вы описываете. Дальше List<POJO> я не иду. Просто незачем. Хотя бы потому, что мои задачи всегда опираются на схему (базы) данных, а JPA оперирует именно этими POJO и List'ами из них.
no subject
Date: 2010-05-21 09:44 pm (UTC)no subject
Date: 2010-05-21 09:46 pm (UTC)Именно это генерики и делают.
Collection<String>
Вот, я сейчас сказал, что в эту коллекцию я хочу класть строки. Компилятор проверяет, что может, и вставляет эксплицитное приведение к строкам или парам, когда мы из коллекции что-то достаем. Хотите, чтобы это делалось проще? Предложите свой дизайн. Не бойтесь, завтра же начните разрабатывать, целый выходной впереди. (Hint: это займет пять лет, и получится хуже, чем существующий дизайн. Been there, done that.)
no subject
Date: 2010-05-21 09:59 pm (UTC)no subject
Date: 2010-05-21 10:07 pm (UTC)В связи с чем философский вопрос — стоит ли подвергать простого программиста воздействию передовых идей пятнадцатилетней давности, или надо еще десять-пятнадцать лет выдержки добавить, чтобы все окончательно утряслось?
no subject
Date: 2010-05-21 10:10 pm (UTC)Но там, хотя бы, все кунштюки с темплейтами можно оправдать тем, что с их помощью можно делать (всё же, полноценный механизм кодогенерации), в джаве же такого нет =(
no subject
Date: 2010-05-21 10:17 pm (UTC)С одной стороны, дженерики говорят программисту: Collection<String> и Collection<Number> - два совершенно разных типа. С другой стороны, у рантайма есть свое представление о типе, в которое это различие не укладывается. Это рантаймовое представление в свою очередь тесно связано с фундаментальными возможностями языка. В такой ситуации расщеплять систему типов на две - для компилятора и для рантайма - не может не привести к сложной, запутанной системе, которую будет тяжело понять.
Если мы определим цель не как "расширить систему типов так, чтобы она могла выразить все, что мы хотим сказать о содержимом коллекций", а "дать подсказку для статического анализа, которая поймает основные баги с содержимым коллекций", то и расщепления, о котором я говорю выше, не будет, и соблазн вводить ? и автореферентные типы - даже и не появится.
no subject
Date: 2010-05-21 10:22 pm (UTC)no subject
Date: 2010-05-21 10:42 pm (UTC)Если ты начинаешь путаться в генериках - значит, твой код дурно пахнет.
Особенно приясно старый 1.4 джава код "портировать" в 1.5, добавляя генерии к коллекциям. То тут то там выясняется, что в переменной хранится 2,3 а то и больше типов данных. И ты не понимаешь, в каком случае - какой. Ну что, добавляем переменную со строгой типизацией - и жить становится лучше.
no subject
Date: 2010-05-21 10:42 pm (UTC)2. В свете вышеизложенного становится непонятным, в чем разница между «типом» и «подсказкой компилятору для статического анализа». То есть что такое тип, я знаю; это такой языковой конструкт с определенными свойствами, а про подсказки компилятору мне непонятно, как они должны быть устроены. Если начать их устройство формально определять, не получим ли мы вторую систему типов? Создается впечатление, что да, получим; может быть, она окажется лучше первой, ну так тогда можно будет первую выкинуть совсем и пользоваться только второй.
no subject
Date: 2010-05-21 10:47 pm (UTC)no subject
Date: 2010-05-21 10:55 pm (UTC)Спасибо большое, тем, кто потратил время добавить их к Джаве. Чего бы это им не стоило.
no subject
Date: 2010-05-21 10:57 pm (UTC)Кроме того, явно видно, за что мы её платим.
В джаве же, как и в посте написано, мы получаем намного меньше, чем даём.
no subject
Date: 2010-05-21 11:02 pm (UTC)Кроме тех случаев, когда хочется массив создать.
Или исключение кинуть.
Или воплотить (из вполне резонных и разумных соображений) Interface<Type1> и Interface<Type2> одновременно.
Или [список можно продолжить].
Да, иногда можно натолкнуться на тонкости, но это иногда.
Если это "иногда" включает в себя вещи, которые программист делает постоянно и рутинно - типа массивов и исключений - а не раз в год по праздникам - то получается одно из двух: либо программист должен понимать, что такое стирание типов и рантаймовые типы, либо должен зазубрить таблицу правил, где можно, а где нельзя применять параметризованные типы, "потому что партия так сказала".
2. В свете вышеизложенного становится непонятным, в чем разница между «типом» и «подсказкой компилятору для статического анализа». То есть что такое тип, я знаю; это такой языковой конструкт с определенными свойствами, а про подсказки компилятору мне непонятно, как они должны быть устроены. Если начать их устройство формально определять, не получим ли мы вторую систему типов?
Разница, полагаю, в том, что тип привязан к значению (value) и путешествует вместе с ним, а подсказка обычно локальна и действует в пределах данных строки/метода/класса/файла. Например, возьмите подсказку @Override: она никак не влияет на сигнатуру метода, а всего лишь помогает компилятору найти типичные ошибки при определении метода.
no subject
Date: 2010-05-21 11:03 pm (UTC)no subject
Date: 2010-05-21 11:10 pm (UTC)А то, что из темплайтов можно устроить "meta-programming", открыли почти случайно и лет через 10 (хотя
Степанов, наверное, догадывался раньше).
В итоге разработчики Java уже знали из опыта С++, что введение параметризованных типов -- это полная машина Тьюринга в compile time, такой встроеный почти Хаскель, но с безумным синтаксисом.
Так что выбора у них не было: или не вводить шаблоны совсем, или сделать легальным такой код, который средний программист не сможет даже прочесть.
no subject
Date: 2010-05-21 11:14 pm (UTC)The type system is very logical (including your Collection
The type system is very logical (including your Collection<Pair<String,Object>> - like example).
Basically, there is a simle rule: generic types name exact types, and if you want to allow for derived types, you need to use "extends" syntax. Also, everything is strongly types at the "parsing unit" level (a statement when parsing a statement; a function declaration and function contents when parsing a function; class declaration and class contents when parsing a class).
Now, I DO consider it a mistake to require the "extends" syntax. It would be best if a derived type could ALWAYS be used when a base type is mentioned. Java's stength has always been at ignoring little "features" in favor of simplicity.
Java, including its generics, is a so much cleaner language than C++, for example, where templates are just humongously complicated preprocessor macros, and defeat the "programming by contract" model.
As for "how do you make a Collection of Strings or Pairs?", the answer is that you should not do that, because "a String or a Pair" is a terrible abstraction, and, luckily, the language resists it! If "a String or a Pair" does represent some abstraction in your program, you should make a class that encapsulates this abstraction and then have a Collection of those classes.
no subject
Date: 2010-05-21 11:28 pm (UTC)no subject
Date: 2010-05-21 11:47 pm (UTC)черт, а нельзя сразу писать на Haskell? :)))) код получается в разы меньше :)
все вкусности хитрого наследования типов я прочувствовал именно в Haskel, где оно очень элегантно и вкусно сделано :)
no subject
Date: 2010-05-21 11:57 pm (UTC)они пошли другим путем. рантайм не имеет почти никакого понятия, чем он оперирует, но есть отдельных интрумент - такой супер-навороченный lint, который читает из комментариев в формате PHPdoc ну или подобном(т.е. просто коммент перед описанием функции), понимает что она принимает и что она выдает на выход.
после чего строит модель всего этого у себя и анализирует.
позволяет ловить какие-то дичайшие и мелкие ошибки в логике, которые точно будут вываливаться в рантайме :)
естественно требуется поддерживать код и комментарии синхронными - но это на порядок легче, уворачиваться от _принудительной_ системы типизации, которую навязывает язык :))))
no subject
Date: 2010-05-22 12:00 am (UTC)Про исключения надо помнить, согласен, но соблазн кинуть исключение генерик-типа возникает редко, и удовлетворить его легко.
Реализовать в одном классе Interface<Type1> и Interface<Type2> — плохая идея уже на уровне исходного языка, независимо от того, есть erasure или нет.
Список можно продолжить, но продолжить его чем-то действительно важным будет непросто (хотя важность in the eye of the beholder).
В общем, никаких постоянных и рутинных вещей не наблюдаю. Именно что раз в год по праздникам. Ну или если приходится иметь дело с legacy кодом (массивы опять же). Тут да, тут надо держать ухо востро постоянно.
2. Типы, привязанные к значениям, не имеют значения ;) Типы привязаны в первую очередь к переменным и выражениям, именно эти типы нас интересуют, и они вполне локальны. У значений-то как раз генерики стерты, их усложнения вообще не касаются.
no subject
Date: 2010-05-22 12:03 am (UTC)Но пока религия не позволяет: Хаскели могли бы, но не снизойдут, а "реальные" программисты не потянут. Пока что последние предпочитают стандартизировать compiler error messages (которые просто трасинг для compile-time кода).
no subject
Date: 2010-05-22 12:04 am (UTC)no subject
Date: 2010-05-22 12:17 am (UTC)