Pull to refresh

Comments 180

Спасибо, был рад стараться. Если тема востребована, готов написать ликбез по парадигмам программирования в похожем стиле.
Буду рад прочитать.
Если можно, то пишите глубже и хардкорнее :) Про типизацию ещё можно многое нарассказывать.
И спасибо за статью!
На здоровье! Нарассказывать действительно можно много: и про типизацию Хиндли-Милнера, и про автоматический вывод типов, и про безопасную типизацию (как раз что-то среднее между слабой и сильной), ну и конечно можно привести больше примеров кода — показать, где тот или иной вид типизации помогает писать код, а где — мешает.

Плюс, можно поподробнее описать как те или иные виды типизации реализованы различных языках программирования.

Однако статья итак вышла длинной, а парадигмы программирования — еще более глубокая тема.

В любом случае я хотел донести до читателя только те знания, которые с помогут в программировании, и что главное, будут интересны каждому. И в следующей статье я также попытаюсь извлечь из большой темы самую полезную и интересную информацию.
Как писалось ниже, типизация хорошо описана в книге Types and Programming Languages, Benjamin C. Pierce.
Книга переведена на русский язык. Можно нагуглить по названию «Типы в языках программирования».
Спасибо, она есть у меня :)
Рекомендую почитать вот это еще по типизации.

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.
Жаль, что вы Scala обошли стороной. К какому типу можно отнести ее?
Ух. Как раз сейчас учу этот дивный язык. В целом можно сказать, что типизация в нем статическая, строгая, неявная.

Хотя по-поводу явности/неявности сложно что-либо сказать, т.к. для var и val тип указывать не нужно (неявная с опциональной явностью), а для def нужно указывать типы аргументов (полностью явная).
Про то, что она сильная, тоже не все так однозначно. При желании можно наплодить неявных преобразований.
И не будем вспоминать про scala.Dynamic…
Def от val отличается тем, что в val записывается сразу значение, а def определяется по нужде. Так в скала val x = square(2) сразу вычислится и будет равнятся 4 то def x = square(2) будет вычислятся каждый раз при вызове.
Так же в скала не обязательно указывать тип возвращаемого значения функцие, если она не рекурсивная.

В скале есть элементы как динамической типизации и слабой(оператор implicit позволяющий описывать неявные приведения типов)

Так же мне кажется, что данная методология все же ближе императивным языкам, в идеале функиональные языки по другому смотрят на эти вещи.
Динамической типизации в скале нет, есть вывод типов на этапе компиляции, позволяющий их иногда не указывать явно.
Это «слабая» типизация в терминах автора статьи, но никак не динамическая :-)
Ну есть еще lazy val, который вычислится по нужде один раз.
Собственно, явная она ровно настолько, чтобы быть строгой. В def'е КМК компилятор не может вывести типы аргументов, а потому они обязательны.
Кстати, забавный вопрос: если учесть, что типизированные акторы почти не используются, а при написании кода с использованием акторов он может почти полностью состоять из отправки сообщений (вместо вызовов методов), то не привносит ли это в scala элемент динамической типизации?

Так что если покопаться:
Статическая/динамическая:
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
Языки с неявной типизацией тяготеют к write-only-коду. Писать на них легко, но вот чтение кода потом требует на порядок больше усилий.
Когда я после 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) { ... }

… и все уже не так плохо. Хотя дисциплины нужно больше на порядок, да.
По приведенному примеру помогли комментарии к методу, в стиле API дока.

>>> Языки с неявной типизацией тяготеют к write-only-коду.
Ага, некоторые просто напишут что нужно в виде отдельной функции, причем часто в глобальном пространстве и довольны, а ведь тут как нигде важна жесткая стандартизированная структура кода (конвенции, модульность, паттерны и тд), чтобы хоть как-то облегчить дальнейшую поддержку и развитие подобного «динамического» кода. Хотя бы модульность и классовый подход использовать желательно.
Дело привычки. Меня это так же бесило, когда я с C# на Ruby переходил. Потом привык и теперь, уверен, меня будет бесить необходимость указывать типы. :)
Это дело личных предпочтений, мне вот приходится и на джаваскрипт пописывать и на питоне, так вот мне лично кажется, что лучше указывать типы — понятней код и меньше шанс накосячить передав не тот тип.
В языках Haskell и Scala в REPL можно прсмотреть выведенный компилятором тип. Помогает, когда разбираешься с чужей или забытой своей программой.
Автоматический рефрактор (в том числе иерархия всех классов в одни-два клика, иерархия вызовов метода и тд) не является преимуществом статической типизации? Только не нужно утверждать что статический анализ кода позволит сделать тоже самое для динамического языка программирования (в JS можно всяких замыканий наворотить что анализатору будет не легко это разбирать).

>>> Легкость в освоении — языки с динамической типизацией обычно очень хороши для того, чтобы начать программировать.
А еще гики любят :)

