Контакти

Динамічна типізація. Лікнеп по типізації в мовах програмування Динамічна типізація краще ніж сувора

Ця стаття містить необхідний мінімум тих речей, які просто необхідно знати про типізації, щоб не називати динамічну типізацію злом, Lisp - безтиповою мовою, а C - мовою зі строгою типізацією.

У повній версії знаходиться докладний опис всіх видів типізації, приправлене прикладами коду, посиланнями на популярні мови програмування і показовими картинками.

Рекомендую прочитати спочатку коротку версію статті, а потім при наявності бажання і повну.

Коротка версія

Мови програмування по типізації прийнято ділити на два великих табори - типізовані і нетипізовані (Безтипові). До першого наприклад відносяться C, Python, Scala, PHP і Lua, а до другого - мова асемблера, Forth і Brainfuck.

Так як "безтипових типізація" по своїй суті - проста як пробка, далі вона ні на які інші види не ділиться. А ось типізовані мови розділяються ще на кілька пересічних категорій:

  • Статична / динамічна типізація. Статична визначається тим, що кінцеві типи змінних і функцій встановлюються на етапі компіляції. Тобто вже компілятор на 100% впевнений, який тип де знаходиться. У динамічної типізації всі типи з'ясовуються вже під час виконання програми.

    приклади:
    Статична: C, Java, C #;
    Динамічна: Python, JavaScript, Ruby.

  • Сильна / слабка типізація (також іноді говорять сувора / нестрогая). Сильна типізація виділяється тим, що мова не дозволяє змішувати в виразах різні типи і не виконує автоматичні неявні перетворення, наприклад не можна відняти з рядка безліч. Мови зі слабкою типізацією виконують безліч неявних перетворень автоматично, навіть якщо може відбутися втрата точності або перетворення неоднозначно.

    приклади:
    Сильна: Java, Python, Haskell, Lisp;
    Слабка: C, JavaScript, Visual Basic, PHP.

  • Явна / неявна типізація. Явно-типізовані мови відрізняються тим, що тип нових змінних / функцій / їх аргументів потрібно задавати явно. Відповідно мови з неявній типизацией перекладають це завдання на компілятор / інтерпретатор.

    приклади:
    Явна: C ++, D, C #
    Неявна: PHP, Lua, JavaScript

Також потрібно зауважити, що всі ці категорії перетинаються, наприклад мову C має статичну слабку явну типізацію, а мова Python - динамічну сильну неявну.

Тим-не менше не буває мов зі статичної та динамічної типізації одночасно. Хоча забігаючи вперед скажу, що тут я брешу - вони дійсно існують, але про це пізніше.

Детальна версія

Якщо короткої версії Вам здалося недостатньо, добре. Не дарма ж я писав детальну? Головне, що в короткій версії просто неможливо було вмістити всю корисну і цікаву інформацію, а докладна буде можливо занадто довгою, щоб усі могли його прочитати, не напружуючись.

безтипових типізація

У безтипових мовах програмування - все суті вважаються просто послідовностями біт, різної довжини.

Безтипових типізація зазвичай властива низькорівневим (мова асемблера, Forth) і езотеричним (Brainfuck, HQ9, Piet) мов. Однак і у неї, поряд з недоліками, є деякі переваги.

переваги
  • Дозволяє писати на гранично низькому рівні, причому компілятор / інтерпретатор не заважатиме будь-якими перевірками типів. Ви вільні робити будь-які операції над будь-якими видами даних.
  • Одержуваний код зазвичай більш ефективний.
  • Прозорість інструкцій. При знанні мови зазвичай немає сумнівів, що з себе представляє той чи інший код.
недоліки
  • Складність. Часто виникає необхідність в поданні комплексних значень, таких як списки, рядки або структури. З цим можуть виникнути незручності.
  • Відсутність перевірок. Будь-які безглузді дії, наприклад віднімання покажчика на масив з символу будуть вважатися абсолютно нормальними, що загрожує так важко впіймати помилками.
  • Низький рівень абстракції. Робота з будь-яким складним типом даних нічим не відрізняється від роботи з числами, що звичайно буде створювати багато труднощів.
Сильна безтіповая типізація?

Так, таке існує. Наприклад в мові асемблера (для архітектури х86 / х86-64, інших не знаю) не можна ассембліровать програму, якщо ви спробуєте завантажити в регістр cx (16 біт) дані з регістра rax (64 біта).

mov cx, eax; помилка часу ассемблирования

Так виходить, що в ассемлере все-таки є типізація? Я вважаю, що цих перевірок недостатньо. А Ваша думка, звичайно, залежить тільки від Вас.

Статична і динамічна типізації

Головне, що відрізняє статичну (static) типізацію від динамічної (dynamic) то, що всі перевірки типів виконуються на етапі компіляції, а не етапі виконання.

Деяким людям може здатися, що статична типізація занадто обмежена (насправді так і є, але від цього давно позбулися за допомогою деяких методик). Деяким же, що динамічно типізовані мови - це гра з вогнем, але які ж риси їх виділяють? Невже обидва види мають шанси на існування? Якщо немає, то чому багато як статично, так і динамічно типізованих мов?

Давайте розберемося.

Переваги статичної типізації
  • Перевірки типів відбуваються тільки один раз - на етапі компіляції. А це означає, що нам не потрібно буде постійно з'ясовувати, чи не намагаємося ми поділити число на рядок (і або видати помилку, або здійснити перетворення).
  • Швидкість виконання. З попереднього пункту ясно, що статично типізовані мови практично завжди швидше динамічно типізованих.
  • При деяких додаткових умовах, дозволяє виявляти потенційні помилки вже на етапі компіляції.
Переваги динамічної типізації
  • Простота створення універсальних колекцій - куп всього і вся (рідко виникає така необхідність, але коли виникає динамічна типізація виручить).
  • Зручність опису узагальнених алгоритмів (наприклад сортування масиву, яка буде працювати не тільки на списку цілих чисел, але і на списку речових і навіть на списку рядків).
  • Легкість в освоєнні - мови з динамічною типізацією зазвичай дуже гарні для того, щоб почати програмувати.

узагальнене програмування

Добре, найважливіший аргумент за динамічну типізацію - зручність опису узагальнених алгоритмів. Давайте уявимо собі проблему - нам потрібна функція пошуку по кільком масивів (або списками) - по масиву цілих чисел, по масиву речових і масиву символів.

Як же ми будемо її вирішувати? Вирішимо її на 3-ех різних мовах: одному з динамічною типізацією і двох із статичною.

Алгоритм пошуку я візьму один з найпростіших - перебір. Функція буде отримувати шуканий елемент, сам масив (або список) і повертати індекс елемента, або, якщо елемент не знайдений - (-1).

Динамічне рішення (Python):

Def find (required_element, list): for (index, element) in enumerate (list): if element \u003d\u003d required_element: return index return (-1)

Як бачите, все просто і ніяких проблем з тим, що список може містити хоч числа, хоч списки, хоч інші масиви немає. Дуже добре. Давайте підемо далі - вирішимо цю-ж задачу на Сі!

Статична рішення (Сі):

Unsigned int find_int (int required_element, int array, unsigned int size) (for (unsigned int i \u003d 0; i< size; ++i) if (required_element == array[i]) return i; return (-1); } unsigned int find_float(float required_element, float array, unsigned int size) { for (unsigned int i = 0; i < size; ++i) if (required_element == array[i]) return i; return (-1); } unsigned int find_char(char required_element, char array, unsigned int size) { for (unsigned int i = 0; i < size; ++i) if (required_element == array[i]) return i; return (-1); }

Ну, кожна функція окремо схожа на версію з Python, але чому їх три? Невже статичну програмування програло?

І так і ні. Є кілька методик програмування, одну з яких ми зараз розглянемо. Вона називається узагальнене програмування і мова C ++ її непогано підтримує. Давайте подивимося на нову версію:

Статична рішення (узагальнене програмування, C ++):

Template unsigned int find (T required_element, std :: vector array) (for (unsigned int i \u003d 0; i< array.size(); ++i) if (required_element == array[i]) return i; return (-1); }

Добре! Це виглядає не сильно складніше ніж версія на Python і при цьому не довелося багато писати. До того ж ми отримали реалізацію для всіх масивів, а не тільки для 3-ох, необхідних для вирішення задачі!

Ця версія схоже саме те, що потрібно - ми отримуємо одночасно плюси статичної типізації та деякі плюси динамічної.

