Энтони Хоар, человек который ввёл в употребление NULL-указатель высказал следующую мысль: I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years. Простите, что не по-русски, не нашёл качественного перевода. Суть в том, что он считает введение NULL ошибкой, которая стоила многих сил, и что вместо введения NULL необходимо было ввести дополнительные проверки во время компиляции. Мне интересно, возможна ли жизнь без NULL, и как это особенно согласуется с динамическими языками (быть может для языков со статической типизацей этого и можно было избежать, а для динамических -- нет?). Вообще, как много есть языков, которые обходятся без NULL или эквивалента? Как минимум один мне известен: Haskell. UPD Для языков со статической типизацией можно проанализировать код и увидеть все ли переменные инициализированны. (Попробовать обойтись без NULL) Для динамических языков (не только с динамической типизацией, а таких, где есть eval или похожий инструмент) такого гарантированно сделать нельзя.
Ответ
Проблема заключается не в самом null, его использование абсолютно легально, т.к. является по сути не артефактом конкретного языка программирования, а вычислительным приемом, паттерном, - общим для всей теории программирования. Всегда существуют вычислительные процессы (функции), работу которых можно оценивать с двух позиций: 1) есть результат; 2) нет результата. C этой точки зрения значение null есть унифицированный способ кодирования ситуации "нет результата".
Проблема заключается в способе интеграции этого паттерна в систему типов языка. Значение null в большинстве языков не имеет типа, точнее, null является значением некоторого специального типа, являющегося подтипом ВСЕХ типов нашего языка. Поэтому null может быть числом, строкой, кнопкой пользовательского интерфейса, и вообще принимать любую форму. По сути дела, null - это "хак", непонятно как вписавшийся в статическую типизацию артефакт динамической типизации. Именно здесь начинаются проблемы, о которых пишет Хоар, и с которыми я согласен на 200%. Такой способ интеграции значения null означает, что в ЛЮБОМ месте, где мы ожидаем некоторое значение, мы можем получить null. И для нас как программистов нет способа гарантированно узнать, получим ли мы его или нет, а для компилятора гарантированно проверить, что в коде мы учли ту ситуацию, когда вместо ожидаемого результата получен null. Ситуацию могли бы несколько поправить (но не спасти!) хорошая документация и дисциплинированность программистов. Если бы не тот факт, что именно этих "добродетелей" в реальности почти не встретишь.
Чтобы исправить проблемы null, нужно выполнить два условия:
программисты должны быть лишены возможности использовать null в тех местах, где ЯВНО не объявили такую возможность;
компилятор должен проверять и гарантировать нам, что клиентский код учел все такие ситуации, где вместо результата может быть получен null.
Это можно сделать одним способом: сделать null значением некоторого обычного типа. В Haskell этим типом является Maybe, в Scala - Option, в F# - option. Значения null для этих языков называются соответственно Nothing, None и снова None. Тогда все встает на свои места:
Выполнение 1-го условия:
// Ошибка компиляции: мы не указали ЯВНО, что можно использовать None
def notLessThan5(x: Int): Int = if (x >= 5) x else None
// OK
def notLessThan5(x: Int): Option[Int] = if (x >= 5) Some(x) else None
Выполнение 2-го условия:
// Ошибка компиляции: мы не учли, что notLessThan5() может не вернуть результата (вернуть None)
println(notLessThan5(10) + notLessThan5(3))
// OK
println(notLessThan5(10).getOrElse(5) + notLessThan5(3).getOrElse(5)) // напечатает 15
Во всех случаях соблюдение правильных принципов работы со значениями null проверит за нас компилятор, не допустив, чтобы NullPointerException "всплыла" в самый неподходящий момент во время эксплуатации программы. Проблемы null решены. Следует особенно заметить: мы не отказываемся от использования null. Мы просто интегрируем этот null в систему типов иначе, чем это сделано сейчас, в Java и им подобных. None, Nothing и проч. - есть точные эквиваленты null, только интегрированные в систему типов.
Динамические языки программирования не используют преимущества статической типизации, поэтому все, что описано выше, их не касается. Избежать тех проблем, о которых говорил Хоар, в динамических языках нельзя в принципе - любая функция может вернуть ЛЮБОЕ значение. Это касается не только null, но вообще любых значений. Поэтому умные люди, которые используют динамические языки в повседневной практике, давно уже описали в литературе защитные практики от гибкости динамических языков. Основной практикой, помимо дисциплины и документации, является повсеместное и всеобъемлющее тестирование кода, причем как можно раньше, в идеале, до написания самого кода (TDD). Чудес не бывает: верификацию при проверке типов приходится заменять верификацией при тестировании. На эту тему рекомендую послушать великолепный доклад Robert Martin'а, произнесенный на RailsConf'09, "What Killed Smalltalk Could Kill Ruby, Too"
Комментариев нет:
Отправить комментарий