PS сам пишу на Java и JavaScript.
«Автоматический рефакторинг» конечно имелся ввиду.
Троллинг мод он:

C#:
var foo — это какая типизация?
dynamic foo — а это какая?

;)
Конечно правильное замечание, но я учел многие исключения из классического разделения.

«Также нужно упомянуть, что многие статические языки позволяют использовать динамическую типизацию, например:

C# поддерживает псевдо-тип dynamic.
...»
Да и С++ имеет всякие типы вроде boost::any, boost::variant и QVariant, быть может когда нибудь такая штука и в стандарт попадет.
В принципе эмулировать динамическую типизацию в ЯП со слабой статической типизацией достаточно просто до определенного предела.
Да и про ООП языки вроде Java и C# нельзя сказать, что они статически типизированы.

Например если функция имеет сигнатуру A do_something( B b, C c ); то мы не можем быть уверены в том, что будут переданы именно экземпляры B и C, а возвращен экземпляр A, т.к. на деле там могут быть наследники этих классов или даже null.

Конечно в общем случае это статическая типизация, однако черты динамической так же присутствуют.
Все-таки по общепринятой терминологии наследование — это отношение «является». Так что экзепляр наследника B является экземпляром B.

Ну а null не достаточно для того. чтобы говорить о динамичкеской типизации.
Ну и null на этапе компиляции тоже имеет соответствующий тип, очевидно.
C null все сложно. Можно, конечно, говорить о том, что null является экземпляром класса со всеми методам, переопределенными так, что они бросают NRE.
Но ведь бывают не переопределяемые методы и поля, доступ к которым не может быть переопределен броском исключения.

Все-таки null — хак над системой типов. Кстати, как показывают некоторые языки (например Haskell и Scala), хак этот совершенно не обязателен и даже вреден.

Даже том же C# ввели Nullable, который имеет нечто общее с Maybe (и Option), хоть и не является полноценным аналогом, да и поздно в C# от null избавляться.
Ну, допустим, во многих случаях при связывании на этапе компиляции нереально понять какой метод вязать при неуказании конкретного типа для null. Пусть даже та же Java, например, как быть компилятору в случае двух методов: blabla( A a ) и blabla( B b ) и вызове: blabla( null )?
Допустим A и B — интерфейсы. Тогда если С реализует оба интерфейса, то мы получим ровно тот же вопрос в случае blabla(new C()).

В Java, как и в C# null — хак над системой типов.
В Scala же, например, хоть и не рекомендуется использовать null, но для совместимости с Java он есть и более того встроен в систему типов довольно забавным образом:
null является единственным экземпляром класса Null, который является наследником всех ссылочных типов (наследников AnyRef).
Таким образом ситуация blabla( null ) и blabla(new C()) не просто вызывают один и тот же вопрос, как в Java, но и действительно являются одинаковыми.
В данном случае разумеется. Но ведь при этом выражение new C() обладает типом? :) Плюс и там и там этот вопрос можно решить явным указанием типа, не так ли? (A)new C() или (A)null.
Так я изначально утверждал, что ситуация двоякая:
С одной стороны null, якобы, является экземпляром B, а с другой нарушает не только принцип подстановки, но и правила. распространяющиеся на все другие экземпляры (невозможность броска исключения при обращении к полю или не переопределяемому методу).
Статическая.
Динамическая.

А в чём троллинг?
var foo — статическая типизация с ограниченным автоматическим выводом типов
dynamic foo — статическая типизация с выключенной статической проверкой типов (так написано в msdn)
Видимо, автор решил взять для примера только зрелые языки. Всё-таки, как бы мы ни любили D, он до сих пор не устаканился, да и компилятор всё ещё решето.
Наконец-то есть статья, которой можно всем тыкать в спорах про типизацию :) Спасибо!
> нетипизированные (бестиповые).

Я не согласен с таким термином. Нельзя в термин вкладывать смысл что типов нет (как это вообще?) если на самом деле они есть (как вы сами сказали — битовые последовательности различной длины, можно давать и другие определения). И кроме того, если над ними ещё и производится проверки типов!

Смотрите сами: если вы оперируете какими-то значениями, то все такие допустимые значения принадлежат к какому-то множеству. И это множество и задаёт «тип». Поэтому я считаю сам термин «безтиповые» или «нетипизированные» некорректным.
При необходимости можно и с С работать как с «нетипизированным» языком: все объекты захватывать с помощью malloc, описывать как char* (или int*) и завести там свою структуру «типов», подходящую конкретной задаче. И в функциях самостоятельно разбираться с семантикой каждого конкретного объекта. Базовые типы C останутся только для общения с процессором (как в ассемблере).
И как это противоречит тому, что я сказал? Вы же будете использовать тип int? Будете. А ваша собственная структура «типов» будет только у программиста в голове.
Тип int я буду использовать в той же мере, в какой использую регистр eax в ассемблере. Как я и сказал, для общения с процессором. И моя структура «типов» действительно будет только у программиста в голове, а никак не в языке — множество значений и операции над объектами существовать будут, но несколько выше, чем средства типизации, предоставляемые языком. В программе-то типы будут. Но это не те типы, что предусмотрены языком.
Во многих (я бы даже сказал «во всех», но для этого мне нужно проверить все) реальных программах система типов в какой-то мере в голове у программиста, потому что ни встроенные типы, ни средства создания типов не могут точно смоделировать предметную область. Вот например:

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();

