Тази публикация е обединена с по-дълга: Спектърът на езиците за програмиране

Почти всеки практически език за програмиране има система от типове, която определя как да се присвояват типове на различни конструкции в езика и как конструкциите от тези типове взаимодействат помежду си. Повечето програмисти характеризират типовите системи с два набора от свойства. Единият е свързан с това кога се прилагат правилата на системата от типове (известна още като проверка на типа): динамични или статични; Другото е свързано с това колко гаранция за безопасност предоставя типовата система: силна или слаба.

Това е объркваща тема. Попаднах на статии на множество иначе уважавани уебсайтове, които твърдят, че статичното въвеждане означава, че променливите трябва да бъдат декларирани преди употреба, а динамичното въвеждане означава друго. Това е много подвеждащо. Haskell е статично типизиран език, но програмистите не трябва да декларират типа на всяко име (Haskell няма променливи, чиито стойности „варират“, така че избягвам да използвам термина тук). Типовете се извеждат от това как се използват имената. Незадължителната анотация на типа е най-вече в полза на хората, които четат, а не на компилатора. Виждал съм също статии, които приравняват динамичното със слабо и статичното със силно. Това също е грешно. Един динамично типизиран език може да бъде по-строго типизиран от статично типизиран език.

За да разберем разграничението, трябва да разделим имената и стойностите. Името е идентификаторът, който използвате в програма за препратка към обекти като обекти и функции. Ценностите са самите субекти. Едно име може да препраща към различни стойности по различно време, а една стойност може да бъде препращана с различни имена.

В статично типизираните езици имената имат типове и типът на името обикновено не може да се променя в своя обхват. Име от определен тип може да се отнася само до стойности от този тип. В израз като a = sum(b, c), a, b, c и sum всички имат типове. Компилаторът проверява дали типовете a и b съвпадат с типовете параметри sum и дали типът резултат sum съвпада с типа a. Ако не, той издава грешка при типа и отказва да компилира програмата.

В динамично типизираните езици самите имена нямат типове, но стойностите, към които се отнасят, имат. В последния пример a = sum(b, c), когато програмата се изпълнява, времето за изпълнение на езика разглежда стойностите b и c и се уверява, че са от типа, към който sum може да се приложи. Например, sum(b, c) може да бъде имплементирано като b + c, времето за изпълнение на езика проверява дали b и c се отнасят за типовете, за които е дефиниран операторът +. Ако не, хвърля изключение.

Нека се обърнем към силата на безопасността на типа, като разгледаме някои примери.

И C, и Rust са статично типизирани, но предоставят различни нива на безопасност на типа. Помислете за следната C програма:

#include <stdio.h>
int main() {
  int numbers[] = {0, 1, 2};
  printf("%d", numbers[6]);
  return 0;
}

Има очевиден проблем, но може да се компилира успешно. В зависимост от компилатора, който използвате, може да видите предупреждение, но това е просто евристика, добавена от компилатора, за да помогне на програмистите. Стандартът на езика C позволява програмата да бъде компилирана. Когато стартирам тази програма на моя Mac, тя отпечатва 32766%, което е просто безсмислието, което се е случило на това място в паметта. Ако това беше сложна програма, вероятно щеше да бъде разочароващ бъг.

Следната програма Rust се опитва да направи същото:

fn main() {
    let numbers = [0, 1, 2];
    println!("{}", numbers[5]);
}

Но когато се компилира, се издава следната грешка. Няма да има шанс да бяга.

error: index out of bounds: the len is 3 but the index is 5
 --> array.rs:3:20
  |
3 |     println!("{}", numbers[5]);

Това е така, защото в Rust дължината е част от типа на литерал на масив, така че [0, 1] и [0, 1, 2] всъщност са различни типове. В този случай компилаторът може да открие незаконен достъп до литерал на масив само като погледне типа. За да проверите това, добавете ред към кода на rust:

fn main() {
    let numbers = [0, 1, 2];
    let foo: () = numbers;  // <- add
    println!("{}", numbers[5]);
}

Ще видите следната грешка от компилатора:

error[E0308]: mismatched types
 --> array.rs:3:19
  |
3 |     let foo: () = numbers;
  |                   ^^^^^^^ expected (), found array of 3 elements
  |
  = note: expected type `()`
             found type `[{integer}; 3]`

Това е умишлена грешка при несъответствие на типа, която добавяме, за да покажем типа на numbers. Последният ред казва, че е [{integer}; 3], в който 3 е дължината на масива. За вектори с променлив размер Rust използва тип Result, за да принуди програмистите да проверяват за възможност за грешки извън границите.

Печално известните типови системи на Perl и PHP измъчваха безброй програмисти. Следното е копирано дословно от официалното ръководство за PHP:

$foo = 1 + "10.5";                // $foo is float (11.5)
$foo = 1 + "-1.3e3";              // $foo is float (-1299)
$foo = 1 + "bob-1.3e3";           // $foo is integer (1)
$foo = 1 + "bob3";                // $foo is integer (1)
$foo = 1 + "10 Small Pigs";       // $foo is integer (11)
$foo = 4 + "10.2 Little Piggies"; // $foo is float (14.2)
$foo = "10.0 pigs " + 1;          // $foo is float (11)
$foo = "10.0 pigs " + 1.0;        // $foo is float (11)

PHP и Perl са изключително снизходителни към несъответствията на типовете и отиват много, за да масират аргументите във всички необходими типове. За бързи и мръсни скриптове те могат да позволят на програмиста да свърши нещата с възможно най-малко код, но за големи проекти те са добри в заравянето на грешки. Повечето други широко използвани езици днес изискват изрично преобразуване между несвързани типове, независимо дали са динамично или статично въведени. Например в Clojure трябва да извикате Integer/parseInt, за да анализирате низ до цяло число.

Както статичната проверка на типа, така и силната система за тип помагат за разкриване на грешки възможно най-рано, обикновено за сметка на това, че правят програмите по-подробни, но това са различни неща.