Часто утверждают, что динамическая типизация является более выразительной , и я, по большей части, готов согласиться с этой характеристикой. По своей природе язык с динамической типизацией накладывает меньше ограничений на меня, программиста, и позволяет мне «выражать себя» более свободно. Я не собираюсь сейчас приводить аргументы в пользу статической типизации, которая в любом случае вполне понятна любому, кто когда-либо писал или поддерживал кусок кода приличного размера с использованием IDE. Однако я хотел бы указать на особый смысл, в котором динамическая типизация гораздо менее выразительна, чем статическая типизация, и последствия этого.
(Теперь, пожалуйста, потерпите меня немного, потому что я собираюсь пойти по довольно живописному маршруту, чтобы добраться до моей реальной точки.)
В двух словах, динамическая типизация менее выразительна, чем статическая, поскольку она не полностью выражает типы . Конечно, это довольно глупая тавтология. Но это все еще стоит сказать. Почему? Потому что типы имеют значение. Даже в динамически типизированном языке, таком как Python, Smalltalk или Ruby, типы по-прежнему имеют значение. Чтобы понять и поддерживать код, мне все еще нужно знать типы вещей . В самом деле, это верно даже для слаботипированного JavaScript!
Следствием этого является то, что динамически типизированный код гораздо менее самодокументируется, чем статически типизированный код. Быстро, что я могу перейти к следующей функции? Что я от этого получу?
1
|
function split (string, separators) |
Эта функция подписи, переведенная на Цейлон, сразу стала более понятной:
1
|
{String*} split (String string, {Character+} separators) |
Это, конечно, означает, что динамическая типизация возлагает гораздо большую нагрузку на программиста, чтобы добросовестно комментировать и документировать вещи . И поддерживать эту документацию.
С другой стороны, статическая типизация вынуждает меня поддерживать правильные аннотации типов в функции split()
, даже когда ее реализация меняется, даже когда я спешу, и моя IDE даже поможет мне автоматически реорганизовать их. Ни одна IDE на Земле не предлагает такую же помощь, поддерживая комментарии!
Теперь подумайте, что еще эта дополнительная выразительность покупает мне. Предположим, что у Foo
и Bar
нет общего интересного супертипа. На любом динамическом языке я мог бы написать следующий код:
1
2
3
4
5
6
7
|
class Super { function fun() { return Foo(); } } class Sub extends Super { function fun() { return Bar(); } } |
Но если бы я написал такую вещь, мои товарищи по команде, вероятно, захотели бы меня задушить! Здесь просто слишком много возможностей для клиента, вызывающего fun()
чтобы обрабатывать только случай Foo
и не понимать, что иногда он вместо этого возвращает Bar
. Как правило, большинство программистов избегают кода, подобного описанному выше, и следят за тем, чтобы fun()
всегда возвращала типы, тесно связанные наследованием.
В качестве второго примера рассмотрим известную дыру в системе типов Java: null
. На Java я мог бы написать:
1
2
3
4
5
6
7
|
class Super { public Foo fun() { return Foo(); } } class Sub extends Super { public Foo fun() { return null ; } } |
Опять же, этого часто избегают разработчики Java, особенно для общедоступных API. Поскольку подпись fun()
не является частью того, что он может возвращать null
, и поэтому вызывающая сторона может просто не обращать внимания на этот случай, что приводит к NPE где-то еще дальше, часто рекомендуется использовать исключение вместо возвращает значение null
из метода, принадлежащего общедоступному API.
Теперь давайте рассмотрим Цейлон.
1
2
3
4
5
6
7
|
class Super() { shared default Foo? fun() => Foo(); } class Sub() extends Super() { shared actual Null fun() => null ; } |
Поскольку тот факт, что fun()
может вернуть значение null
является частью его сигнатуры, и поскольку система типов заставляет любого вызывающего пользователя учитывать возможность возврата значения NULL, в этом коде нет ничего плохого. Поэтому на Цейлоне часто лучше определять функции или методы, которые просто возвращают null
а не генерируют защитное исключение. Таким образом, мы приходим к очевидному парадоксу:
Будучи более строгими в том, как мы обрабатываем нуль, мы делаем ноль намного более полезным.
Теперь, поскольку «обнуляемый тип» на Цейлоне — это просто особый случай типа объединения, мы можем обобщить это наблюдение для других типов объединения. Рассматривать:
1
2
3
4
5
6
7
|
class Super() { shared default Foo|Bar fun() => Foo(); } class Sub() extends Super() { shared actual Bar fun() => Bar(); } |
Опять же, в этом коде нет ничего плохого. Любой клиент, вызывающий Super.fun()
, вынужден обрабатывать оба возможных конкретных типа возврата.
Я хочу сказать, что мы достигли чистой выразительности, добавив статические типы. Вещи, которые были бы опасно подвержены ошибкам без статической типизации, стали полностью безопасными и полностью самодокументирующимися.