— или как-нибудь еще более надёжно… Правда, это уже не Си. Но и на Си можно перевести, локализовав возможные некорректности в одном участке кода.
>> ограничить digit промежутком [0; 9] на уровне языка. И тип такой… создать.
Это же dependent types, да?
В каких-то языках это было. Не то в Паскале, не то в PL… давно было, не помню.
В Паскале было, называлось range types.
До сих пор скучаю по этой фиче в «мейнстримовых» языках — безмерно помогает самодокументируемости кода.
Нет, диапазон же известен статически.
Согласен, бестиповый язык — не совсем удачное определение для ассемблера или форта.

Однако это устоявшийся термин, плюс, я не знаю чем бы его можно было заменить.
Понимаете, я не вижу даже где вы проводите черту. В тех же ассемблерах, например, из-за ограничений системы команд часто даже побитовое преобразование целого числа в число с плавающей точкой выполняется или отдельной командой или через память или ещё каким-то специальным образом. В данном отношении Си не менее «бестиповый», но тем не менее, вы его не относите к бестиповым.
Может быть, отличие в том, что в Си целые и вещественные числа различаются, когда они находятся в памяти (если не пользоваться преобразованиями типов указателей), а в ассемблере — только когда они попали в регистры?
> если не пользоваться преобразованиями типов указателей

Ничего себе «если»! В Си вы даже malloc() без невного преобразования типа указателя вызвать не сможете.

> а в ассемблере — только когда они попали в регистры?

А в большинстве ассемблеров нет типа «указатель» (в некоторых есть, там где для указателей специальные регистры). Указатель — число нужного размера.
Вызвать malloc я смогу. Вот воспользоваться его результатом — не очень. Оно и понятно, в памяти, захваченной malloc'ом пока еще нет объектов какого-то определенного типа. Её надо сначала разметить (хотя бы преобразовав указатель к нужному типу). Тогда она перейдет из «никакого» типа в определенный…

Про тип «указатель» в ассемблере я ничего не говорил. Но в 8086 они были и занимали целых два регистра (es:bx и т.п.) И аксиоматика чисел на них не очень работала.
И воспользоваться результатом запросто. Вы туда сможете memcpy делать как душе заблагорассудится. Я прототип некой файловой системы так писал. Не будет работать только синтаксический сахар типа ++ и прочего люрекса.
Что важно, memcpy не волнует, какие там типы. Копирует и всё.
Пару лет назад была смешная история. Кто-то нашел более быстрый алгоритм для memcpy, который тут же закоммитили в федорины glibc.
Оп-па — поломалось что-то совершенно на первый взгляд левое (воспроизведение звука, например).
Разбор полетов показал, что ядерщики пользовались копированием памяти с перекрытием региона (кажется, потому что быстрее, чем memmove). А новый алгоритм копировал память по-арабски (справа налево).
:) вы невнимательно прочитали пост Линуса

Ядерщики вообще тут не при чём. Функция memcpy — в glibc, а не в ядре.

Накосячили по факту вообще в адоби. Они в флешплеере для линукса копировали перекрывающие регионы (как и показал valgrind), а в документации указано, что так делать не надо и результат не определён.

Баг проявился, когда glibc, желая всё жосско приоптимизировать для какого-то очередного пентиума, начали копировать память задом наперёд. Вот тут-то неопределённость поведения и проявилась.

Линус там ниже распинается в духе, что «в таком случае, нужно было забить на оптимизацию, раз это ломает уже использующиеся популярные приложения».
Между прочим, в C есть совершенно легальное средство работать с одной и той же памятью и как с int и как с double без преобразования указателей. Называется union. Правда, в коде он практически не встречается (или мне не повезло его встретить), но компиляторы его до сих пор поддерживают.
Так что С еще на шаг ближе к ассемблеру в смысле типизированности…
> Между прочим, в C есть совершенно легальное средство работать с одной и той же памятью и как с int и как с double без преобразования указателей. Называется union. [...] но компиляторы его до сих пор поддерживают.

Потому что это единственный разрешённый стандартом способ это делать. А вот преобразования типов указателей во многих случаях — нарушение strict aliasing.
Регулярно вижу union в коде, работающем с форматами файлов, потому что там это естественное и прозрачное представление «неоднозначного» формата.

Но не только там. В самый недавний раз видел его в нутрях Zend Engine, например.

Ещё классика — в сетевых протоколах.

