Страницы

Поиск по вопросам

среда, 4 декабря 2019 г.

Язык программирования без null

#null #дизайн_языка #типизация


Энтони Хоар, человек который ввёл в употребление 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
или похожий инструмент) такого гарантированно сделать нельзя.

    


Ответы

Ответ 1



Проблема заключается не в самом 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".

Ответ 2



Если поставить себе такую цель - написать программу не использующую NULL, скажем на C++, то это вполне осуществимо. Так что, в общем случае без NULL на C++ жить можно. На C# сложнее будет это сделать, поскольку большинство методов в .NET могут вернуть null. И как минимум проверять на null придется. Но это, согласитесь, уже не проблема языка, а проблема библиотек. Были бы библиотеки не использующие null, и программы можно было бы писать без него. К тому же, можно создать классы-обертки для того чтобы они инкапсулировали в себе работу с NULL и для внешнего наблюдателя никаких нулов не будет. Правильные программы на C++ пишутся именно так. Не думаю, что эти рассуждения не верны для других языков.

Ответ 3



Для минимизации вероятности типовых ошибок следует просто использовать стандартные алгоритмические подходы. ArgumentNullException кошернее, чем NullReferenceException, так как с большей вероятностью возникает именно там, где ошибка, а не постфактум.

Комментариев нет:

Отправить комментарий