Здорово, що це взагалі можливо, але може бути ще краще. По-перше узагальнене програмування може бути зручніше і красивіше (наприклад в мові Haskell). По-друге крім узагальненого програмування також можна застосувати поліморфізм (результат буде гірше), перевантаження функцій (аналогічно) або макроси.

Статика в динаміці

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

  • C # підтримує псевдо-тип dynamic.
  • F # підтримує синтаксичний цукор у вигляді оператора ?, на базі чого може бути реалізована імітація динамічної типізації.
  • Haskell - динамічна типізація забезпечується модулем Data.Dynamic.
  • Delphi - за допомогою спеціального типу Variant.

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

  • Common Lisp - декларації типів.
  • Perl - з версії 5.6, досить обмежено.

Сильна і слабка типізації

Мови з сильною типізацією не дозволяють змішувати суті різних типів у виразах і не виконують ніяких автоматичних перетворень. Також їх називають "мови з строгою типізацією". Англійський термін для цього - strong typing.

Слабо типізовані мови, навпаки всіляко сприяють, щоб програміст змішував різні типи в одному вираженні, причому компілятор сам приведе все до єдиного типу. Також їх називають "мови з нестрогой типизацией". Англійський термін для цього - weak typing.

Слабку типізацію часто плутають з динамічної, що абсолютно невірно. Динамічно збірний мову може бути і слабо і сильно типізований.

Однак мало, хто надає значення строгості типізації. Часто заявляють, що якщо мова статично типізований, то Ви зможете відловити безліч потенційних помилок при компіляції. Вони Вам брешуть!

Мова при цьому повинен мати ще й сильну типізацію. І правда, якщо компілятор замість повідомлення про помилку буде просто додавати рядок до числа, або що ще гірше, відніме із одного масиву інший, який нам сенс, що все "перевірки" типів будуть на етапі компіляції? Правильно - слабка статична типізація ще гірше, ніж сильна динамічна! (Ну, це моя думка)

Так що-ж у слабкій типізації взагалі немає плюсів? Можливо так виглядає, проте не дивлячись на те, що я затятий прихильник сильної типізації, повинен погодитися, що у слабкої теж є переваги.

Хочете дізнатися які?

Переваги сильною типізації
  • Надійність - Ви отримаєте виключення або помилку компіляції, натомість неправильної поведінки.
  • Швидкість - замість прихованих перетворень, які можуть бути досить витратними, з сильною типізацією необхідно писати їх явно, що змушує програміста як мінімум знати, що ця ділянка коду може бути повільним.
  • Розуміння роботи програми - знову-таки, замість неявного приведення типів, програміст пише все сам, а значить приблизно розуміє, що порівняння рядка і числа відбувається не само-собою і не по-помахом чарівної палички.
  • Визначеність - коли ви пишете перетворення вручну ви точно знаєте, що ви перетворюєте і будь-що. Також ви завжди будете розуміти, що такі перетворення можуть привести до втрати точності і до невірних результатів.
Переваги слабкої типізації
  • Зручність використання змішаних виразів (наприклад з цілих і дійсних чисел).
  • Абстрагування від типізації та зосередження на завданні.
  • Стислість записи.

Гаразд, ми розібралися, виявляється у слабкій типізації теж є переваги! А чи є способи перенести плюси слабкою типізації в сильну?

Виявляється є і навіть два.

Неявне приведення типів, в однозначних ситуаціях і без втрат даних

Ух ... Досить довгий пункт. Давайте я буду далі скорочувати його до "обмежене неявне перетворення" Так що ж означає однозначна ситуація і втрати даних?

Однозначна ситуація, це перетворення або операція в якій сутність відразу зрозуміла. Ось наприклад додавання двох чисел - однозначна ситуація. А перетворення числа в масив - немає (можливо створиться масив з одного елемента, можливо масив, з такою довгою, заповнений елементами по-замовчуванню, а можливо число перетворюється в рядок, а потім в масив символів).

Втрата даних це ще простіше. Якщо ми перетворимо дійсне число 3.5 в ціле - ми втратимо частину даних (насправді ця операція ще й неоднозначна - як буде проводитися округлення? У більшу сторону? В меншу? Відкидання дробової частини?).

Перетворення в неоднозначних ситуаціях і перетворення з втратою даних - це дуже, дуже погано. Нічого гіршого цього в програмуванні немає.

Якщо ви мені не вірите, вивчіть мову PL / I або навіть просто пошукайте його специфікацію. У ньому є правила перетворення між УСІМА типами даних! Це просто пекло!

Гаразд, давайте згадаємо про обмежене неявне перетворення. Чи є такі мови? Так, наприклад в Pascal Ви можете перетворити ціле число в реальне, але не навпаки. Також схожі механізми є в C #, Groovy і Common Lisp.

Гаразд, я говорив, що є ще спосіб отримати пару плюсів слабкою типізації в сильному мовою. І так, він є і називається поліморфізм конструкторів.

Я поясню його на прикладі чудового мови Haskell.

Поліморфні конструктори з'явилися в результаті спостереження, що найчастіше безпечні неявні перетворення потрібні при використанні числових літералів.

Наприклад в вираженні pi + 1, не хочеться писати pi + 1.0 або pi + float (1). Хочеться написати просто pi + 1!

І це зроблено в Haskell, завдяки тому, що у литерала 1 немає конкретного типу. Це ні ціле, ні речовий, ні комплексне. Це ж просто число!

В результаті при написанні простий функції sum xy, перемножуються всі числа від x до y (з инкрементом в 1), ми отримуємо відразу кілька версій - sum для цілих, sum для речових, sum для раціональних, sum для комплексних чисел і навіть sum для всіх тих числових типів що Ви самі визначили.

Звичайно рятує цей прийом тільки при використанні змішаних виразів з числовими літералами, а це лише верхівка айсберга.