Поддержу, типы есть всегда. Просто иногда у языка есть, например, ровно один тип значений. Пример: MUMPS/M, там всё — строка.
А ассемблер скорее вполне себе строго типизированный, просто типы — это числа разной длины и всё, других типов нет.
Ну да, возможно правильнее сказать однотиповый, нежели безтиповый.
А невозможность скопировать весемь байт туда где могут лежать только 4 — это не строгая типизация, это просто здравый смысл. Вместо этого есть опкод, который копирует младшие байты. Ему в теории можно было бы назначить алиас, если так хочется типового абсолютизма. Но это никому не нужно по понятным причинам.
В ассемблере нет проверки типов. Как и собственно типов. Есть фиксированный набор команд, который попросту включает в себя не все варианты. Все что делает ассемблер — ищет по таблице. Если не может скопировать из одного регистра в другой, то это значит у процессора нет такого опкода, а не то, что где то там лексическая проверка выдала запрет.
Ну либо я сильно отстал от ассемблеров, раньше было так.
> Все что делает ассемблер — ищет по таблице.

Вы тоже не можете провести точно границу. Разве граница в том, что «безтиповые» ищут по таблице?

Лексическая/синтаксическая/семантическая проверка в компиляторе ЯВУ тоже может быть реализована таблично. А в ассемблере может быть реализована и нетаблично, а алгоритмически.
Граница в том, что mov ax, bx и mov bx, ax это две разные команды, а не одна команда с двумя аргументами, куда что-то можно подставить, а что-то нет.
Тут алгоритмически можно что-то сделать только если там есть явная зависимость, ее может и не быть. И даже если она есть, то тут первичны команды, а алгоритм под них подстраивается, а в строгой типизации наоборот, первичен алгоритм или набор правил, а команды по этим правилам формируются.
> Граница в том, что mov ax, bx и mov bx, ax это две разные команды, а не одна команда с двумя аргументами, куда что-то можно подставить, а что-то нет.

Вы не поверите, но это одна и та же команда с двумя полями, куда подставляется любой номер регистра. (Посмотрите в интеловском мануале: mov r/m16, r16.)
При этом mov ax,bx и mov bx,es — остаются разными командами… А написать mov es,ds вообще нельзя.
Потому что это mov r/m16, Sreg. А mov Sreg, Sreg вообще нет. Видите, даже тут выходит что-то похожее на типы.
Я-то вижу. Мне dword ptr хватает, чтобы увидеть, что базовые типы есть. А вот с пользовательскими — полная свобода, делай что хочешь, лишь бы не побили.
Типы указателей не считаются. Речь о типах данных.
Ага.
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, если программист сам для себя придумал конверсию.
В обоих случаях типизация данных(а не команд) лежит выше ассемблера, в компиляторе, либо в голове(блокнотике) программиста.
С точки зрения процессора это все равно разные команды. Говоря языком Си, это mov_ax_bx(); а не mov(ax,bx);

Собственно что определяет типизацию вообще — проверка типов. Ничто же не помешает скопировать int в uint, да хоть бы и в short. В любом из случаев это после компиляции всего лишь адрес в памяти. Но если проверка типа не прошла — исполнение будет запрещено с требованием предоставить явное преобразование.
Слабая типизация характеризуется ровно тем же, Проверкой типов. Только при невыполнении условия совпадения типов, будет автоматически подставлено неявное преобразование.

В ассемблере же проверки типов нет. Нигде, ни на одном из этапов. Тип всегда один, отличается лишь операция. Из одного и того же адреса памяти 16битный mov возмет 2 байта, а 32хбитный 4. И никто никогда не узнает, что там «однобайтовый тип» лежал. Пока код не упадет.

На примере арифметических операций, кстати еще лучше видно, что нет никаких типов, это даже было, помнится, в презентации amd — несколько каскадированных 32хбитных операций теперь схлопываются в одну, получаем выигрыш в скорости. Любую операция, хоть бы и таке же 64хбитное сложение можно было сделать и на 8ми битном процессоре. Но теперь, когда АМД внедрила новый, никем ранее не использованный 8 байтовый тип данных… хехе.

