Comments 180
И спасибо за статью!
Плюс, можно поподробнее описать как те или иные виды типизации реализованы различных языках программирования.
Однако статья итак вышла длинной, а парадигмы программирования — еще более глубокая тема.
В любом случае я хотел донести до читателя только те знания, которые с помогут в программировании, и что главное, будут интересны каждому. И в следующей статье я также попытаюсь извлечь из большой темы самую полезную и интересную информацию.
Книга переведена на русский язык. Можно нагуглить по названию «Типы в языках программирования».
blogs.msdn.com/b/ericlippert/archive/2012/10/15/is-c-a-strongly-typed-or-a-weakly-typed-language.aspx
Эрик Липперт интересно пишет про слабую и сильную типизацию C#.
What? You mean, is C# a strongly typed and a weakly typed language?
Yes, C# is a strongly typed language and a weakly typed language.
Хотя по-поводу явности/неявности сложно что-либо сказать, т.к. для var и val тип указывать не нужно (неявная с опциональной явностью), а для def нужно указывать типы аргументов (полностью явная).
И не будем вспоминать про scala.Dynamic…
Так же в скала не обязательно указывать тип возвращаемого значения функцие, если она не рекурсивная.
В скале есть элементы как динамической типизации и слабой(оператор implicit позволяющий описывать неявные приведения типов)
Так же мне кажется, что данная методология все же ближе императивным языкам, в идеале функиональные языки по другому смотрят на эти вещи.
Так что если покопаться:
Статическая/динамическая:
foo.method("a")
bar ! Message("a")
Сильная/слабая:
def test(b: Bar) = ???
implicit def fooToBar(f: Foo): Bar = ???
test(new Foo)
Явная по желанию:
val f1 = new Foo
val f2: Foo = getFoo
Когда я после C++ попробовал JavaScript, меня просто дико выбешивало, что там совершенно невозможно понять, что именно надо передавать в функцию и что потом эта функция возвращает.
Сравним:
function getItemType(list,item)
или же
const std::string& getItemType(ProductsList &list,const int item)
Во первом случае надо шерстить код или документацию, а во втором можно сразу вызывать, лишь разок увидев всплывающую подсказку.
// string getItemType (ProductsList | [Product] list, int item) --
// -- returns type of product with index "item" from list of products.
function getItemType(list,item)
Кстати, некоторые IDE иногда даже покажут вам этот коммент как Кстати, некоторые IDE иногда даже покажут вам этот коммент как «всплывающую подсказку» всплывающую инлайн доку.
Что, увы, совершенно не помогает когда изучаем лог коммитов или читаем большие объемы кода — по наведению глаза на идентификатор IDE всплывающую подсказку не показывают, а наводть мышку на каждый интересный идентификатор спокойно увеличивает время ревью в несколько раз O_O.
function getItemType(productsList, itemIndex) { ... }
… и все уже не так плохо. Хотя дисциплины нужно больше на порядок, да.
>>> Языки с неявной типизацией тяготеют к write-only-коду.
Ага, некоторые просто напишут что нужно в виде отдельной функции, причем часто в глобальном пространстве и довольны, а ведь тут как нигде важна жесткая стандартизированная структура кода (конвенции, модульность, паттерны и тд), чтобы хоть как-то облегчить дальнейшую поддержку и развитие подобного «динамического» кода. Хотя бы модульность и классовый подход использовать желательно.
>>> Легкость в освоении — языки с динамической типизацией обычно очень хороши для того, чтобы начать программировать.
А еще гики любят :)
PS сам пишу на Java и JavaScript.
C#:
var foo — это какая типизация?
dynamic foo — а это какая?
;)
«Также нужно упомянуть, что многие статические языки позволяют использовать динамическую типизацию, например:
C# поддерживает псевдо-тип dynamic.
...»
В принципе эмулировать динамическую типизацию в ЯП со слабой статической типизацией достаточно просто до определенного предела.
Например если функция имеет сигнатуру
A do_something( B b, C c );
то мы не можем быть уверены в том, что будут переданы именно экземпляры B и C, а возвращен экземпляр A, т.к. на деле там могут быть наследники этих классов или даже null.Конечно в общем случае это статическая типизация, однако черты динамической так же присутствуют.
Ну а null не достаточно для того. чтобы говорить о динамичкеской типизации.
Но ведь бывают не переопределяемые методы и поля, доступ к которым не может быть переопределен броском исключения.
Все-таки null — хак над системой типов. Кстати, как показывают некоторые языки (например Haskell и Scala), хак этот совершенно не обязателен и даже вреден.
Даже том же C# ввели Nullable, который имеет нечто общее с Maybe (и Option), хоть и не является полноценным аналогом, да и поздно в C# от null избавляться.
В Java, как и в C# null — хак над системой типов.
В Scala же, например, хоть и не рекомендуется использовать null, но для совместимости с Java он есть и более того встроен в систему типов довольно забавным образом:
null является единственным экземпляром класса Null, который является наследником всех ссылочных типов (наследников AnyRef).
Таким образом ситуация blabla( null ) и blabla(new C()) не просто вызывают один и тот же вопрос, как в Java, но и действительно являются одинаковыми.
Динамическая.
А в чём троллинг?
dynamic foo — статическая типизация с выключенной статической проверкой типов (так написано в msdn)
Я не согласен с таким термином. Нельзя в термин вкладывать смысл что типов нет (как это вообще?) если на самом деле они есть (как вы сами сказали — битовые последовательности различной длины, можно давать и другие определения). И кроме того, если над ними ещё и производится проверки типов!
Смотрите сами: если вы оперируете какими-то значениями, то все такие допустимые значения принадлежат к какому-то множеству. И это множество и задаёт «тип». Поэтому я считаю сам термин «безтиповые» или «нетипизированные» некорректным.
char c = get();
if (c >= '0' && c <= '9') {
int digit = c - '0';
В большинстве языков вы никак не ограничите digit промежутком [0; 9] на уровне языка. И тип такой вам не дадут создать.
Digit D(get());
if(D.IsValid()){
int digit=D.Value();
— или как-нибудь еще более надёжно… Правда, это уже не Си. Но и на Си можно перевести, локализовав возможные некорректности в одном участке кода.
Это же dependent types, да?
Однако это устоявшийся термин, плюс, я не знаю чем бы его можно было заменить.
Ничего себе «если»! В Си вы даже malloc() без невного преобразования типа указателя вызвать не сможете.
> а в ассемблере — только когда они попали в регистры?
А в большинстве ассемблеров нет типа «указатель» (в некоторых есть, там где для указателей специальные регистры). Указатель — число нужного размера.
Про тип «указатель» в ассемблере я ничего не говорил. Но в 8086 они были и занимали целых два регистра (es:bx и т.п.) И аксиоматика чисел на них не очень работала.
memcpy
делать как душе заблагорассудится. Я прототип некой файловой системы так писал. Не будет работать только синтаксический сахар типа ++ и прочего люрекса.memcpy
, который тут же закоммитили в федорины glibc.Оп-па — поломалось что-то совершенно на первый взгляд левое (воспроизведение звука, например).
Разбор полетов показал, что ядерщики пользовались копированием памяти с перекрытием региона (кажется, потому что быстрее, чем
memmove
). А новый алгоритм копировал память по-арабски (справа налево).Ядерщики вообще тут не при чём. Функция memcpy — в glibc, а не в ядре.
Накосячили по факту вообще в адоби. Они в флешплеере для линукса копировали перекрывающие регионы (как и показал valgrind), а в документации указано, что так делать не надо и результат не определён.
Баг проявился, когда glibc, желая всё жосско приоптимизировать для какого-то очередного пентиума, начали копировать память задом наперёд. Вот тут-то неопределённость поведения и проявилась.
Линус там ниже распинается в духе, что «в таком случае, нужно было забить на оптимизацию, раз это ломает уже использующиеся популярные приложения».
Так что С еще на шаг ближе к ассемблеру в смысле типизированности…
Потому что это единственный разрешённый стандартом способ это делать. А вот преобразования типов указателей во многих случаях — нарушение strict aliasing.
Но не только там. В самый недавний раз видел его в нутрях Zend Engine, например.
А ассемблер скорее вполне себе строго типизированный, просто типы — это числа разной длины и всё, других типов нет.
А невозможность скопировать весемь байт туда где могут лежать только 4 — это не строгая типизация, это просто здравый смысл. Вместо этого есть опкод, который копирует младшие байты. Ему в теории можно было бы назначить алиас, если так хочется типового абсолютизма. Но это никому не нужно по понятным причинам.
Ну либо я сильно отстал от ассемблеров, раньше было так.
Вы тоже не можете провести точно границу. Разве граница в том, что «безтиповые» ищут по таблице?
Лексическая/синтаксическая/семантическая проверка в компиляторе ЯВУ тоже может быть реализована таблично. А в ассемблере может быть реализована и нетаблично, а алгоритмически.
Тут алгоритмически можно что-то сделать только если там есть явная зависимость, ее может и не быть. И даже если она есть, то тут первичны команды, а алгоритм под них подстраивается, а в строгой типизации наоборот, первичен алгоритм или набор правил, а команды по этим правилам формируются.
Вы не поверите, но это одна и та же команда с двумя полями, куда подставляется любой номер регистра. (Посмотрите в интеловском мануале: mov r/m16, r16.)
mov byte ptr es:[bx],1
и
mov word ptr es:[bx],1
— разные действия. (в синтаксисе могу ошибаться, но по-моему, обе команды существуют).
*(char*)es_bx=1;
и
*(short*)es_bx=1;
соответственно? По-моему, ничем. И можно задавать те же самые вопросы. Особенно если «типы указателей не считаются».
Другое дело, что пару команд
mov es:[bx],al
и
mov es:[bx],ax
мы так просто не адаптируем, у сишного указателя es_bx какой-то тип есть — и он определяет, что лежит в этой ячейке «по умолчанию», а у ассемблерного es:[bx] типа нет — он «по умолчанию» адаптируется к типу второго арумента (а при неоднозначности — ругается).
Наверное, в этом можно увидеть разницу…
char a;
short b;
А потом пишешь
a =1;
b = 1;
то компилятор, проверив тип данных находящихся в ячейке сам назначит нужную ассемблерную команду. Если же пишешь на асме сам, то никто за тебя не проверит тип ячейки и не запретит использовать «не ту» команду.
И в случае a =b; язык строгой типизации запретит, язык со слабой типизацией подберет сам нужную команду, а на асме сам программист должен будет решить что использовать, потому что, опять же, никто не запретит ему использовать al в той же команде mov, да и вобщем то хоть бы и ah, если программист сам для себя придумал конверсию.
В обоих случаях типизация данных(а не команд) лежит выше ассемблера, в компиляторе, либо в голове(блокнотике) программиста.
Собственно что определяет типизацию вообще — проверка типов. Ничто же не помешает скопировать int в uint, да хоть бы и в short. В любом из случаев это после компиляции всего лишь адрес в памяти. Но если проверка типа не прошла — исполнение будет запрещено с требованием предоставить явное преобразование.
Слабая типизация характеризуется ровно тем же, Проверкой типов. Только при невыполнении условия совпадения типов, будет автоматически подставлено неявное преобразование.
В ассемблере же проверки типов нет. Нигде, ни на одном из этапов. Тип всегда один, отличается лишь операция. Из одного и того же адреса памяти 16битный mov возмет 2 байта, а 32хбитный 4. И никто никогда не узнает, что там «однобайтовый тип» лежал. Пока код не упадет.
На примере арифметических операций, кстати еще лучше видно, что нет никаких типов, это даже было, помнится, в презентации amd — несколько каскадированных 32хбитных операций теперь схлопываются в одну, получаем выигрыш в скорости. Любую операция, хоть бы и таке же 64хбитное сложение можно было сделать и на 8ми битном процессоре. Но теперь, когда АМД внедрила новый, никем ранее не использованный 8 байтовый тип данных… хехе.
зЫ все сдаюсь, пусть будет типизированный, бг
Terms like “dynamically typed” are arguably misnomers and should probably be replaced by “dynamically checked”.
Types and Programming Languages, Benjamin C. Pierce.
Вот, Tcl тоже бестиповый с точки зрения программиста и был фактически бестиповым в реализации давным давно, пока его не начали оптимизировать. Только, в отличие от ваших примеров это высокоуровневый скриптовый язык и единственный тип в нём — строка. Даже вместо указателей на непрозрачные объекты (поток, сокет и т.д.) используется их уникальное имя (file1, sock42 и т.д.) т.е. строка.
Да, с CL всё правильно. Типизация сильная, динамическая, с опциональной явной. Вдобавок, некоторые реализации вроде SBCL при наличии деклараций типов пытаются его статически анализировать и выдавать ошибки или генерировать более оптимальный машинный код, но в первую очередь тип — это документация. Другие применения в стандарте не прописаны.
> меньшее — почему мне нельзя проверить равенство?
А если его «заменили» на такое же, но посчитанное другим способом?
Да и вообще, я бы не дал «палец на отсечение», что равенство выполнится даже если никто ничего больше не писал. Есть, например, настройки оптимизации вещественных вычислений у компилятора, которые, кстати, умеют делать «unsafe» оптимизации.
> Или если число лежит на отрезке [p,q], где p и q целые, и надо отдельно отработать случай a==q
> (при этом любое a=q-eps в этот случай не попадает).
Если у вас есть такая логика в программе с вещественными числами, то 99%, что проблема кроется в архитектуре.
> Почему я должен всех запутывать и писать a>=q
А вот > вместо >= писать как раз можно. Если вы конечно уверены, что у вас там строго больше.
Вообще, по поводу «запутывания», на знаю как вас, но меня код типа IsWithinAccuracy(a, b) совсем не запутывает.
А вот a == b в случае вещественных числе запутывает и очень даже сильно. Потому что я автоматически пытаюсь понять, что имел в виду автор, если он сравнивает «напрямую». Должны быть очень веские причины писать такое сравнение. Я сходу даже придумать не могу такой причины.
Где key — координата х, value — значение функции.
Тогда вам не придется делать преобразование (int)x, потому что вместо этого будет что-то вроде A.lower_bound(x).
А нету преобразования — нету проблемы :)
Ну а в вашей реализации я бы внутри функции не закладывался на то, что выполняется условие x <= N, выполняться должно только x <= N + epsilon (а в этом случае нужно обязательно ставить >= вместо ==). Потому как совершенно неизвестно, как был получен х.
Про hash_map я не понял. Он быстрее вектора? Или занимает меньше памяти? Или автоматически выполняет интерполяцию? В чём его выигрыш?
> Известно, что x — средняя G-компонента пикселей на фрагменте картинки. Так что она наверняка 0<=x<=255, причём значение 255 достигается, а больше — никогда.
Не наверняка. Может получиться 255.00000000000001 если у вас пиксели все по 255
А чтобы искать ближайшую или ближайшую левую точку, понадобится уже что-то вроде дерева. С поиском за O(log(N)).
Насчет 255.0000000000001 — интересно. Как это может быть? Пикселей на то, чтобы переполнить 52 бита мантиссы, у нас нет (потребовалась бы 16-терапиксельная картинка), поэтому сумма будет вычислена точно. Как деление целого числа на целое может дать неверный результат? Вы хотите сказать, что 9.0/3.0 это не обязательно 3.0?
по поводу 255. Да согласен, не получается в данном случае, но я б все равно перестраховался. В конце-концов 0;255 может резко измениться на какой-нибудь float[0;1] в другом формате
насчет hash_map: оно умеет искать не только точное соотвествие, но и наиболее близкие элементы сверху/снизу, так что тут проблемы нету.А как? Насколько я понимаю, оно работает на хэш-таблицах, а как эту фичу приделать туда я придумать не могу
Кстати, относительно проблемы Mrrl — по мне так хорошее решение, но оно обломается если у него между ближайшими точками будет разное расстояние. Впрочем, я думаю это ему не грозит.
1 — взять заведомо малое значение, но достаточное, чтобы ошибка его превысила. Например, 1е-10. Это зависит от задачи. 1е-10 — это такой середнячок, и не очень много, и не очень мало, приблизительно середина точности дабл (точность дабл что-то около 17 знаков, точно не помню).
2 — «правильный», но сложный и зачастую не нужный: считать ошибку паралелльно со всеми расчетами.
Для этого есть специальные формулы.
Подробнее — ищите мат часть в интернете
С другой стороны, правильно написанное сравнение A[n]<A[n-1] с учетом eps, таки сортирует массив, но числа в пределах eps остаются при этом перемешанными.
Сравнивать с точностью до фиксированного числа eps можно только в том случае, когда все сравниваемые числа имеют примерно одинаковый порядок.
С нулем всегда нужно сравнивать только на точное равенство и неравенство, иначе произойдет существенное сужение динамического диапазона используемых чисел.
Сравнение с учетом точности используется только при сравнении двух чисел, ни одно из которых не обязательно в точности равно нулю. При этом выбор величины параметра eps должен учитывать порядок сравниваемых чисел. Один из вариантов такой:
real abseps = (eps + 2.0*REAL_EPS) * (1.0 + max(fabs(a), fabs(b)));
Здесь eps — желаемая точность сравнения, REAL_EPS — абсолютная точность используемого вещественного типа real, abseps — та абсолютная точность, с которой необходимо сравнивать числа a и b.
Если у вас есть такая логика в программе с вещественными числами, то 99%, что проблема кроется в архитектуре.
Стандарт IEEE 754 гарантирует, что для целых чисел, сохраненных в виде вещественного числа все операции, не выводящие за пределы можества представимых целых чисел (в том числе и сравнение), выполняются точно и в полном соответствии с аксиомами целых чисел. Поэтому целые вещественные числа можно и нужно сравнивать на точное равенство и в некоторых языках присутствуют только вещественные числа.
Стандарт IEEE 754 гарантирует, что для целых чисел, сохраненных в виде вещественного числаКаких именно целых чисел? 10^300 тоже целое число, но представить точно 10^300+1 в 64 битах не получится.
Более того, в JS
Math.pow(2,63) == Math.pow(2,63)-1
(как и в Питоне (2.0**63+1) == (2.0**63)
), т.е. точности double не хватит даже для int64 (что, разумеется, очевидно из того, что их битовая ёмкость одинакова).Что-то в этом есть, не правда ли?
Вначале был нужен шаблонизатор, который в подготовленную страничку вставит полученные из хранилища данные. Для таких задач не нужна ни статическая ни явная типизация, она только усложнит написания.
Задачи в браузере тоже не были глобальными. Джаваскрипт использовался маленькими функциями-вставками не превращая страницу в полноценное приложение. Простота и краткость были очень важными критериями.
Наследственность очень тяжело побороть.
В последнее время, я стал всё чаще видеть попытки использовать статические языки (ocaml, haskell, scala) для геренерации джаваскрипт кода.
Постепенно набирают популярность Asp.Net экосистема, Go, ocaml со стороны сервера. Есть попытки сделать что-то приближенное к современному понимаю типизации, например yesod для haskell.
Процесс медленный, но в задачах, сложность которых заставляет испытывать дикий восторг от возможности автоматической проверки хоть чего-либо, языки с сильной статической типизацией обгоняют всё остальное. Сложность задач решаемых при сайтостроении плавно приближается к такому уровню.
Ну да ладно, это все равно не имеет отношения к типизации, steck уже объяснил чем была выгодна динамическая типизация в вебе.
В PHP есть элементы опциональной явной сильной типизации — type hinting. Весьма ограниченная — только аргументы функций/методов, только объектные типы или массивы, — но есть. Например,
function has_permissions(User $user, array $permissions) {}
вызовет ошибку времени исполнения при попытке вызвать её с первым аргументом не класса User (или не имплементацией интерфейса User) или их наследников, а c вторым — не массивом. Но для скаляров это не реализовано, насколько я понимаю, по причине того, что PHP всё же слаботипизированный язык и type hinting приведёт к тому, что он практически не будет выполнять своей роли (передача строки вместо целого будет преобразовывать строку в целое) или нарушит единообразие, что неявные преобразования перестанут работать в некоторых случаях, а в PHP его и так мало :). Однако автоматический вывод типов довольно сложная вещь, и даже в таком крутом языке как Haskell, он иногда не справляется. (как пример можно привести ограничение мономорфизма)Ограничение мономорфизма используется для оптимизации, а не по причине причине несправляемости.
Например, представим что у нас есть функция.
test xs = let l = length xs in (l, l)
То есть функция принимает на вход массив и выдаём массив и выдаёт пару (длина массива, длина массива)
Если есть ограничение мономорфизама, то у длины будет выведен тип
test :: [a] -> Int
и всё посчитается, как интуитивно ожидалось.
А теперь представим, что ограничения нет и используется максимально общий тип.
test :: (Num b, Num c) => [a] -> (b,c)
то есть функция принимает на вход массив и выдаёт два «числа». Два объекта, у которых есть все свойста чисел.
Пусть у нас есть помимо Int ещё представитель TraceInt, который каждое своё действие (сложение, умножение и тп) пишет в лог.
тогда, можно написать
let use1 = test [1] :: (Int, TraceInt)
Поскольку никакого преобразования из TraceInt в Int в принципе не существует, то длинну списка придётся вычислять дважды.
Это тоже понятно и логично. Проблемы начинаются когда мы пытаемся использовать тот же тип.
let use2 = test [1] :: (Int, Int)
let use3 = test [1] :: (TraceInt, TraceInt)
Компилятор не может заранее знать как будет вызываться метод в будущем, поэтому в данных примерах также вычисляет длинну списка по 2 раза. То есть в выражении
test xs = let l = length xs in (l, l)
будет больше вычислений, чем ожидается по здравому смыслу.
Итог: если не использовать ограничения мономорфизма и всегда стараться брать максимально общий тип, то возможны огромные просадки производительности.
Groovy имено такой язык. Не просто поддерживает динамику, а именно задумывался как язык одновременно со статической и динамической типизацией.
> Преимущества статической типизации…
Основное и главное (для меня) преимущество статической типизации — это поддержка IDE. Не нужно при вызове API каждый раз бегать в документацию, сверять имена и типы параметров. IDE сама показывает необходимые варианты, исходя из контекста. В итоге работа со сторонними API убыстряется в разы.
What To Know Before Debating Type Systems blog.steveklabnik.com/posts/2010-07-17-what-to-know-before-debating-type-systems
Сильная типизация выделяется тем, что язык не позволяет смешивать в выражениях различные типы и не выполняет автоматические неявные преобразования, например нельзя вычесть из строки множество.
…
Примеры:
Сильная: Java
И что, в джаве так нельзя написать?
System.out.println(1 + "a");
int — тип
String — тип
1 + «a» — выражение с различными типами
Какая разница какие там свойства у String если данный пример противоречит приведенному в статье свойству сильной типизации — «не позволяет смешивать в выражениях различные типы»
Тут оператор (не метод!) "+".
Он просто определен таким образом, что принимает с одной стороны строку, а с другой — Object. Здесь не выполняется преобразование.
Хотя да, вы можете вспомнить про boxing/unboxing.
Сроки — это часть языка, у них есть тип.
Вы не можете переопределить оператор + для других типов.
Возможность такого сложения для строк предоставляется не библиотекой, а встроенной системой типов джавы во время компиляции, и является ничем иным как неявным преобразованием типов.
Вы не можете переопределить оператор + для других типов.
Java вообще не дает доступа к операторам. Их нельзя переопределять и добавлять новые.
Любой оператор в Java — это скорее синтаксическая конструкция.
Имело бы смысл говорить о преобразовании типов, если бы был класс мест, в которых это преобразование бы выполнялось. Например если бы в метод, принимающий строку, можно было передать что угодно с неявным преобразованием к строке.
Если хотите, конкатенация в Java — синтаксический сахар над созданием StringBuilder, несколькими вызовами append и затем toString.
И здесь не будет неявного преобразования к строке. Здесь будет вызов append(Object obj) (у вас append(int i)) у StringBuilder.
1 + «a» + someObject — это StringBuilder().append(1).append(«a»).append(someObject).toString().
Внутри, в генерируем коде, может быть все что угодно. Например, следуя вашей логике возражений, в джаве в каком то смысле вообще нет никаких типов, классов и методов, а есть интерпретатор и куски бинарных данных которые интерпретируются.
Неявное преобразование типов это вообще-то и есть синтаксический сахар.
То что у джавы оно допускается не везде, не означает что его там нет.
Но на самом деле мое возражение связано с некорректностью утверждения что при сильной типизации недопускаются выражения с разными типами. Оно по сути бессмысленно, что подтвержается моим примером.
При чем тут System.out.println и java.io.PrintStream.println, если результат выражения {1 + «a»} — String?
Судя по всему amosk акцентирует внимание именно на операторе "+" и его действии.
Если рассматривать "+" как математическую операцию, то это операция вида (A, A) → A. То есть переводящая пару объектов из одного множества в объект из того же множества.
При таком подходе эта операция не определена даже для пары (0, 0.0), так как это объекты разных множеств (разных типов) и требуется неявное преобразование.
То же самое и в случае (_, String) -> String. Первый аргумент, при математическом рассмотрении, неявно преобразуется к String.
Выше я пытался аргументировать к тому, что "+" в Java не соответствует своему математическому тезке, а представляет отдельную строго определенную операцию.
Ваши же аргументы я понять не могу. Мне даже не понятно часть с апеллированием к примитивным типам в Java, так как они в языке, позиционирующемся как чистый ООП, представляют из себя отход от основной идеологии в целях быстродействия.
Язык с динамической неявной типизацией:
var x = 1;
if (something)
x = 'Hello!';
else
x = Complex(1, 3);
Язык с динамической явной типзацией
var x: Integer = 1;
if (something)
x: String = 'Hello!';
else
x: Complex = Complex(1, 3);
Таким образом можно перенести в динамическую типизацию некоторые преимущества статической (например
x: String = 3;
компилироваться/интерпретироваться не будет), т.е. программист должен будет явно указывать когда хочет запихнуть в переменную значение другого типа, что можно использовать и для проверки ошибок, и для оптимизации.А вообще, все это — proof of concept.
Кстати такая ситуация может наблюдаться в rust. Правда, там не переменные, а биндинги, поэтому под одно и то же имя может соответствовать разному типу.
Вполне допустимы ситуации типа:
let a: &str = "42";
// explicit type in let
let a: i32 = a.parse().unwrap();
// explicit type in parse call
let b = "18".parse::<i32>().unwrap();
// compile-time error, type cannot be inferred
// let c = "18".parse().unwrap();
println!("{}, {}", a + 1, b - 1);
Примечание: я намерено использовал некаррированную функцию, а также намерено записал частную сигнатуру вместо более общей add :: (Num a) => a -> a -> a*, т.к. хотел показать идею, без объяснения синтаксиса Haskell'а.
Если вы используете кортеж, то сигнатура функции будет:
add :: (Num a) => (a, a) -> a
В статье хорошо изложены концепции типизации и хорошо видны противоречия возникающие в типизированных языках. Ясно, что необходима и статическая и динамическая и сильная и слабая типизация. И различные языки по разному находят этот баланс. В основном за счет расширения типов в языке или встроенных библиотеках. При этом совершенно очевидно что базовых типов недостаточно и появляются типы вроде даты и IP адресов. В целом все это мне напоминает ситуацию, когда сначала строят стену, а затем ее героически преодолевают. И очень горды там, как это удачно удалось осуществить. Популярный миф о том что типизация помогает создавать более надежные программы, не более чем миф. Все обстоит с точностью до наоборот. Яркий пример тому бестиповой язык MUMPS. Отсутствие в нем типов решает все проблемы типизации. Они в MUMPS просто отсутствуют. В результате имеется язык который можно освоить за один вечер. При этом простота языка никак не сказалась на его возможностях. Типизация это зло. Конечно картина с типизацией неоднозначна. Типы тесно связаны со структурой данных. Декларирование данных позволяет так же описать структуру данных, например массив. Встроенные библиотеки добавляют другие структуры стеки, очереди, хеш таблицы. Безтиповой язык должен решать и эту проблему. И MUMPS ее блестяще решил. Любая переменная может являться деревом произвольной структуры, в том числе и простой переменной. А уж из дерева сделать массив, стек или хеш таблицу ничего не стоит. Все типы ненужны. Транслятор сам разбирается с типом переменной. Операции однозначно задают тип операндов и результата. Такие понятия как типы переменных можно выбросить из головы и заняться поставленной задачей. Кстати рассматриваемый пример на MUMPS будет выглядеть так:
find(.mass,element) set node=»»
for i=0:1 set node=$O(mass(node)) q:node=»»!(node=element)
q i
При этом переменная mass может моделировать и массив и стек и хеш таблицу.
Популярный миф о том что типизация помогает создавать более надежные программы, не более чем миф. Все обстоит с точностью до наоборот. Яркий пример тому бестиповой язык MUMPS.
А где примеры надежности?
Критики MUMPS прямо называют эту технологию устаревшей[3] и указывают на такие недостатки MUMPS как[3][4]:
- отсутствие типизации (все данные хранятся как строки);
- низкий уровень абстракции;
- нечитабельность синтаксиса, особенно при использовании сокращений.
Язык MUMPS критики называют провоцирующим ошибки, поскольку[3][4]:
- отсутствует обязательное объявление (декларирование) переменных;
- не поддерживаются привычные приоритеты арифметических операций (например, выражение 2+3×10 даёт в MUMPS значение 50);
- лишний пробел или разрыв строки может совершенно изменить смысл синтаксической конструкции;
- ключевые слова языка не зарезервированы и могут широко использоваться в качестве идентификаторов.
А что Википедия истина в последней инстанции? А для обвинений надо быть в курсе.
>Критики MUMPS прямо называют эту технологию устаревшей[3] и указывают на такие >недостатки MUMPS как[3][4]:
>отсутствие типизации (все данные хранятся как строки);
Это достоинство а не недостаток. А как что хранится неизвестно и даже безразлично. Программиста это никак не касается. Я думаю, что различные реализации MUMPS данные хранят по разному.
>низкий уровень абстракции;
Совершенно бездоказательное утверждение. На чем основан такой вывод? Абстракция от типов точно есть. Ни разу не встречал анализа уровня абстракции языка MUMPS.
>нечитабельность синтаксиса, особенно при использовании сокращений.
Не сокращай. Но даже с сокращениями вполне читабельный синтаксис. Ничем принципиально не отличающийся от других языков программирования.
>Язык MUMPS критики называют провоцирующим ошибки, поскольку[3][4]:
>отсутствует обязательное объявление (декларирование) переменных;
Ну это никак не провоцирует ошибки. Все перевернуто с ног на голову. Само декларирование порождает массу понятий и правил их использования. И как следствие именно оно и провоцирует ошибки.
>не поддерживаются привычные приоритеты арифметических операций (например, >выражение 2+3×10 даёт в MUMPS значение 50);
Да именно так. Непривычно не значит хуже. Приоритеты операций отсутствуют. Это очень простое правило которым легко пользоваться и трудно допустить ошибку. А вы попробуйте запомнить приоритеты всех операций в Си. Голова идет кругом. Я все равно вынужден либо расставлять скобки, либо каждый раз лезть в справочник. Приоритеты в Си не везде очевидны.
>лишний пробел или разрыв строки может совершенно изменить смысл синтаксической конструкции;
ключевые слова языка не зарезервированы и могут широко использоваться в качестве идентификаторов.
Ну и что? Во многих языках пробел завязан в синтаксисе. И как это влияет на надежность программ? MUMS на порядки надежней любого другого языка. Это простой, логичный, компактный и не противоречивый язык. В нем нет подводных камней. Описание всего языка вместе с примерами занимало 20 страниц книжки половинного формата A4. И этого вполне достаточно. При программировании в основном встречается 2 ошибки деление на ноль и неопределенный индекс. Так как нет декларирования и развитая структура переменных, программы просты, компактны и надежны. Я с 1984года программирую на MUMPS и считаю, что то что написано в Википедии поверхностный взгляд некомпетентного специалиста.
А Си, Паскаль, Си++ не устаревшие? А новые, что принципиально лучше старых? Чем это? Новые языки делаются очень просто. Берем Си и добавляем пробел в конце или еще что нибудь. Принципиально нового, кроме MUMPS ничего нет. Только в MUMPS управление данными полностью взял на себя язык. Унифицировано обращение к данным в памяти и на внешних носителях. В MUMPS работать с встроенной базой данных так же легко как и с памятью. То что я на MUMPS писал месяц, потом на Delphi переносил 2 года при этом пришлось купить и прочитать целый стеллаж книг. Программирование не на MUMPS это убийство времени, ресурсов и сил.
Ruby — Динамическая | Сильная | Неявная
Не соглашусь про сильную (если определять её как отсутствие автоматического приведения вообще). В ruby есть coercion, который позволяет автоматически приводить типы (расширять по необходимости, например, целые в числа с плавающей точкой).
Аналогично в случае java возможно автоматическое приведение типа, если не происходит потери данных (widening primititve conversion byte -> short -> int -> long -> float -> double
, char -> int -> long -> float -> double
) или если тип переменной/параметра является суперклассом приводимого объекта. Плюс есть numeric promotions аналогичные механизму coercion в ruby, которые автоматически расширяют типы в арифметических выражениях при необходимости. Более подробно см. 5 главу JLS.
Это даже не вспоминая про boxing/unboxing, которые тоже являются неявной конверсией, которая может приводить к OutOfMemoryError
или NullPointerException
.
Привет из 2019-го!
Из новостей — появился TypeScript.
НО! с 2017 года он сильно скаканул и курс на типизацию сильно вырос. Хотя он еще разрешает работать в такой манере — во многих компаниях (во всех, где я работал) курс взят на написание более строго кода.
По поводу неявности типов в недавнем релизе PHP 7.4:
<?php
declare(strict_types=1);
class User
{
private Id $id;
private string $name;
public function rename(string $name): User
{
$this->name = $name;
return clone $this;
};
}
Такой код, очевидно каждому, не назовешь неявно типизированным :)
За строгость тут отвечает declare(strict_types=1), которая не даст приводить скаляры неявным образом там, где указан тип
Должен быть по всей видимости auto a = 5u;
// Автоматический вывод типа
unsigned int a = 5;
auto b = a + 3;
Ликбез по типизации в языках программирования