Таким чином можна сказати, що найкращим виходом буде балансування на межі, між сильною і слабкою типізацією. Але поки ідеальний баланс не тримає ні одна мова, тому я більше схиляюся до сильно типізованим мов (таким як Haskell, Java, C #, Python), а не до слабо типізованим (таким як C, JavaScript, Lua, PHP).

Явна і неявна типізації

Мова з явною типизацией передбачає, що програміст повинен вказувати типи всіх змінних і функцій, які оголошує. Англійський термін для цього - explicit typing.

Мова з неявній типизацией, навпаки, пропонує Вам забути про типах і перекласти завдання виведення типів на компілятор або інтерпретатор. Англійськи термін для цього - implicit typing.

По-початку можна вирішити, що неявна типізація рівносильна динамічної, а явна - статичної, але далі ми побачимо, що це не так.

Чи є плюси у кожного виду, і знову ж таки, чи є їх комбінації і чи є мови з підтримкою обох методів?

Переваги явною типізації
  • Наявність у кожної функції сигнатури (наприклад int add (int, int)) дозволяє без проблем визначити, що функція робить.
  • Програміст відразу записує, якого типу значення можуть зберігатися в конкретній змінної, що знімає необхідність запам'ятовувати це.
Переваги неявній типізації
  • Скорочення записи - def add (x, y) явно коротше, ніж int add (int x, int y).
  • Стійкість до змін. Наприклад якщо в функції тимчасова змінна була того-ж типу, що і вхідний аргумент, то в явно типизированном мовою при зміні типу вхідного аргументу потрібно буде змінити ще й тип тимчасової змінної.

Добре, видно, що обидва підходи мають як плюси так і мінуси (а хто очікував чогось го ще?), Так давайте пошукаємо способи комбінування цих двох підходів!

Явна типізація по-вибору

Є мови, з неявній типизацией за замовчуванням і можливістю вказати тип значень при необхідності. Справжній тип вираження транслятор виведе автоматично. Один з таких мов - Haskell, давайте я наведу простий приклад, для наочності:

Без явного вказівки типу add (x, y) \u003d x + y - Явна вказівку типу add :: (Integer, Integer) -\u003e Integer add (x, y) \u003d x + y

Примітка: я має намір використовував некаррірованную функцію, а також має намір записав приватну сигнатуру замість більш загальної add :: (Num a) -\u003e a -\u003e a -\u003e a, тому що хотів показати ідею, без пояснення синтаксису Haskell "а.

Хм. Як ми бачимо, це дуже красиво і коротко. Запис функції займає всього 18 символів на одному рядку, включаючи прогалини!

Однак автоматичний висновок типів досить складна річ, і навіть в такому крутому мовою як Haskell, він іноді не справляється. (Як приклад можна привести обмеження мономорфізму)

Чи є мови з явною типизацией за замовчуванням і неявній по-необхідності? кон
ечно.

Неявна типізація по-вибору

У новому стандарті мови C ++, названому C ++ 11 (раніше називався C ++ 0x), було введено ключове слово auto, завдяки якому можна змусити компілятор вивести тип, виходячи з контексту:

Давайте порівняємо: // Ручне вказівку типу unsigned int a \u003d 5; unsigned int b \u003d a + 3; // Автоматичний висновок типу unsigned int a \u003d 5; auto b \u003d a + 3;

Не погано. Але запис скоротилася не сильно. Давайте подивимося приклад з ітераторами (якщо не розумієте, не бійтеся, головне зауважте, що запис завдяки автоматичному висновку дуже сильно скорочується):

// Ручне вказівку типу std :: vector vec \u003d randomVector (30); for (std :: vector :: const_iterator it \u003d vec.cbegin (); ...) (...) // Автоматичний висновок типу auto vec \u003d randomVector (30); for (auto it \u003d vec.cbegin (); ...) (...)

Ух ти! Ось це скорочення. Гаразд, але чи можна зробити що-небудь в дусі Haskell, де тип значення буде залежати від типів аргументів?

І знову відповідь так, завдяки ключовим словом decltype в комбінації з auto:

// Ручне вказівку типу int divide (int x, int y) (...) // Автоматичний висновок типу auto divide (int x, int y) -\u003e decltype (x / y) (...)

Може здатися, що ця форма запису не сильно хороша, але в комбінації з узагальненим програмуванням (templates / generics) неявна типізація або автоматичний висновок типів творять чудеса.

Деякі мови програмування по даній класифікації

Я наведу невеликий список з популярних мов і напишу як вони підрозділяються по кожній категорії "типізації".

JavaScript - Динамічна / Слабка / Неявна Ruby - Динамічна / Сильна / Неявна Python - Динамічна / Сильна / Неявна Java - Статична / Сильна / Явна PHP - Динамічна / Слабка / Неявна C - Статична / Слабка / Явна C ++ - Статична / Полусільная / Явна Perl - Динамічна / Слабка / Неявна Objective-C - Статична / Слабка / Явна C # - Статична / Сильна / Явна Haskell - Статична / Сильна / Неявна Common Lisp - Динамічна / Сильна / Неявна

Можливо я десь помилився, особливо з CL, PHP і Obj-C, якщо по певній мові у Вас інша думка - напишіть в коментарях.

висновок

Окей. Вже скоро буде світло і я відчуваю, що про типізацію більше нічого сказати. Ой як? Тема бездонна? Дуже багато залишилося недоговорено? Прошу в коментарі, поділіться корисною інформацією.



Ця стаття розповідає про різницю між статично типізований і динамічно типізований мовами, розглядає поняття "сильної" і "слабкої" типізації, і порівнює потужність систем типізації в різних мовах. Останнім часом спостерігається чіткий рух в бік більш суворих і потужних систем типізації в програмуванні, тому важливо розуміти про що йде мова коли говорять про типах і типізації.



Тип - це колекція можливих значень. Ціле число може мати значеннями 0, 1, 2, 3 і так далі. Булево може бути істиною або брехнею. Можна придумати свій тип, наприклад, тип "ДайПять", в якому можливі значення "дай" і "5", і більше нічого. Це не рядок і не число, це новий, окремий тип.


Статично типізовані мови обмежують типи змінних: мова програмування може знати, наприклад, що x - це Integer. В цьому випадку програмісту забороняється робити x \u003d true, це буде некоректний код. Компілятор відмовиться компілювати його, так що ми не зможемо навіть запустити такий код. Інший статично типізований мова може володіти іншими виразними можливостями, і ніяка з популярних систем типів не здатна висловити наш тип ДайПять (але багато хто може висловити інші, більш витончені ідеї).


Динамічно типізовані мови позначають значення типами: мова знає, що 1 це integer, 2 це integer, але він не може знати, що змінна x завжди містить integer.


Середовище виконання мови перевіряє ці мітки в різні моменти часу. Якщо ми спробуємо скласти два значення, то вона може перевірити, чи є вони числами, рядками або масивами. Потім вона складе ці значення, склеїть їх або видасть помилку, залежно від типу.

Статично типізовані мови

Статичні мови перевіряють типи в програмі під час компіляції, ще до запуску програми. Будь-яка програма, в якій типи порушують правила мови, вважається некоректною. Наприклад, більшість статичних мов відхилить вираз "a" + 1 (мова Сі - це виняток з цього правила). Компілятор знає, що "a" - це рядок, а 1 - це ціле число, і що + працює тільки коли ліва і права частина відносяться до одного типу. Так що йому не потрібно запускати програму щоб зрозуміти, що існує проблема. Кожен вираз в статично типізований мові належить до певного типу, який можна визначити без запуску коду.


Багато статично типізовані мови вимагають позначати тип. Функція в Java public int add (int x, int y) приймає два цілих числа і повертає третій ціле число. Інші статично типізовані мови можуть визначити тип автоматично. Та ж сама функція складання в Haskell виглядає так: add x y \u003d x + y. Ми не розголошуємо мови типи, але він може визначити їх сам, бо знає, що + працює тільки на числах, так що x і y повинні бути числами, це свідчить про те add приймає два числа як аргументи.


Це не зменшує "статичність" системи типів. Система типів в Haskell знаменита своєю статичністю, строгістю і потужністю, і в по всіх цих фронтах Haskell випереджає Java.

Динамічно типізовані мови

Динамічно типізовані мови не вимагають вказувати тип, але і не визначають його самі. Типи змінних невідомі до того моменту, коли у них є конкретні значення при запуску. Наприклад, функція в Python


def f (x, y): return x + y

може складати два цілих числа, склеювати рядки, списки і так далі, і ми не можемо зрозуміти, що саме відбувається, поки не запустимо програму. Можливо, в якийсь момент функцію f викличуть з двома рядками, і з двома числами в інший момент. В такому випадку x і y будуть містити значення різних типів в різний час. Тому кажуть, що значення в динамічних мовами володіють типом, але змінні і функції - немає. Значення 1 це безперечно integer, але x і y можуть бути чим завгодно.

порівняння

Більшість динамічних мов видадуть помилку, якщо типи використовуються некоректно (JavaScript - відоме виключення; він намагається повернути значення для будь-якого виразу, навіть коли воно не має сенсу). При використанні динамічно типізованих мов навіть проста помилка виду "a" + 1 може виникнути в бойовому оточенні. Статичні мови запобігають такі помилки, але, звичайно, ступінь запобігання залежить від потужності системи типів.


Статичні і динамічні мови побудовані на фундаментально різних ідеях про коректність програм. У динамічному мовою "a" + 1 це коректна програма: код буде запущений і з'явиться помилка в середовищі виконання. Однак, в більшості статично типізованих мов вираз "a" + 1 - це не програма: Вона не буде скомпільована і не буде запущена. Це некоректне код, так само, як набір випадкових символів! &% ^ @ * &% ^ @ * - це некоректний код. Це додаткове поняття про коректність і некоректності не має еквівалента в динамічних мовами.

Сильна і слабка типізація

Поняття "сильний" і "слабкий" - дуже неоднозначні. Ось деякі приклади їх використання:

    Іноді "сильний" означає "статичний".
    Тут все просто, але краще використовувати термін "статичний", тому що більшість використовують і розуміють його.

    Іноді "сильний" означає "не чинить неявне перетворення типів".
    Наприклад, JavaScript дозволяє написати "a" + 1, що можна назвати "слабкою типізацією". Але майже всі мови надають той чи інший рівень неявного перетворення, яке дозволяє автоматично переходити від цілих чисел до чисел з плаваючою комою на кшталт 1 + 1.1. У реальності, більшість людей використовують слово "сильний" для визначення кордону між прийнятним і неприйнятним перетворенням. Немає якоїсь загальноприйнятої кордону, вони все неточні і залежать від думки конкретної людини.

    Іноді "сильний" означає, що неможливо обійти суворі правила типізації в мові.

  • Іноді "сильний" означає безпечний для пам'яті (memory-safe).
    Сі - це приклад небезпечного для пам'яті мови. Якщо xs - це масив чотирьох чисел, то Сі з радістю виконає код xs або xs, повертаючи якесь значення з пам'яті, яка знаходиться відразу за xs.

Давайте зупинимося. Ось як деякі мови відповідають цим визначенням. Як можна помітити, тільки Haskell послідовно "сильний" за всіма параметрами. Більшість мов не такі чіткі.



( "Коли як" в колонці "Неявні перетворення" означає, що поділ між сильним і слабким залежить від того, які перетворення ми вважаємо прийнятними).


Найчастіше терміни "сильний" і "слабкий" відносяться до невизначеної комбінації різних визначень вище, і інших, які не відображаються тут визначень. Весь цей безлад робить слова "сильний" і "слабкий" практично безглуздими. Коли хочеться використовувати ці терміни, то краще описати, що саме мається на увазі. Наприклад, можна сказати, що "JavaScript повертає значення, коли складається рядок з числом, але Python повертає помилку". В такому випадку ми не будемо витрачати свої сили на спроби дійти згоди про безліч значень слова "сильний". Або, ще гірше: прийдемо до недозволеному нерозуміння через термінології.


У більшості випадків терміни "сильний" і "слабкий" в інтернеті є неясними і погано визначеними думками конкретних людей. Вони використовуються, щоб назвати язик "поганим" або "хорошим", і це думка обертається в технічний жаргон.



Сильна типізація: Система типів, яку я люблю і з якою мені комфортно.

Слабка типізація: Система типів, яка турбує мене або з якої мені не комфортно.

Поступова типізація (gradual typing)

Чи можна додати статичні типи в динамічні мови? У деяких випадках - так. В інших це складно або неможливо. Найбільш очевидна проблема - це eval і інші схожі можливості динамічних мов. Виконання 1 + eval ( "2") в Python дає 3. Але що дасть 1 + eval (read_from_the_network ())? Це залежить від того, що в мережі на момент виконання. Якщо отримаємо число, то вираз коректно. Якщо рядок, то немає. Неможливо дізнатися до запуску, так що неможливо аналізувати тип статично.


Незадовільне розв'язання на практиці - це задати висловом eval () тип Any, що нагадує Object в деяких об'єктно-орієнтованих мовах програмування або інтерфейс interface () в Go: це тип, якому задовольняє будь-яке значення.


Значення типу Any не обмежені нічим, так що зникає можливість системи типів допомагати нам в коді з eval. Мови, в яких є і eval і система типів, повинні відмовлятися від безпеки типів при кожному використанні eval.


У деяких мовах є опціональна або поступова типізація (gradual typing): вони динамічні за замовчуванням, але дозволяють додавати деякі статичні анотації. В Python недавно додали опціональні типи; TypeScript - це надбудова над JavaScript, в якому є опціональні типи; Flow виробляє статичний аналіз старого доброго коду на JavaScript.


Ці мови пропонують деякі переваги статичної типізації, але вони ніколи не дадуть повної гарантії, як по-справжньому статичні мови. Деякі функції будуть статично типізований, а деякі будуть динамічно типізований. Програмісту завжди потрібно знати і боятися різниці.

Компіляція статично типізований коду

Коли відбувається компіляція статично типізований коду, спочатку перевіряється синтаксис, як в будь-якому компіляторі. Потім перевіряються типи. Це означає, що статичний мову спочатку може поскаржитися на одну синтаксичну помилку, а після її виправлення поскаржитися на 100 помилок типізації. Виправлення синтаксичної помилки не створило ці 100 помилок типізації. Компілятор просто не мав можливості виявити помилки типів, поки не був виправлений синтаксис.


Компілятори статичних мов зазвичай можуть генерувати більш швидкий код, ніж компілятори динамічних. Наприклад, якщо компілятор знає, що функція add приймає цілі числа, то він може використовувати нативну інструкцію ADD центрального процесора. Динамічний мова буде перевіряти тип при виконанні, вибираючи один з безлічі функцій add в залежності від типів (складаємо integers або floats або склеюємо рядки або, може бути, списки?) Або потрібно вирішити, що виникла помилка і типи не відповідають один одному. Всі ці перевірки займають час. У динамічних мовах використовуються різні трюки для оптимізації, наприклад JIT-компіляція (just-in-time), де код перекомпілюється при виконанні після отримання всієї необхідної про типах інформації. Однак, ніякої динамічний мова не може зрівнятися по швидкістю з акуратно написаним статичним кодом на мові начебто Rust.

Аргументи на користь статичних і динамічних типів

Прихильники статичної системи типів вказують на те, що без системи типів прості помилки можуть привести до проблем в продакшені. Це, звичайно ж, правда. Той, хто використовував динамічний мову, відчув це на собі.


Прихильники динамічних мов вказують на те, що на таких мовах, здається, легше писати код. Це безперечно справедливо для деяких видів коду, який ми час від часу пишемо, як, наприклад, той код з eval. Це спірне рішення для регулярної роботи, і тут має сенс згадати невизначений слово "легко". Річ Хики відмінно розповів про слово "легко", і його зв'язок зі словом "просто". Подивившись цю доповідь ви зрозумієте, що нелегко правильно використовувати слово "легко". Бійтеся "легкості".


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


JavaScript намагається продовжити роботу, навіть якщо це означає безглузду конвертацію (на кшталт "a" + 1, що дає "a1"). Python в свою чергу намагається бути консервативним і часто повертає помилки, як у випадку з "a" + 1.


Існують різні підходи з різними рівнями безпеки, але Python і JavaScript обидва є динамічно типізований мовами.



Haskell ж не дозволить скласти integer і float без явного перетворення перед цим. Сі і Haskell обидва є статично типізований, не дивлячись на такі великі відмінності.


Є безліч варіацій динамічних і статичних мов. Будь-яке беззастережне висловлювання виду "статичні мови краще, ніж динамічні, коли справа стосується Х" - \u200b\u200bце майже гарантовано дурниця. Це може бути правдою в разі конкретних мов, але тоді краще сказати "Haskell краще, ніж Python коли справа стосується Х".

Різноманітність статичних систем типізації

Давайте поглянемо на два знаменитих прикладу статично типізованих мов: Go і Haskell. В системі типізації Go немає узагальнених типів, типів з "параметрами" від інших типів. Наприклад, можна створити свій тип для списків MyList, який може зберігати будь-які потрібні нам дані. Ми хочемо мати можливість створювати MyList цілих чисел, MyList рядків і так далі, не змінюючи вихідний код MyList. Компілятор повинен стежити за типізацією: якщо є MyList цілих чисел, і ми випадково додаємо туди рядок, то компілятор повинен відхилити програму.


Go спеціально був спроектований таким чином, щоб неможливо було ставити типи на кшталт MyList. Найкраще, що можна зробити, це створити MyList "порожніх інтерфейсів": MyList може містити об'єкти, але компілятор просто не знає їх тип. Коли ми дістаємо об'єкти з MyList, нам потрібно повідомити компілятору їх тип. Якщо ми говоримо "Я дістаю рядок", але в реальності значення - це число, то буде помилка виконання, як у випадку з динамічними мовами.


У Go також немає безлічі інших можливостей, які присутні в сучасних статично типізованих мовах (або навіть в деяких системах 1970-х років). У творців Go були свої причини для цих рішень, але думка людей з боку з цього приводу іноді може звучати різко.


Тепер давайте порівняємо з Haskell, який володіє дуже потужною системою типів. Якщо задати тип MyList, то тип "списку чисел" це просто MyList Integer. Haskell не дасть нам випадково додати рядок в список, і упевниться, що ми не покладемо елемент зі списку в строкову змінну.


Haskell може висловлювати набагато складніші ідеї безпосередньо типами. Наприклад, Num a \u003d\u003e MyList a означає "MyList значень, які відносяться до одного типу чисел". Це може бути список integer "ов, float" ов або десяткових чисел з фіксованою точністю, але це безперечно ніколи не буде списком рядків, що перевіряється при компіляції.


Можна написати функцію add, яка працює з будь-якими чисельними типами. У цій функції буде тип Num a \u003d\u003e (a -\u003e a -\u003e a). Це означає:

  • a може бути будь-яким чисельним типом (Num a \u003d\u003e).
  • Функція приймає два аргументи типу a і повертає тип a (a -\u003e a -\u003e a).

Останній приклад. Якщо тип функції це String -\u003e String, то вона приймає рядок і повертає рядок. Але якщо це String -\u003e IO String, то вона також робить якийсь введення / виведення. Це може бути звернення до диска, до мережі, читання з терміналу і так далі.


Якщо у функції в типі немає IO, то ми знаємо, що вона не робить ніяких операцій введення / виводу. У веб-додатку, наприклад, можна зрозуміти, чи зраджує функція базу даних, просто глянувши на її тип. Ніякі динамічні і майже ніякі статичні язики не способи на таке. Це особливість мов з найпотужнішою системою типізації.


У більшості мов нам довелося б розбиратися з функцією і всіма функціями, які звідти викликаються, і так далі, в спробах знайти щось, що змінює базу даних. Це виснажливий процес, в якому легко припуститися помилки. А система типів Haskell може відповісти на це питання просто і гарантовано.


Порівняйте цю потужність з Go, який не здатний висловити просту ідею MyList, не кажучи вже про "функції, яка приймає два аргументи, і вони обидва чисельні і одного типу, і яка робить введення / виведення".


Підхід Go спрощує написання інструментів для програмування на Go (зокрема, реалізація компілятора може бути простою). До того ж, потрібно вивчити менше концепцій. Як ці переваги можна порівняти зі значними обмеженнями - суб'єктивний питання. Однак, не можна посперечатися, що Haskell складніше вивчити, ніж Go, і що система типів в Haskell набагато могутніше, і що Haskell може запобігти набагато більше типів багів при компіляції.


Go і Haskell настільки різні мови, що їх угрупування в один клас "статичних мов" може вводити в оману, не дивлячись на те, що термін використовується коректно. Якщо порівнювати практичні переваги безпеки, то Go ближче до динамічних мов, ніж до Haskell "у.


З іншого боку, деякі динамічні мови безпечніше, ніж деякі статичні мови. (Python в цілому вважається набагато безпечніше, ніж Сі). Коли хочеться робити узагальнення про статичних або динамічних мовах як групах, то не забувайте про величезну кількість відмінностей між мовами.

Конкретні приклади відмінності в можливостях систем типізації

У більш потужних системах типізації можна вказати обмеження на більш дрібних рівнях. Ось кілька прикладів, але не зациклюйтеся на них, якщо синтаксис незрозумілий.


У Go можна сказати "функція add приймає два integer" а й повертає integer ":


func add (x int, y int) int (return x + y)

У Haskell можна сказати "функція приймає будь-який чисельний тип і повертає число того ж типу ":


f :: Num a \u003d\u003e a -\u003e a -\u003e a add x y \u003d x + y

У Idris можна сказати "функція приймає два integer" а й повертає integer, але перший аргумент повинен бути менше другого аргументу ":


add: (x: Nat) -\u003e (y: Nat) -\u003e (auto smaller: LT x y) -\u003e Nat add x y \u003d x + y

Якщо спробувати викликати функцію add 2 1 де перший аргумент більше другого, то компілятор відхилить програму під час компіляції. Неможливо написати програму, де перший аргумент більше другого. Рідкісний мову має таку змогу. У більшості мов така перевірка відбувається при виконанні: ми б написали щось на зразок if x\u003e \u003d y: raise SomeError ().


У Haskell немає еквівалента такого типу як в прикладі з Idris вище, а в Go немає еквівалента ні прикладу з Haskell, ні прикладу з Idris. У підсумку, Idris може запобігти безлічі багів, які не зможе запобігти Haskell, а Haskell зможе запобігти безлічі багів, які не помітить Go. В обох випадках необхідні додаткові можливості системи типізації, які зроблять мову більш складним.

Системи типізації деяких статичних мов

Ось грубий список систем типізації деяких мов в порядку зростання потужності. Цей список дасть вам загальне уявлення про потужність систем, не потрібно ставитися до нього як до абсолютної правди. Зібрані в одну групу мови можуть сильно відрізнятися один від одного. У кожній системі типізації є свої заморочки, і більшість з них дуже складні.

  • C (1972), Go (2009): Ці системи зовсім потужні, без підтримки узагальнених типів. Неможливо задати тип MyList, який би означав "список цілих чисел", "список рядків" і т.д. Замість цього доведеться робити "список непозначених значень". Програміст повинен вручну повідомляти "це список рядків" кожен раз, коли рядок витягується зі списку, і це може привести до помилки при виконанні.
  • Java (1995), C # (2000): Обидві мови підтримують узагальнені типи, так що можна сказати MyList і отримати список рядків, про який компілятор знає і може стежити за дотриманням правил типів. Елементи зі списку будуть володіти типом String, компілятор буде форсувати правила при компіляції як зазвичай, так що помилки при виконанні менш вірогідні.
  • Haskell (1990), Rust (2010), Swift (2014 року): Всі ці мови мають декілька просунутими можливостями, в тому числі узагальненими типами, алгебраїчними типами даних (ADTs), і класами типів або чимось схожим (типи класів, ознаки (traits) і протоколи, відповідно). Rust і Swift більш популярні, ніж Haskell, і їх просувають великі організації (Mozilla і Apple, відповідно).
  • Agda (2007), Idris (2011): Ці мови підтримують залежні типи, дозволяючи створювати типи на кшталт "функція, яка приймає два цілих числа х і y, де y більше, ніж x". Навіть обмеження "y більше, ніж x" форсується при компіляції. При виконанні y ніколи не буде менше або дорівнює x, що б не трапилося. Дуже тонкі, але важливі властивості системи можуть бути перевірені статично в цих мовах. Їх вивчає дуже мало програмістів, але ці мови викликають у них величезний ентузіазм.

Спостерігається явне рух в бік більш потужних систем типізації, особливо якщо судити по популярності мов, а не по простому факту існування мов. Відомий виняток - це Go, який пояснює, чому багато прихильників статичних мов вважають його кроком назад.


Група два (Java і C #) - це мейнстрімовим мови, зрілі і широко використовуються.


Група три знаходиться на порозі входу в мейнстрім, з великою підтримкою з боку Mozilla (Rust) і Apple (Swift).


Група чотири (Idris and Agda) далекі від мейнстріму, але це може змінитися з часом. Мови групи три були далеко від мейнстріму ще десять років тому.

сувора типізація - один з варіантів політики роботи з типами даних, яка використовується в мовах програмування.

Сувора типізація має на увазі виконання наступних обов'язкових умов:

  1. Будь-який об'єкт даних (змінна, константа, вираз) в мові завжди має строго певний тип, який фіксується на момент компіляції програми (статична типізація) або визначається під час виконання (динамічна типізація).
  2. Допускається присвоювання змінної тільки значення, що має строго той же тип даних, що і змінна, ті ж обмеження діють щодо передачі параметрів і повернення результатів функцій.
  3. Кожна операція вимагає параметрів строго певних типів.
  4. Неявне перетворення типів не допускається (тобто транслятор сприймає будь-яку спробу використовувати значення не ту типу, який був описаний для змінної, параметра, функції або операції, як синтаксичну помилку).

При точному проходженні вимогам суворої типізації навіть однакові за складом значень і допустимим операціями типи даних є несумісними. Якщо в програмі необхідно присвоїти значення одного типу даних змінної іншого типу, це можна зробити, але тільки шляхом явного застосування спеціальної операції перетворення типу, яка в таких випадках зазвичай є частиною мови програмування (хоча може формально і не бути нею, а надаватися стандартними бібліотеками) .

Єдиний практично використовуваний мову програмування з суворою типізацією - це Ада. Досить велике число поширених мов програмування використовують нестрогую статичну типізацію. До таких мов відносяться, наприклад Pascal, Модула-2, Java. У них обов'язково опис типів змінних, параметрів і функцій, але допускається неявне приведення типів - в разі, якщо значення одного типу присвоюється змінної іншого, то компілятор автоматично генерує код для перетворення значення в потрібний тип, якщо тільки таке перетворення не призводить до втрати даних. Так, наприклад, ціле число можна привласнювати змінної, оголошеної як число з плаваючою точкою, а зворотне прісваіваеніе без явного приведення типів заборонено, оскільки з високою ймовірністю призведе до помилки. Деякі мови, формально мають поняття типу даних, в дійсності можна вважати нетипізований. До таких мов відноситься класичний Сі, в якому, хоча оголошення типів і потрібно, в дійсності всі типи даних є сумісними з присвоєння (сучасні компілятори Сі обмежують цю свободу і видають, щонайменше, попередження при небезпечних перетвореннях типів).

В теорії програмування сувора типізація є неодмінним елементом забезпечення надійності розроблюваних програмних засобів. При правильному застосуванні (подразумевающем, що в програмі оголошуються і використовуються окремі типи даних для логічно несумісних значень) вона захищає програміста від простих, але труднообнаружіваемих помилок, пов'язаних зі спільним використанням логічно несумісних значень, що виникають іноді просто через елементарну описки. Подібні помилки виявляються ще на етапі компіляції програми, тоді як при можливості неявного приведення практично будь-яких типів один до одного (як, наприклад, в класичному мовою Сі) ці помилки виявляються тільки при тестуванні, причому не всі і не відразу. З іншого боку, багато професійні програмісти не люблять строгу типізацію через її незручності - вона збільшує обсяг програми і час її написання, вимагає більш ретельного опрацювання коду, що багатьом здається зайвим.

Ця стаття містить необхідний мінімум тих речей, які просто необхідно знати про типізації, щоб не називати динамічну типізацію злом, Lisp - безтиповою мовою, а C - мовою зі строгою типізацією.

У повній версії знаходиться докладний опис всіх видів типізації, приправлене прикладами коду, посиланнями на популярні мови програмування і показовими картинками.

Рекомендую прочитати спочатку коротку версію статті, а потім при наявності бажання і повну.

Коротка версія

Мови програмування по типізації прийнято ділити на два великих табори - типізовані і нетипізовані (безтипові). До першого наприклад відносяться C, Python, Scala, PHP і Lua, а до другого - мова асемблера, Forth і Brainfuck.

Так як «безтипових типізація» за своєю суттю - проста як пробка, далі вона ні на які інші види не ділиться. А ось типізовані мови розділяються ще на кілька пересічних категорій:

  • статична / динамічна типізація. Статична визначається тим, що кінцеві типи змінних і функцій встановлюються на етапі компіляції. Тобто вже компілятор на 100% впевнений, який тип де знаходиться. У динамічної типізації всі типи з'ясовуються вже під час виконання програми.

    приклади:
    Статична: C, Java, C #;
    Динамічна: Python, JavaScript, Ruby.

  • сильна / слабка типізація (також іноді говорять сувора / нестрогая). Сильна типізація виділяється тим, що мова не дозволяє змішувати в виразах різні типи і не виконує автоматичні неявні перетворення, наприклад не можна відняти з рядка безліч. Мови зі слабкою типізацією виконують безліч неявних перетворень автоматично, навіть якщо може відбутися втрата точності або перетворення неоднозначно.

    приклади:
    Сильна: Java, Python, Haskell, Lisp;
    Слабка: C, JavaScript, Visual Basic, PHP.

  • явна / неявна типізація. Явно-типізовані мови відрізняються тим, що тип нових змінних / функцій / їх аргументів потрібно задавати явно. Відповідно мови з неявній типизацией перекладають це завдання на компілятор / інтерпретатор.

    приклади:
    Явна: C ++, D, C #
    Неявна: PHP, Lua, JavaScript

Також потрібно зауважити, що всі ці категорії перетинаються, наприклад мову C має статичну слабку явну типізацію, а мова Python - динамічну сильну неявну.

Тим-не менше не буває мов зі статичної та динамічної типізації одночасно. Хоча забігаючи вперед скажу, що тут я брешу - вони дійсно існують, але про це пізніше.

Детальна версія

Якщо короткої версії Вам здалося недостатньо, добре. Не дарма ж я писав детальну? Головне, що в короткій версії просто неможливо було вмістити всю корисну і цікаву інформацію, а докладна буде можливо занадто довгою, щоб усі могли його прочитати, не напружуючись.

безтипових типізація

У безтипових мовах програмування - все суті вважаються просто послідовностями біт, різної довжини.

Безтипових типізація зазвичай властива низькорівневим (мова асемблера, Forth) і езотеричним (Brainfuck, HQ9, Piet) мов. Однак і у неї, поряд з недоліками, є деякі переваги.

переваги
  • Дозволяє писати на гранично низькому рівні, причому компілятор / інтерпретатор не заважатиме будь-якими перевірками типів. Ви вільні робити будь-які операції над будь-якими видами даних.
  • Одержуваний код зазвичай більш ефективний.
  • Прозорість інструкцій. При знанні мови зазвичай немає сумнівів, що з себе представляє той чи інший код.
недоліки
  • Складність. Часто виникає необхідність в поданні комплексних значень, таких як списки, рядки або структури. З цим можуть виникнути незручності.
  • Відсутність перевірок. Будь-які безглузді дії, наприклад віднімання покажчика на масив з символу будуть вважатися абсолютно нормальними, що загрожує так важко впіймати помилками.
  • Низький рівень абстракції. Робота з будь-яким складним типом даних нічим не відрізняється від роботи з числами, що звичайно буде створювати багато труднощів.
Сильна безтіповая типізація?
Так, таке існує. Наприклад в мові асемблера (для архітектури х86 / х86-64, інших не знаю) не можна ассембліровать програму, якщо ви спробуєте завантажити в регістр cx (16 біт) дані з регістра rax (64 біта).

Mov cx, eax; помилка часу ассемблирования

Так виходить, що в ассемлере все-таки є типізація? Я вважаю, що цих перевірок недостатньо. А Ваша думка, звичайно, залежить тільки від Вас.

Статична і динамічна типізації

Головне, що відрізняє статичну (static) типізацію від динамічної (dynamic) то, що всі перевірки типів виконуються на етапі компіляції, а не етапі виконання.

Деяким людям може здатися, що статична типізація занадто обмежена (насправді так і є, але від цього давно позбулися за допомогою деяких методик). Деяким же, що динамічно типізовані мови - це гра з вогнем, але які ж риси їх виділяють? Невже обидва види мають шанси на існування? Якщо немає, то чому багато як статично, так і динамічно типізованих мов?

Давайте розберемося.

Переваги статичної типізації
  • Перевірки типів відбуваються тільки один раз - на етапі компіляції. А це означає, що нам не потрібно буде постійно з'ясовувати, чи не намагаємося ми поділити число на рядок (і або видати помилку, або здійснити перетворення).
  • Швидкість виконання. З попереднього пункту ясно, що статично типізовані мови практично завжди швидше динамічно типізованих.
  • При деяких додаткових умовах, дозволяє виявляти потенційні помилки вже на етапі компіляції.
  • Прискорення розробки за підтримки IDE (відсівання варіантів, свідомо не підходять за типом).
Переваги динамічної типізації
  • Простота створення універсальних колекцій - куп всього і вся (рідко виникає така необхідність, але коли виникає динамічна типізація виручить).
  • Зручність опису узагальнених алгоритмів (наприклад сортування масиву, яка буде працювати не тільки на списку цілих чисел, але і на списку речових і навіть на списку рядків).
  • Легкість в освоєнні - мови з динамічною типізацією зазвичай дуже гарні для того, щоб почати програмувати.

узагальнене програмування
Добре, найважливіший аргумент за динамічну типізацію - зручність опису узагальнених алгоритмів. Давайте уявимо собі проблему - нам потрібна функція пошуку по кільком масивів (або списками) - по масиву цілих чисел, по масиву речових і масиву символів.

Як же ми будемо її вирішувати? Вирішимо її на 3-ех різних мовах: одному з динамічною типізацією і двох із статичною.

Алгоритм пошуку я візьму один з найпростіших - перебір. Функція буде отримувати шуканий елемент, сам масив (або список) і повертати індекс елемента, або, якщо елемент не знайдений - (-1).

Динамічне рішення (Python):
def find (required_element, list): for (index, element) in enumerate (list): if element \u003d\u003d required_element: return index return (-1)

Як бачите, все просто і ніяких проблем з тим, що список може містити хоч числа, хоч списки, хоч інші масиви немає. Дуже добре. Давайте підемо далі - вирішимо цю-ж задачу на Сі!

Статична рішення (Сі):
unsigned int find_int (int required_element, int array, unsigned int size) (for (unsigned int i \u003d 0; i< size; ++i) if (required_element == array[i]) return i; return (-1); } unsigned int find_float(float required_element, float array, unsigned int size) { for (unsigned int i = 0; i < size; ++i) if (required_element == array[i]) return i; return (-1); } unsigned int find_char(char required_element, char array, unsigned int size) { for (unsigned int i = 0; i < size; ++i) if (required_element == array[i]) return i; return (-1); }

Ну, кожна функція окремо схожа на версію з Python, але чому їх три? Невже статичну програмування програло?

І так і ні. Є кілька методик програмування, одну з яких ми зараз розглянемо. Вона називається узагальнене програмування і мова C ++ її непогано підтримує. Давайте подивимося на нову версію:

Статична рішення (узагальнене програмування, C ++):
template unsigned int find (T required_element, std :: vector array) (for (unsigned int i \u003d 0; i< array.size(); ++i) if (required_element == array[i]) return i; return (-1); }

Добре! Це виглядає не сильно складніше ніж версія на Python і при цьому не довелося багато писати. До того ж ми отримали реалізацію для всіх масивів, а не тільки для 3-ох, необхідних для вирішення задачі!

Ця версія схоже саме те, що потрібно - ми отримуємо одночасно плюси статичної типізації та деякі плюси динамічної.

Здорово, що це взагалі можливо, але може бути ще краще. По-перше узагальнене програмування може бути зручніше і красивіше (наприклад в мові Haskell). По-друге крім узагальненого програмування також можна застосувати поліморфізм (результат буде гірше), перевантаження функцій (аналогічно) або макроси.

Статика в динаміці
Також потрібно згадати, що багато статичні мови дозволяють використовувати динамічну типізацію, наприклад:
  • C # підтримує псевдо-тип dynamic.
  • F # підтримує синтаксичний цукор у вигляді оператора ?, на базі чого може бути реалізована імітація динамічної типізації.
  • Haskell - динамічна типізація забезпечується модулем Data.Dynamic.
  • Delphi - за допомогою спеціального типу Variant.
Також, деякі динамічно типізовані мови дозволяють скористатися перевагами статичної типізації:
  • Common Lisp - декларації типів.
  • Perl - з версії 5.6, досить обмежено.
Отже, йдемо далі?

Сильна і слабка типізації

Мови з сильною типізацією не дозволяють змішувати суті різних типів у виразах і не виконують ніяких автоматичних перетворень. Також їх називають «мови з строгою типізацією». Англійський термін для цього - strong typing.

Слабо типізовані мови, навпаки всіляко сприяють, щоб програміст змішував різні типи в одному вираженні, причому компілятор сам приведе все до єдиного типу. Також їх називають «мови з нестрогой типизацией». Англійський термін для цього - weak typing.

Слабку типізацію часто плутають з динамічної, що абсолютно невірно. Динамічно збірний мову може бути і слабо і сильно типізований.

Однак мало, хто надає значення строгості типізації. Часто заявляють, що якщо мова статично типізований, то Ви зможете відловити безліч потенційних помилок при компіляції. Вони Вам брешуть!

Мова при цьому повинен мати ще й сильну типізацію. І правда, якщо компілятор замість повідомлення про помилку буде просто додавати рядок до числа, або що ще гірше, відніме із одного масиву інший, який нам сенс, що все «перевірки» типів будуть на етапі компіляції? Правильно - слабка статична типізація ще гірше, ніж сильна динамічна! (Ну, це моя думка)

Так що-ж у слабкій типізації взагалі немає плюсів? Можливо так виглядає, проте не дивлячись на те, що я затятий прихильник сильної типізації, повинен погодитися, що у слабкої теж є переваги.

Хочете дізнатися які?

Переваги сильною типізації
  • Надійність - Ви отримаєте виключення або помилку компіляції, натомість неправильної поведінки.
  • Швидкість - замість прихованих перетворень, які можуть бути досить витратними, з сильною типізацією необхідно писати їх явно, що змушує програміста як мінімум знати, що ця ділянка коду може бути повільним.
  • Розуміння роботи програми - знову-таки, замість неявного приведення типів, програміст пише все сам, а значить приблизно розуміє, що порівняння рядка і числа відбувається не само-собою і не по-помахом чарівної палички.
  • Визначеність - коли ви пишете перетворення вручну ви точно знаєте, що ви перетворюєте і будь-що. Також ви завжди будете розуміти, що такі перетворення можуть привести до втрати точності і до невірних результатів.
Переваги слабкої типізації
  • Зручність використання змішаних виразів (наприклад з цілих і дійсних чисел).
  • Абстрагування від типізації та зосередження на завданні.
  • Стислість записи.
Гаразд, ми розібралися, виявляється у слабкій типізації теж є переваги! А чи є способи перенести плюси слабкою типізації в сильну?

Виявляється є і навіть два.

Неявне приведення типів, в однозначних ситуаціях і без втрат даних
Ух ... Досить довгий пункт. Давайте я буду далі скорочувати його до «обмежене неявне перетворення» Так що ж означає однозначна ситуація і втрати даних?

Однозначна ситуація, це перетворення або операція в якій сутність відразу зрозуміла. Ось наприклад додавання двох чисел - однозначна ситуація. А перетворення числа в масив - немає (можливо створиться масив з одного елемента, можливо масив, з такою довгою, заповнений елементами по-замовчуванню, а можливо число перетворюється в рядок, а потім в масив символів).

Втрата даних це ще простіше. Якщо ми перетворимо дійсне число 3.5 в ціле - ми втратимо частину даних (насправді ця операція ще й неоднозначна - як буде проводитися округлення? У більшу сторону? В меншу? Відкидання дробової частини?).

Перетворення в неоднозначних ситуаціях і перетворення з втратою даних - це дуже, дуже погано. Нічого гіршого цього в програмуванні немає.

Якщо ви мені не вірите, вивчіть мову PL / I або навіть просто пошукайте його специфікацію. У ньому є правила перетворення між УСІМА типами даних! Це просто пекло!

Гаразд, давайте згадаємо про обмежене неявне перетворення. Чи є такі мови? Так, наприклад в Pascal Ви можете перетворити ціле число в реальне, але не навпаки. Також схожі механізми є в C #, Groovy і Common Lisp.

Гаразд, я говорив, що є ще спосіб отримати пару плюсів слабкою типізації в сильному мовою. І так, він є і називається поліморфізм конструкторів.

Я поясню його на прикладі чудового мови Haskell.

Поліморфні конструктори з'явилися в результаті спостереження, що найчастіше безпечні неявні перетворення потрібні при використанні числових літералів.

Наприклад в вираженні pi + 1, не хочеться писати pi + 1.0 або pi + float (1). Хочеться написати просто pi + 1!

І це зроблено в Haskell, завдяки тому, що у литерала 1 немає конкретного типу. Це ні ціле, ні речовий, ні комплексне. Це ж просто число!

В результаті при написанні простий функції sum xy, перемножуються всі числа від x до y (з инкрементом в 1), ми отримуємо відразу кілька версій - sum для цілих, sum для речових, sum для раціональних, sum для комплексних чисел і навіть sum для всіх тих числових типів що Ви самі визначили.

Звичайно рятує цей прийом тільки при використанні змішаних виразів з числовими літералами, а це лише верхівка айсберга.

Таким чином можна сказати, що найкращим виходом буде балансування на межі, між сильною і слабкою типізацією. Але поки ідеальний баланс не тримає ні одна мова, тому я більше схиляюся до сильно типізованим мов (таким як Haskell, Java, C #, Python), а не до слабо типізованим (таким як C, JavaScript, Lua, PHP).

Явна і неявна типізації

Мова з явною типизацией передбачає, що програміст повинен вказувати типи всіх змінних і функцій, які оголошує. Англійський термін для цього - explicit typing.

Мова з неявній типизацией, навпаки, пропонує Вам забути про типах і перекласти завдання виведення типів на компілятор або інтерпретатор. Англійськи термін для цього - implicit typing.

По-початку можна вирішити, що неявна типізація рівносильна динамічної, а явна - статичної, але далі ми побачимо, що це не так.

Чи є плюси у кожного виду, і знову ж таки, чи є їх комбінації і чи є мови з підтримкою обох методів?

Переваги явною типізації
  • Наявність у кожної функції сигнатури (наприклад int add (int, int)) дозволяє без проблем визначити, що функція робить.
  • Програміст відразу записує, якого типу значення можуть зберігатися в конкретній змінної, що знімає необхідність запам'ятовувати це.
Переваги неявній типізації
  • Скорочення записи - def add (x, y) явно коротше, ніж int add (int x, int y).
  • Стійкість до змін. Наприклад якщо в функції тимчасова змінна була того-ж типу, що і вхідний аргумент, то в явно типизированном мовою при зміні типу вхідного аргументу потрібно буде змінити ще й тип тимчасової змінної.
Добре, видно, що обидва підходи мають як плюси так і мінуси (а хто очікував чогось го ще?), Так давайте пошукаємо способи комбінування цих двох підходів!
Явна типізація по-вибору
Є мови, з неявній типизацией за замовчуванням і можливістю вказати тип значень при необхідності. Справжній тип вираження транслятор виведе автоматично. Один з таких мов - Haskell, давайте я наведу простий приклад, для наочності:
- Без явного вказівки типу add (x, y) \u003d x + y - Явна вказівку типу add :: (Integer, Integer) -\u003e Integer add (x, y) \u003d x + y

Примітка: я має намір використовував некаррірованную функцію, а також має намір записав приватну сигнатуру замість більш загальної add :: (Num a) \u003d\u003e a -\u003e a -\u003e a *, тому що хотів показати ідею, без пояснення синтаксису Haskell "а.

Щоб максимально просто пояснити дві абсолютно різні технології, почнемо спочатку. Перше, з чим стикається програміст при написанні коду - оголошення змінних. Ви можете помітити, що, наприклад, в мові програмування C ++ необхідно вказувати тип змінної. Тобто якщо ви оголошуєте змінну x, то обов'язково потрібно додати int - для зберігання цілочисельних даних, float - для зберігання даних з плаваючою точкою, char - для символьних даних, і інші доступні типи. Отже, в C ++ використовується статична типізація, так само як і в його попередника C.

Як працює статична типізація?

У момент оголошення змінної компілятор потрібно знати, які функції і параметри він може використовувати по відношенню до неї, а які ні. Тому програмісту необхідно відразу чітко позначити тип змінної. Зверніть увагу також на те, що в процесі виконання коду тип змінної змінити не можна. Зате можна створити свій власний тип даних і використовувати його в подальшому.

Розглянемо невеликий приклад. При ініціалізації змінної x (int x;) ми вказуємо ідентифікатор int - це скорочення від який зберігає тільки цілі числа в діапазоні від - 2 147 483 648 до 2 147 483 647. Таким чином, компілятор розуміє, що може виконувати над цієї змінної математичні значення - суму, різниця, множення і ділення. А ось, наприклад, функцію strcat (), яка з'єднує два значення типу char, застосувати до x не можна. Адже якщо зняти обмеження і спробувати з'єднати два значення int символьним методом, тоді станеться помилка.

Навіщо знадобилися мови з динамічною типізацією?

Незважаючи на деякі обмеження, статична типізація має ряд переваг, і не вносить великого дискомфорту в написання алгоритмів. Проте, для різних цілей можуть знадобитися і більш «вільні правила» щодо типів даних.

Вдалий приклад, який можна привести - JavaScript. Ця мова програмування зазвичай використовують для вбудовування в фреймворк з метою отримання функціонального доступу до об'єктів. Через таку особливість він набув великої популярності в web-технологіях, де ідеально почувається динамічна типізація. В рази спрощується написання невеликих скриптів і макросів. А також з'являється перевага в повторному використанні змінних. Але таку можливість використовують досить рідко, через можливі плутанини і помилок.

Який вид типізації краще?

Суперечки про те, що динамічна типізація краще, ніж сувора, не припиняються і до цього дня. Зазвичай вони виникають у вузькоспеціалізованих програмістів. Безумовно, веб-розробники повсякденно використовують всі переваги динамічної типізації для створення якісного коду і підсумкового програмного продукту. У той же час системні програмісти, які розробляють складні алгоритми на низькорівневих мовах програмування, як правило, не потребують таких можливостях, тому їм цілком вистачає статичної типізації. Бувають, звичайно, винятки з правил. Наприклад, повністю реалізована динамічна типізація в Python.

Тому визначати лідерство тієї чи іншої технології, потрібно виходячи тільки з вхідних параметрів. Для розробки легких і гнучких фреймворків краще підійде динамічна типізація, в той час як для створення масивної і складної архітектури краще використовувати строгу типізацію.

Поділ на «сильну» і «слабку» типізацію

Серед як російськомовних, так і англомовних матеріалів з програмування можна зустріти вираз - «сильна» типізація. Це не окреме поняття, а точніше такого поняття в професійному лексиконі взагалі не існує. Хоча багато хто намагається його по-різному інтерпретувати. Насправді, «сильну» типізацію слід розуміти як ту, яка зручна саме для вас і з якої максимально комфортно працювати. А «слабка» - незручна і неефективна для вас система.

особливість динаміки

Напевно ви помічали, що на стадії написання коду компілятор аналізує написані конструкції і видасть помилку при розбіжності типів даних. Але тільки не JavaScript. Його унікальність в тому, що він в будь-якому випадку зробить операцію. Ось легкий приклад - ми хочемо скласти символ і число, що не має сенсу: «x» + 1.

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

У динамічних мовах цю операцію виконати можна, але в більшості випадків піде помилка вже на стадії виконання коду, так як компілятор не аналізує в реальному часі типи даних і не може приймати рішення про помилки в цій галузі. JavaScript унікальний тим, що виконає таку операцію і отримає набір незрозумілими символами. На відміну від інших мов, які просто завершать роботу програми.

Чи можливі суміжні архітектури?

На даний момент ніякої суміжній технології, яка могла б одночасно підтримувати статичну і динамічну типізацію в мовах програмування, не існує. І можна впевнено сказати, що не з'явиться. Так як архітектури відрізняються один від одного в фундаментальних поняттях і не можуть використовуватися одночасно.

Але, тим не менш, в деяких мовах можна поміняти типізацію за допомогою додаткових фреймворків.

  • У мові програмування Delphi - підсистема Variant.
  • У мові програмування AliceML - додаткові пакети.
  • У мові програмування Haskell - бібліотека Data.Dynamic.

Коли сувора типізація дійсно краще динамічної?

Однозначно затвердити перевагу суворої типізації над динамічної можна тільки в тому випадку, якщо ви програміст. У цьому сходяться абсолютно всі IT-фахівці. При навчанні фундаментальним і базовим навичкам програмування краще використовувати строгу типізацію, щоб придбати якусь дисципліну при роботі зі змінними. Потім, при необхідності, можна перейти на динаміку, але навички роботи, придбані з суворою типізацією, зіграють свою важливу роль. Ви навчитеся ретельно перевіряти змінні і враховувати їх типи, при проектуванні і написанні коду.

Переваги динамічної типізації

  • Зводить до мінімуму кількість символів і рядків коду через непотрібність попереднього оголошення змінних і зазначення їх типу. Тип буде визначено автоматично, після присвоєння значення.
  • У невеликих блоках коду спрощується візуальне і логічне сприйняття конструкцій, через відсутність «зайвих» рядків оголошення.
  • Динаміка позитивно впливає на швидкість роботи компілятора, так як він не враховує типи, і не перевіряє їх на відповідність.
  • Підвищує гнучкість і дозволяє створювати універсальні конструкції. Наприклад, при створенні методу, який повинен взаємодіяти з масивом даних, не потрібно створювати окремі функції для роботи з числовими, текстовими та іншими типами масивів. Достатньо написати один метод, і він буде працювати з будь-якими типами.
  • Спрощує виведення даних з систем управління базами даних, тому динамічну типізацію активно використовують при розробці веб-додатків.

Детальніше про мови програмування зі статичної типізацією

  • C ++ - найбільш поширена мова програмування загального призначення. На сьогоднішній день має кілька великих редакцій і велику армію користувачів. Став популярним завдяки своїй гнучкості, можливості безмежного розширення і підтримки різних парадигм програмування.

  • Java - мова програмування, який використовує об'єктно-орієнтований підхід. Набув поширення завдяки мультіплатформенності. При компіляції код інтерпретується в байт-код, який може виконуватися на будь-якій операційній системі. Java і динамічна типізація несумісні, так як мова строго типізований.

  • Haskell - також один з популярних мов, код якого може інтегруватися в інші мови і взаємодіяти разом з ними. Але, незважаючи на таку гнучкість, має строгу типізацію. Оснащений великим вбудованим набором типів і можливістю створення власних.

Детальніше про мови програмування з динамічним видом типізації

  • Python - це мова програмування, який створювався насамперед для полегшення роботи програміста. Має ряд функціональних поліпшень, завдяки яким збільшує читабельність коду і його написання. Багато в чому цього вдалося домогтися завдяки динамічної типізації.

  • PHP - мова для створення скриптів. Повсюдно застосовується в веб-розробці, забезпечуючи взаємодію з базами даних, для створення інтерактивних динамічних веб-сторінок. Завдяки динамічній типізації істотно полегшується роботи з базами даних.

  • JavaScript - вже згадуваний вище мову програмування, який знайшов застосування в веб-технологіях для створення веб-сценаріїв, що виконуються на стороні клієнта. Динамічна типізація використовується для полегшення написання коду, адже зазвичай він розбивається на невеликі блоки.

Динамічний вид типізації - недоліки

  • Якщо була допущена помилка або груба помилка при використанні або оголошенні змінних, то компілятор відобразить її. А проблеми виникнуть при виконанні програми.
  • При використанні статичної типізації всі оголошення змінних і функцій зазвичай виносяться в окремий файл, який дозволяє в подальшому з легкістю створити документацію або взагалі використовувати сам файл як документацію. Відповідно, динамічна типізація не дозволяє використовувати таку особливість.

Підведемо підсумок

Статична і динамічна типізації використовуються для абсолютно різних цілей. У деяких випадках розробники переслідують функціональні переваги, а в деяких - чисто особисті мотиви. У будь-якому випадку щоб визначитися з видом типізації для себе, необхідно ретельно вивчити їх на практиці. Надалі при створенні нового проекту і вибору типізації для нього це зіграє велику роль і дасть розуміння ефективного вибору.



Сподобалася стаття? поділіться їй