зЫ все сдаюсь, пусть будет типизированный, бг
А можно пояснить, почему у С++ полусильная типизация? И относится ли сильная/слабая типизация к ООП в том числе или только к базовым типам?
В C++ есть не-explicit конструкторы, операторы приведения типа, а ещё он сам с удовольствием принимает за bool что угодно int'ы и double, не равные нулю.
В чистом Си — слабая типизация, в С++ же была взята его база и были изменены некоторые правила, из-за чего появились как черты как сильной так и слабой типизации.
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 при наличии деклараций типов пытаются его статически анализировать и выдавать ошибки или генерировать более оптимальный машинный код, но в первую очередь тип — это документация. Другие применения в стандарте не прописаны.
Да сколько уже можно — в Tcl ЕСТЬ И ДРУГИЕ ТИПЫ КРОМЕ STRING — посмотрите сорцы интерпретатора tclAssembly.c tclCmdIL.c
Да, есть. Я прекрасно об этом знаю, т.к. писал для него пару новых типов. Но при разработке на Tcl о типах можно смело забыть, пока не окажется, что ваш продукт тормозит чуть больше чем хотелось бы.
Уберите, пожалуйста, сравнения вещественных чисел в стиле а == б. Нельзя их так сравнивать…
Почему нельзя? Конечно, если происхождение значений независимо, они, с большой вероятностью, не совпадут. Но если я положил в a какое-то большое число, а потом решил проверить, осталось ли оно там, или его заменили на меньшее — почему мне нельзя проверить равенство? Или если число лежит на отрезке [p,q], где p и q целые, и надо отдельно отработать случай a==q (при этом любое a=q-eps в этот случай не попадает). Почему я должен всех запутывать и писать a>=q?
> Но если я положил в a какое-то большое число, а потом решил проверить, осталось ли оно там, или его заменили на
> меньшее — почему мне нельзя проверить равенство?
А если его «заменили» на такое же, но посчитанное другим способом?
Да и вообще, я бы не дал «палец на отсечение», что равенство выполнится даже если никто ничего больше не писал. Есть, например, настройки оптимизации вещественных вычислений у компилятора, которые, кстати, умеют делать «unsafe» оптимизации.

> Или если число лежит на отрезке [p,q], где p и q целые, и надо отдельно отработать случай a==q
> (при этом любое a=q-eps в этот случай не попадает).
Если у вас есть такая логика в программе с вещественными числами, то 99%, что проблема кроется в архитектуре.

> Почему я должен всех запутывать и писать a>=q
А вот > вместо >= писать как раз можно. Если вы конечно уверены, что у вас там строго больше.

Вообще, по поводу «запутывания», на знаю как вас, но меня код типа IsWithinAccuracy(a, b) совсем не запутывает.
А вот a == b в случае вещественных числе запутывает и очень даже сильно. Потому что я автоматически пытаюсь понять, что имел в виду автор, если он сравнивает «напрямую». Должны быть очень веские причины писать такое сравнение. Я сходу даже придумать не могу такой причины.
Задача, о которой идет речь — представление функции кусочно-линейной интерполяцией. Функция задана на отрезке от 0 до N, гарантируется, что аргумент — вещественное число — число лежит в этом диапазоне, при этом шансы, что оно равно N, сильно ненулевые. Если значение x==N, я возвращаю последний элемент массива, в остальных случаях беру n=(int)x; y=x-n и возращаю A[n]*(1-y)+A[n+1]*y. Где ошибка в архитектуре?
Вместо вектора A должен быть hash_map<double, double>
Где key — координата х, value — значение функции.
Тогда вам не придется делать преобразование (int)x, потому что вместо этого будет что-то вроде A.lower_bound(x).
А нету преобразования — нету проблемы :)

Ну а в вашей реализации я бы внутри функции не закладывался на то, что выполняется условие x <= N, выполняться должно только x <= N + epsilon (а в этом случае нужно обязательно ставить >= вместо ==). Потому как совершенно неизвестно, как был получен х.
Известно, что x — средняя G-компонента пикселей на фрагменте картинки. Так что она наверняка 0<=x<=255, причём значение 255 достигается, а больше — никогда. Но для безопасности действительно лучше написать x>=N, согласен. (а еще лучше — n>=N).
Про hash_map я не понял. Он быстрее вектора? Или занимает меньше памяти? Или автоматически выполняет интерполяцию? В чём его выигрыш?
Оно не быстрее вектора, отличается по скорости на константу, и предназначено для хранения пар ключ-значение, быстрого добавления новых и быстрого (О(1)) поиска пары по ключу. Плюс его в том, что убивается связь индекс == (int)x. Индекс в хеш мапе можно сделать даблом, что позволяет хранить функции не только на промежутке 0 до N, а вообще на любом, да еще и бить их на разные куски (у вас между «реперными» точками всегда расстояние в 1.0). Памяти жрет раза в 2 больше (нужно ключ хранить). В общем выигрыш архитектурный в первую очередь. Ну, и еще потенциальная расширяемость.

> Известно, что x — средняя G-компонента пикселей на фрагменте картинки. Так что она наверняка 0<=x<=255, причём значение 255 достигается, а больше — никогда.
Не наверняка. Может получиться 255.00000000000001 если у вас пиксели все по 255
Интересно, как вы вообще собираетесь использовать double в качестве ключа hash-map, если нет точного сравнения? Неужели hash-функция у вас работает с точностью до eps? А иначе вы значения никогда не найдете, потому что ключ будет каждый раз слегка другой.
А чтобы искать ближайшую или ближайшую левую точку, понадобится уже что-то вроде дерева. С поиском за O(log(N)).
Насчет 255.0000000000001 — интересно. Как это может быть? Пикселей на то, чтобы переполнить 52 бита мантиссы, у нас нет (потребовалась бы 16-терапиксельная картинка), поэтому сумма будет вычислена точно. Как деление целого числа на целое может дать неверный результат? Вы хотите сказать, что 9.0/3.0 это не обязательно 3.0?
насчет hash_map: оно умеет искать не только точное соотвествие, но и наиболее близкие элементы сверху/снизу, так что тут проблемы нету.

