elizarov


Блог Романа Елизарова


Previous Entry Share Next Entry
Проверяемые исключения и аннотация эффектов методов
elizarov

Недавно sorhed и akuklev опубликовали очень интересные слайды, в которых они подробно описывают как можно расширить язык Scala таким образом, чтобы тип функции описывал все её возможные эффекты. Причем не только для того, чтобы просто отличать чистые функции без побочных эффектов от всего остального, а с полной поддержкой всего многообразия промежуточных вариантов: функции выводящие в лог, пишущие и читающие из определенных файлов, кидающие определенные исключения и т.п. При этом результирующий код не сильно отличается от обычного императивного кода и подробно указывать типы по сути нужно только при объявлении функции. Большое преимущество кода написанного таким образом в том, что четкий контроль над эффектами потенциально позволяет безопасно паралеллизовывать выполнения тех веток кода, которые не зависят друг от друга. Да и вообще такой универсальный механизм позволяет на этапе компиляции контролировать множество различных аспектов кода, которые сейчас порой приходится описывать в его документации. Но я не будут вдаваться в подробности, а расскажу про то, как это связано с проверяемыми исключениями.

По сути, механизм проверяемых исключений в Java это частный и очень узкоспециализированный механизм для контроля за эффектами. При прочтении любого труда, который посвящен программированию с эффектами, сразу бросается в глаза общность между ними и проверяемыми исключениями — если вы используете метод, который, например, кидает (throws) IOException (или объявлен как имеющий какой-то эффект), то не приняв специальных мер по его изоляции, обертке или какой-то другой специальной обработке, ваш метод тоже должен быть объявлен как кидающий IOException. Объявление "throws IOException" по сути является аннотацией того факта, что метод занимается вводом/выводом, а, например, "throws InterruptedException" это аннотация того факта, что метод ждет того или иного события и может быть прерван.

Любопытно, что в настоящий момент наблюдаются две совершенно противоположные тенденции в развитии языков программирования. С одной стороны, как обратил внимание yakov_sirotkin в заметке "Конец Checked Exceptions", многие современные post-Java языки, такие как Kotlin (а так же Fantom, Ceylon и другие) не включают поддержку проверяемых исключений. И вообще, бытует популярное мнение, что "Next Big JVM language" (одноименная заметка Stephen Colebourne) не должен поддерживать проверяемые исключения, ибо (цитата из заметки выше) "Spring rejects them. Hibernate rejects them. Java EE rejects them. They are a failed experiment (good in theory, bad in practice)." С другой стороны, теория типов, развивающаяся не только в чистой теории, но и на практике в языках типа Haskell и Scala, позволяет встроить в единый каркас системы типов языка программирования не только списки проверяемых исключений, которые может кинуть функция, но и массу другой информации о возможных эффектах её выполнения.

Серьезная проблема проверяемых исключений всегда была в неумении их использовать. Я уже приводил примеры в своей предыдущей заметке про обработку ошибок в API высокого уровня. И я рад, что умение правильного использования проверяемых исключений постепенно становится всё более распространенным. Однако это далеко не единственная проблема. Система типов в Java недостаточно гибка, чтобы можно было полной мере удобно пользоваться проверяемыми исключениями. Даже IOException, который, казалось бы, по делу используется в стандартных API файлового ввода вывода Java, мешает в классе ByteArrayInputStream и аналогичных классах, которые на самом деле не могут кинуть IOException (в силу фактического отсутствия ввода вывода). Программист все равно обязан что-то делать с IOException, который торчит из всех API ввода-вывода, как, например, из класса-адаптера InputStreamReader, даже если он создан на основе ByteArrayInputStream и IOException в нем не может возникнуть.

Если бы в системе типов Java была возможность обобщать кидаемые методами исключения так, чтобы одна версия интерфейса InputStream могла бы быть использована как с исключениями так и без, то проблему можно было бы решить, но ценой еще большего усложнения всех API, в которые пришлось бы добавлять соответствующие параметры типов. Искусство дизайна правильных API стало бы еще сложней. И это беда любой системы контроля за эффектами. Как только она начинает использоваться в нетривиальных API, в любом шаблоне адаптер и аналогичных ему сразу же возникает проблема обобщения эффектов. Эффект метода начинает сложным образом зависеть от эффекта тех методов, которые он использует. Интерфейсы вынуждены обобщать эффекты своих методов, ибо различные их фактические реализации могут иметь различные эффекты. Похожую проблему можно увидеть c модификатором const в C++, ибо это тоже частый случай проблемы контроля эффекта метода.

Проверяемые исключения в Java и const в C++ вызывают похожую реакцию основной массы программистов. Увидев возникающие проблемы, разработчики языков программирования начинают их избегать при разработке следующих поколений языков программирования. В то время как и то и другое безусловно позволяет программисту более точно выразить эффекты метода в его сигнатуре так, чтобы правильность кода могла бы быть проверена компилятором. А это способствует написанию более надежного программного обеспечения. Дизайнерам универсальных языков программирования следующего поколения предстоит решить проблему уточнения системы типов для аннотации эффектов методов таким образом, чтобы дизайн правильных API и их использование не становились бы от этого заметно сложней.


  • 1