по поводу 255. Да согласен, не получается в данном случае, но я б все равно перестраховался. В конце-концов 0;255 может резко измениться на какой-нибудь float[0;1] в другом формате
Сколько времени занимает поиск близких элементов? Тоже O(1)?
насчет hash_map: оно умеет искать не только точное соотвествие, но и наиболее близкие элементы сверху/снизу, так что тут проблемы нету.
А как? Насколько я понимаю, оно работает на хэш-таблицах, а как эту фичу приделать туда я придумать не могу

Кстати, относительно проблемы Mrrl — по мне так хорошее решение, но оно обломается если у него между ближайшими точками будет разное расстояние. Впрочем, я думаю это ему не грозит.
И еще очень любопытно, как написать функцию сравнения вещественных чисел для сортировки. Если под равными числами понимать числа, равные с заданной точностью, то нарушится аксиоматика порядка (будет возможна ситуация a=b, b=c, a<c), и результат станет непредсказуемым.
Так для сортировки достаточно отношения строгого порядка, т.е. оператора < (std::sort требует именного строгого порядка, иначе может случиться всякое). Ну а если получится. что a < b < c, хотя в настоящей математике получилось бы b < a = c, то, видимо, b столь ничтожно отличается от a и c, что нужно использовать более точные вычисления (какой-нибудь класс рациональных чисел, например), если мы хотим их различать.
Ну, а если у нас массив A[n]=-n*eps/2, а сортировка вздумает проверить, надо ли его сортировать вообще? Условие A[n]<A[n-1] (при сравнении с точностью до eps) не выполняется ни разу (соседние элементы в этом массиве «равны»), поэтому функция имеет право ничего не делать. А различие между первым и последним элементом может быть совсем не ничтожным, причем не в ту сторону…
Если Вы работаете на пределе точности какого-нибудь double и используете тот самый double, то Вы делаете что-то не так.
Кто говорит про предел точности? И вообще, как именно вы предлагаете проверять равенство вещественных чисел? Откуда брать порог точности?
Есть 2 пути:
1 — взять заведомо малое значение, но достаточное, чтобы ошибка его превысила. Например, 1е-10. Это зависит от задачи. 1е-10 — это такой середнячок, и не очень много, и не очень мало, приблизительно середина точности дабл (точность дабл что-то около 17 знаков, точно не помню).
2 — «правильный», но сложный и зачастую не нужный: считать ошибку паралелльно со всеми расчетами.
Для этого есть специальные формулы.

Подробнее — ищите мат часть в интернете
Сравнивать с точностью до eps, обязательно только в том случае, если вы пытаетесь сравнить на РАВЕНСТВО. Любые неравенства сравнивуются «по ситуации». Т.е. если вам сравнение нужно, чтобы, скажем, отфильтровать неверные данные (например, меньше 0) и вы не уверены в точности параметра (он вычислен, скажем, а не из базы взят), то надо писать epsilon и округлять до нуля если вдруг все плохо. В сравнениях же, где важен порядок (сортировка) нужно обязательно сравнивать без eps.

С другой стороны, правильно написанное сравнение 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 (что, разумеется, очевидно из того, что их битовая ёмкость одинакова).
Очевидно, что в каждом случае имеются в виду (точно) представимые целые числа и этот диапазон отличается от диапазона представимых вещественных чисел.
Основная проблема, по-моему, в том, что в мэйнстрим языках нет поддержки «из коробки» чисел с фиксированной точностью — целые или плавающие (как вариант только плавающие), — в то время как в реальной жизни они сплошь и рядом. И мало кто додумывается использовать плавающие числа при измерении, скажем, денег для копеек, а не рублей, при массы — граммов (или миллиграммов), а не килограммов (или граммов), что даст плюсы и целочисленной арифметики — точное сравнение конечных результатов, — и плавающей — более точное вычисление промежуточных результатов, большая гибкость при округлении.
Не знаю, мне кажется, что это несколько иной круг задач, хотя они и довольно широко распространены. А вот исходники, в которых решают квадратное уравнение и ограничивают знаменатель константным eps, просто уже утомляют.
Каждый о своём больном месте :(
отличная для книга для тех кто хочет знать больше «Types and Programming Languages» Benjamin C. Pierce
UFO just landed and posted this here
People familiar with type theory могли бы заметить, что все три языка, наиболее популярные в сайтостроении (JavaScript, PHP, а до PHP — Perl), имеют динамическую слабую неявную типизацию.

Что-то в этом есть, не правда ли?
Конечно. Думаю, это произошло из-за постепенной эволюции языков.
Вначале был нужен шаблонизатор, который в подготовленную страничку вставит полученные из хранилища данные. Для таких задач не нужна ни статическая ни явная типизация, она только усложнит написания.

Задачи в браузере тоже не были глобальными. Джаваскрипт использовался маленькими функциями-вставками не превращая страницу в полноценное приложение. Простота и краткость были очень важными критериями.

Наследственность очень тяжело побороть.
В последнее время, я стал всё чаще видеть попытки использовать статические языки (ocaml, haskell, scala) для геренерации джаваскрипт кода.
Постепенно набирают популярность Asp.Net экосистема, Go, ocaml со стороны сервера. Есть попытки сделать что-то приближенное к современному понимаю типизации, например yesod для haskell.

Процесс медленный, но в задачах, сложность которых заставляет испытывать дикий восторг от возможности автоматической проверки хоть чего-либо, языки с сильной статической типизацией обгоняют всё остальное. Сложность задач решаемых при сайтостроении плавно приближается к такому уровню.
В последнее время, я стал всё чаще видеть попытки использовать статические языки (ocaml, haskell, scala) для геренерации джаваскрипт кода.


Еще C# (Script#).
Предлагаю вам прочитать прекрасную статью: PHP: фрактал плохого дизайна. Людям нравится говно. Что-то в этом есть, не правда ли?

Ну да ладно, это все равно не имеет отношения к типизации, steck уже объяснил чем была выгодна динамическая типизация в вебе.
Ключевое слово — «популярные»
UFO just landed and posted this here
Как-то мне казалось, что есть типизация строгая (strict), а есть сильная (strong). Отличие, кажется: при строгой типизации вообще нет неявных преобразований типов, а сильная допускает безопасные преобразования. Но в любом случае strict сильнее чем strong, очень сильная типизация другими словами.

В PHP есть элементы опциональной явной сильной типизации — type hinting. Весьма ограниченная — только аргументы функций/методов, только объектные типы или массивы, — но есть. Например, function has_permissions(User $user, array $permissions) {} вызовет ошибку времени исполнения при попытке вызвать её с первым аргументом не класса User (или не имплементацией интерфейса User) или их наследников, а c вторым — не массивом. Но для скаляров это не реализовано, насколько я понимаю, по причине того, что PHP всё же слаботипизированный язык и type hinting приведёт к тому, что он практически не будет выполнять своей роли (передача строки вместо целого будет преобразовывать строку в целое) или нарушит единообразие, что неявные преобразования перестанут работать в некоторых случаях, а в PHP его и так мало :).
Delphi: статическая, сильная, явная. Добавьте, плиз, в сводную табличку :)
странно, что ничего нет про duck typing — было бы уместно в этой статье

Про утиную типизацию сказано в подразделе «Обобщенное программирование», просто не указано, что эту возможность принято называть утиной типизацией.
Однако автоматический вывод типов довольно сложная вещь, и даже в таком крутом языке как 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)
будет больше вычислений, чем ожидается по здравому смыслу.

Итог: если не использовать ограничения мономорфизма и всегда стараться брать максимально общий тип, то возможны огромные просадки производительности.
Свежие версии C# я бы отнёс к полу-явной типизации из-за type-inference (var). А F# по способностям выведения типов уже почти подобрался к хаскеллу.
> Тем-не менее не бывает языков со статической и динамической типизаций одновременно. Хотя забегая вперед скажу, что тут я вру — они действительно существуют, но об этом позже.
Groovy имено такой язык. Не просто поддерживает динамику, а именно задумывался как язык одновременно со статической и динамической типизацией.

> Преимущества статической типизации…
Основное и главное (для меня) преимущество статической типизации — это поддержка IDE. Не нужно при вызове API каждый раз бегать в документацию, сверять имена и типы параметров. IDE сама показывает необходимые варианты, исходя из контекста. В итоге работа со сторонними API убыстряется в разы.
Основное и главное (для меня) преимущество статической типизации — это поддержка IDE.


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

Примеры:
Сильная: Java

И что, в джаве так нельзя написать?
System.out.println(1 + "a");
UFO just landed and posted this here
В статье речь идет про типы вообще.
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().
Да не надо мне рассказывать как это внутри реализуется — я это отлично знаю.
Внутри, в генерируем коде, может быть все что угодно. Например, следуя вашей логике возражений, в джаве в каком то смысле вообще нет никаких типов, классов и методов, а есть интерпретатор и куски бинарных данных которые интерпретируются.

Неявное преобразование типов это вообще-то и есть синтаксический сахар.
То что у джавы оно допускается не везде, не означает что его там нет.

Но на самом деле мое возражение связано с некорректностью утверждения что при сильной типизации недопускаются выражения с разными типами. Оно по сути бессмысленно, что подтвержается моим примером.
Согласен, что в татье дано не идеальное определение сильной типизации.
Не согласен с тем, что в java есть неявное преобразование типов.
Пример с оператором "+" — это не неявное преобразование. Это преобразование не менее явное, чем при вызове append(Object obj).
UFO just landed and posted this here
Вы хотите сказать что класс не является типом в том смысле про который идет речь в концепции типизации?
Честно говоря до сих пор не понял ваш ответ.
При чем тут System.out.println и java.io.PrintStream.println, если результат выражения {1 + «a»} — String?