Чуть-чуть перефразирую то, что уже написано в посте.

Основная проблема с декларацией эффектов коротко формулируется так: это нарушает инкапсуляцию. Эффекты, указанные в формальном описании API показывают наружу соответствующие аспекты реализации. (Типы, кстати, делают то же самое, но с типами мы уже научились работать). Найти правильный баланс между необходимостью инкапсулировать реализацию и необходимостью контролировать эффекты пока никому не удалось. Если удастся, это, вероятно, будет такое же хрупкое равновесие как вывод типов Хиндли-Милнера...

Типы, исключения и эффекты не имеют прямого отношения к инкапсуляции. Они лишь механизмы языка, которые позволяют дизайнеру API сообщить информацию о поведении метода доступным для компилятора образом. Инкапсуляция зависит от качества дизайна API. Например, если дизайнер API высокого уровня, которое реализовано поверх файлов (и это лишь деталь реализации), добавляет throws IOException в сигнатуры своих методов, то он тем самым нарушает инкапсуляцию. Он совершил бы такую же ошибку и программируя на языке C, если бы возвращал из функции специальный код возвата в случае ошибки, а подробная информация об ошике ввода вывода была бы доступна через errno. Здесь ошибка в дизайне API одна и та же, и она не зависит от языка -- это отсутствие инкапсуляции при работе с файлами. Только в Java эта ошибка в данном случае видна прямо в сигнатуре метода, а на C надо копаться в реализации, чтобы её обнаружить.

Все адекватные системы эффектов, как и проверямые исключения, позволяют инкапсулировать (обрабатывать) эффекты внутри реализации. Так же как и на C можно обработать все ошибки низкого уровня внутри реализации API. В этом смысле презентация для Scala, со ссылки на которую я начал свою заметку, вполне адекватна. Например, метод который использует Logged методы не обязан сам быть Logged, как и метод использующий файлы не обязан кидать IOException.

Edited at 2012-04-17 01:28 pm (UTC)

давайте признаем очевидные факты:
1. в самой яве ОЧЕНЬ умные товарищи окакзались не в состоянии продумать checked exceptions и воплотить их стройным/непротиворечивым образом в core библиотеках java. Т.е. люди ЕДИНСТВЕННОЙ задачей которых было создать библиотеку для языка с использованием checked exceptions c этой задачей не справились (epic fail).

2. В отличии от этих оч. умных товарищей значительно менее умные (я себя к последним тоже отношу) пишут код который имеет какое-то коммерческое примененние т.е. присутствуют доп. ограничения в виде сроков клиентов ну и проч...

3. фееричных достижений в плане отказоустойчивости/ меньшего кол-ва ошибок у явы по сравнению с другими языками (без checked exceptions) нет.

4. именно по этим причинам поддержки checked exceptions нет в пост-java языках. именно поэтому вменяемые frameworks их не используют.

мое личное мнение - они мешают больше чем помогают. ибо превращают код в мясо try/catch из-за необходимости конвертации exceptions (абстрагирования нижележащих модулей).

так что да, можно создать кружок по интересам и кучке сектантов рассказывать что мир не понял всю мощь checked exceptions. а можно жить дальше и думать/программировать в portable идиомах. идиома checked exceptions - абсолютно не портабельный костыль, без которго ходить и бегать гораздо удобней.


Ну разумеется без checked exceptions в коде не будет try catch!
И вообще все остальные программисты, кроме программистов Sun/Oracle пишут подробную документацию и указывают все исключения которые может выбросить метод, в каких случаях и что с этим делать.

По факту (Delphi) любой метод может выкинуть неизвестно что. В итоге весь боевой код покрывается try catch и в процессе эксплуатации изучаются вылетающие исключения и делается их отличаня от default обработка.

Так удачно эмулируется скиптовый нетепизированный язык на компилируемом строготипизированном языке.
Разумеется эту же мину Андерс Хейлсберг бережно перенёс и в net

вот я как-то сильно не согласен с тезисом, что без checked exceptions не будет try catch -- будут, но только там где это нужно.. и я, видимо, с не очень большим количеством языков знаком, но ... где кроме явы есть checked exceptions?

Рома прав в том что холиварить не будем :)

Я не планировал этой заметкой начать холивар по поводу нужности или ненужности проверяемых исключений. Это примерно как спорить на тему статической или динамической типизации. В этой заметке я лишь хотел показать общие моменты в контроле за эффектами и проверяемыми исключениями — они решают похожие задачи и, безусловно, создают похожие проблемы.

Я как-то тоже писал у себя про использование java checked exceptions не по назначению (константность, ассерт-блоки). http://yurikravchik.livejournal.com/2494.html

Ещё задавал вопрос на форуме, о возможности реализации "маркированных методов" в скале http://it-talk.org/topic15681.html

В mlgame.ru я маркировку использую в полный рост - как раз для отделения блоков ассёртов от блоков применений.

Edited at 2012-08-04 04:07 pm (UTC)

  • 1
?

Log in

No account? Create an account