Судя по всему amosk акцентирует внимание именно на операторе "+" и его действии.

Если рассматривать "+" как математическую операцию, то это операция вида (A, A) → A. То есть переводящая пару объектов из одного множества в объект из того же множества.
При таком подходе эта операция не определена даже для пары (0, 0.0), так как это объекты разных множеств (разных типов) и требуется неявное преобразование.
То же самое и в случае (_, String) -> String. Первый аргумент, при математическом рассмотрении, неявно преобразуется к String.

Выше я пытался аргументировать к тому, что "+" в Java не соответствует своему математическому тезке, а представляет отдельную строго определенную операцию.

Ваши же аргументы я понять не могу. Мне даже не понятно часть с апеллированием к примитивным типам в Java, так как они в языке, позиционирующемся как чистый ООП, представляют из себя отход от основной идеологии в целях быстродействия.
UFO just landed and posted this here
Это по сути перегрузка арифметического оператора. К типизации отношение имеет косвенное.
Жаль, что Go не рассмотрели. В первую очередь интересен тем, что он новичок (по дате создания) в сравнении с популярными языками программирования.
А, кстати, тот же Go имеет неявную типизацию и при этом в сигнатурах методов указываются типы возвращаемых значений.
Для C++ есть еще QVariant из Qt, он позволяет некоторые вещи делать как в динамических языках. Особенно это нужно при интеграции C++ кода с кодом на QML.
Я правильно понял, что если язык имеет динамическую типизацию, то он имеет и неявную?
Я не знаю языков с динамической явной типизацией (что подтверждает Ваш вывод), но чисто теоретически они могут существовать.
Гм. Объявляем явно, а потом присваиваем, что хотим :) Хотя, если рассматривать полиморфизм как ограниченную динамическую типизацию, то можно представить и такое. Но это я за уши притягиваю :)
Ну можно немного подругому сделать, поглядите:

Язык с динамической неявной типизацией:
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);

playground

C#. Для того чтобы тип был динамическим его нужно явно объявить таковым.
Структурную и номинативную типизации забыли обсудить.
Примечание: я намерено использовал некаррированную функцию, а также намерено записал частную сигнатуру вместо более общей add :: (Num a) => a -> a -> a*, т.к. хотел показать идею, без объяснения синтаксиса Haskell'а.

Если вы используете кортеж, то сигнатура функции будет:

add :: (Num a) => (a, a) -> a
У некаррированной add такая и указана (со «специализацией» a = Integer).
Интересная статья. Впервые столкнулся с систематическим изложением данного вопроса. У меня в голове от всего этого была каша. Смешение понятий в вопросе типизации довольно частое явление в статьях. Хочу добавить несколько своих замечаний. Конечно ассемблер безтиповой язык. На уровне языка нет типов. Есть регистры, память и операции над ними. Как то на них отображаются типы языков высокого уровня. Но это уже проблемы этих языков.
В статье хорошо изложены концепции типизации и хорошо видны противоречия возникающие в типизированных языках. Ясно, что необходима и статическая и динамическая и сильная и слабая типизация. И различные языки по разному находят этот баланс. В основном за счет расширения типов в языке или встроенных библиотеках. При этом совершенно очевидно что базовых типов недостаточно и появляются типы вроде даты и 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 прямо называют эту технологию устаревшей[3]
А Си, Паскаль, Си++ не устаревшие? А новые, что принципиально лучше старых? Чем это? Новые языки делаются очень просто. Берем Си и добавляем пробел в конце или еще что нибудь. Принципиально нового, кроме 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.

Дополню, что C++ с аналогичной Java ситуацией, касающейся приведения типов, отнесён к слабой. А ведь там даже void * неявно не приводится к любому указателю в отличии от C (хотя в g++ это можно разрешить с помощью -fpermissive).

Привет из 2019-го!
Из новостей — появился TypeScript.

PHP тут в конце списков и представлен как самый-самый неявный и самый слабый в плане типизации.

НО! с 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), которая не даст приводить скаляры неявным образом там, где указан тип

Динамичечкая типизация местами сильная. Причём не строгая, даже со strict_types, поскольку есть неявные преобразования int float как минимум.

Должен быть по всей видимости auto a = 5u;


// Автоматический вывод типа
unsigned int a = 5;
auto b = a + 3;

UFO just landed and posted this here

В Питоне bool это подтип типа int. True имеет значение 1, False — 0, и они позволяют арифмитические операции.


issubclass(bool, int) # True
isinstance(True, int) # True

Но, например, строку с числом (и соответственно с True) интерпретатор вам сложить не даст.

Sign up to leave a comment.

Articles