для чего нужен тип данных
Немного о типах данных
Часто при обучении программированию возникают недопонимания типа: зачем в языке программирования нужно описывать типы данных, для чего это придумано и где у всего этого смысл.
Прежде чем рассмотреть, зачем нужен тип данных, сначала о том, что такое переменная. Вы присваиваете некоторому участку памяти, где может храниться информация имя – это имя и есть переменная. То есть, если ещё проще, то переменная – это имя некоторой записи в памяти компьютера. Хороший пример – сотовый телефон. Там, вы записи тоже часто храните под некоторыми именами. Например, под именем “магазин” вы можете хранить номер телефона магазина хоз. товаров. Имя “магазин” – переменная, а номер телефона – данные, что в ней хранятся. Вы можете отредактировать запись “магазин” сменив там номер телефона на другой, например, на номер телефона зоомагазина. И вот в той же самой переменной с тем же самым именем теперь уже лежит другая информация – раньше под именем “магазин” скрывался номер телефона хоз. товаров, а теперь там номер телефона зоомагазина. Итак, переменная – это имя, которое вы назначаете некоторому участку памяти, где может храниться разная информация.
Теперь о типе данных. Прежде чем присвоить какому-либо участку памяти имя, компьютеру необходимо «объяснить», данные какого типа будут храниться под эти именем. То есть когда создаёте переменную, то вы не только должны назначить ей некое имя, но также должны указать какого типа данные могут храниться под данным именем. Например, в языке c++ если вы создаёте переменную с именем shop таким образом:
int shop;
то это означает, что в участок памяти с именем shop вы можете записывать только целые числа (идентификатор типа int на это и указывает). Если потом запишете в программе:
то это будет означать, что в участке памяти под именем shop теперь хранится число 4. А вот если вы запишете:
Что такое типы данных и зачем они нужны?
Пожалуйста, приостановите работу AdBlock на этом сайте.
Предполагается, что Вы всё ещё помните с прошлого шага, что такое переменная. Если не помните, вернитесь и посмотрите. А мы продолжаем.
Наверное, вам известно, что в памяти компьютера все числа хранятся в виде последовательности 0 и 1. Так вот, для того чтобы записать любое число только ноликами и единичками существуют определённые правила. Эти правила довольно сильно отличаются для целых чисел и для вещественных чисел. В памяти компьютера числа «3» и «3.0» будут записаны совершенно по-разному. Для тех, кто заинтересовался представлением чисел в памяти компьютера, в конце урока я дам ссылки на дополнительные материалы по этой теме.
Кроме того, зачастую компьютеры гораздо быстрее и лучше могут производить различные действия с целыми числами, чем с вещественными. Если вы не забросите этот курс, то на одном из следующих уроков мы в этом убедимся. Раньше, когда компьютеры занимали целую комнату, это была безусловная истина, а теперь этот факт уже нужно проверять.
Подведём итог. Каждая переменная должна иметь конкретный тип данных. И в эту переменную можно сохранить данные только этого типа.
Базовые типы данных в языке Си.
Ниже выписаны некоторые (не все!) базовые типы данных, которые есть в языке Си, и которые вы будете использовать в ваших программах чаще всего.
Для вещественных чисел есть ещё тип float. Он в некотором смысле хуже типа double. Две основные причины: у типа float меньше диапазон хранимых чисел, меньше точность (примерно 7 знаков после запятой, против 15 у типа double). Про точность у нас ещё будет пример в третьем уроке.
Есть и другие типы данных, но мы пока о них говорить не будем, чтобы не забивать свою память различными нюансами, которые на начальном этапе не так уж важны.
Важно другое. Необходимо понимать, что тип данных для переменной нужно выбирать в зависимости от того, что мы собираемся в этой переменной хранить. Например, если бы мы считали количество посещений какой-то страницы на сайте, то для этого мы использовали переменную типа int, а вот для хранения курса доллара целые числа уже бы не подошли. Думаю, у банка возникли бы серьёзные проблемы после использования такой программы.
Практика
Решите предложенные задачи. Для удобства работы сразу переходите в полноэкранный режим
Исследовательские задачи для хакеров:
Структура языка программирования
Содержание
Дополнительно
Классификация типов данных
Для простых типов данных определяются границы диапазона и количество байт, занимаемых ими в памяти компьютера.
В большинстве языков программирования, простые типы жестко связаны с их представлением в памяти компьютера. Компьютер хранит данные в виде последовательности битов, каждый из которых может иметь значение 0 и 1. Фрагмент данных в памяти может выглядеть следующим образом
Данные на битовом уровне (в памяти) не имеют ни структуры, ни смысла. Как интерпретировать данные, как целочисленное число, или вещественное, или символ, зависит от того, какой тип имеют данные, представленные в этой и последующих ячейках памяти.
Числовые типы данных
Целочисленные типы данных
Исходя из машинного представления целого числа, в ячейке памяти из n бит может хранится 2 n для беззнаковых, и 2 n-1 для знаковых типов.
Рассмотрим теперь конкретные целочисленные типы в трёх языках.
У некоторых типов есть приписка «16 разрядов» или «32 разряда». Это означает, что в зависимости от разрядности операционной системы и компилятора данный тип будет находится в соответствующем диапазоне. По-этому, рекомендуется не использовать int, unsigned int, а использовать их аналоги, но уже жестко определенные, short, long, unsigned short, unsigned long.
В Java нет беззнаковых целочисленных типов данных.
Вещественные типы данных
Числа вещественного типа данных задаются в форме чисел с плавающей запятой.
Плавающая запятая — форма представления действительных чисел, в которой число хранится в форме мантиссы и показателя степени. В случае языков программирования, любое число может быть представлено в следующем виде
Вывод: вещественные типы данных, в отличии от целочисленных, характеризуются диапазоном точности и количеством значащих разрядов.
Рассмотрим конкретные типы данных в наших трёх языках.
Тип decimal создан специально для операций высокой точности, в частности финансовых операций. Он не реализован как примитивный тип, по-этому его частое использование может повлиять на производительность вычислений.
Символьный тип данных
Значение переменной этого типа данных представляет собой один символ. В действительности, это есть целое число. В зависимости от кодировки, это число превращается в некий символ. Данные типы данных характеризуются лишь размером выделяемой под них памяти.
Логический тип данных
Перечислимый тип данных
Во внутреннем представлении, это целочисленный тип данных, только здесь пользователь вместо числе использует заранее определенные строковые значения.
Чтобы прочувствовать эту концепцию, приведем пример на языке С++ (в С# и Java аналогично)
Теперь переменные перечислимого типа Forms могут принимать лишь значения, определенные в примере кода. Это очень удобно, ведь мы уже оперируем не с числами, а с некими смысловыми значениями, замечу лишь, что для компьютера эти значения всё-равно являются целыми числами.
Массив
Каждый массив характеризуется типом данных его элементов, который может быть как простым, так и сложным, то есть любым.
В языках программирования нельзя оперировать всем массивом, работают с конкретным элементом. Чтобы доступиться до него в трёх рассматриваемых нами языках используют оператор «[]».
Структура
Структуры реализованы в языке программирования, чтобы собрать некие близки по смыслу вещи воедино.
Например, есть колесо автомобиля. У колеса есть диаметр, толщина, шина. Шина в свою очередь является структурой, у которой есть свои параметры: материал, марка, чем заполнена. Естественно, для каждого параметра можно создать свою переменную или константу, у нас появится большое количество переменных, которые, чтобы понять к чему они относятся, нужно в именах общую часть выделять. Имена будут нести лишнюю смысловую нагрузку. Получается запутанная история. А так мы определяем две структуры, а затем параметры в них.
Класс
Еще одним пользовательским типом данных является класс. Класс умеет всё, что и структура, но кроме параметров, у него есть и методы, и поддерживает большое количество вещей, связанных с объектно-ориентированным программированием.
Типы данных — Java: Основы
Зачем нужны типы данных
Внутри высокоуровневых языков программирования данные разделяются по типам. Например, строки относятся к типу String, а числа — к типу int. Зачем нужны типы? Для защиты программы от трудноотловимых ошибок. Типы определяют две вещи:
Язык программирования распознает типы. Поэтому Java не позволит нам умножать строку на строку. Но позволит умножать целое число на другое целое число. Наличие типов и таких ограничений в языке защищает программы от случайных ошибок.
Каким образом Java понимает, что за тип данных перед ним? Достаточно просто. Любое значение где-то инициализируется и, в зависимости от способа инициализации, становится понятно, что перед нами. Например, число это просто число не обернутое в парные символы, например, кавычки. А вот строки всегда ограничены двойными кавычками. Например, такое значение «234» – строка, несмотря на то, что внутри нее записаны цифры.
По-английски строки в программировании называются «strings», а строчки текстовых файлов называются «lines». Например, в коде выше одна строчка (lines), и нет никаких строк (strings). В русском иногда может быть путаница, поэтому во всех уроках мы будем говорить *строка** для обозначения типа данных «строка», и строчка для обозначения строчек (lines) в файлах.*
Типов данных в Java много, плюс можно создавать свои. Постепенно мы познакомимся со всеми необходимыми и научимся их правильно использовать.
Явная типизация
Вывод типов в Java появился в 2018 году, но существуют языки, в которых вывод типов существует не один десяток лет. Первый язык с выводом типов называется ML и появился он аж в 1973 году. С тех пор вывод типов был добавлен в уйму языков среди которых Ocaml, Haskell, C#, F#, Kotlin, Scala и множество других. Вывод типов и предпочтителен в большинстве ситуаций, однако бывает такое, что выводимый тип нас не устраивает. Тогда мы можем указать тип явно.
Какие бывают типы
В этом уроке мы рассмотрим систему типов в Java с высоты птичьего полета, не погружаясь в детали. Но сначала ответим на вопрос, зачем вообще про них знать? В коде программ мы все время оперируем данными. Эти данные имеют разную природу, могут быть по разному организованы, что влияет и на удобство работы с ними и эффективность этой работы. Типы преследуют нас буквально на каждом шагу, поэтому без их изучения программирование на Java возможно только на очень базовом уровне.
С другой стороны не пытайтесь запомнить всю эту информацию про типы наизусть (иначе можно приуныть). Она дается лишь для общего представления. Все что надо знать про типы вы и так выучите в процессе программирования. Глобально, типы данных в Java деляться на две большие группы:
У этих групп есть различия, которые мы разберем позже, когда познакоимся с null и объектно-ориентированным программированием. Пока достаточно знания того, что имена примитивных типов начинаются с нижнего регистра (int), а ссылочных с верхнего (String).
Определение переменной y завершилось с ошибкой, потому что мы указали тип byte, но присвоили переменной значение 270, которое выходит за множество возможных значений.
Возникает закономерный вопрос. Зачем аж 4 типа для хранения чисел? Почему бы не сделать один, в который влезает почти любое большое число? Технически так сделать можно, но мы находимся в мире инженерных решений. Это значит, что у любого решения всегда есть обратная сторона, поэтому невозможно сделать идеально, придется чем-то пожертвовать. В данном случае, объемом занимаемой памяти. Если оставить только long, то программа активно оперирующая числами начнет занимать слишком много места в оперативной памяти, что может быть критично.
Такая же логика использовалась для типов float и double. Они оба отвечают за рациональные числа, разница лишь в том, что double это «двойной» float, то есть в памяти он занимает в два раза больше места. Создатели Java полагаются на разумность программистов. На их способность правильно подобрать нужные типы в зависимости от задачи. Для каких-то экстремальных приложений так и происходит, но в типичной разработке все просто. Программисты выбирают int для целых чисел и double для рациональных.
Особняком стоит тип char (символ). Символ это не строка, у него другой способ определения, через одиночные кавычки:
Строка состоящая из одного символа это не символ. С точки зрения здравого смысла кажется не логично, но, с точки зрения, типов все так и должно быть, со временем вы это прочувствуете.
Извлечение символа из строки извлекает как раз символ, а не строку состоящую из одного символа.
Хорошо, а где String спросите вы? String не является примитивным типом. Внутри она представляет из себя массив символов. Несмотря на это техническое различие, строки используются наравне с примитивными типами без особых отличий.
Значение null
Особняком в Java стоит значение null. Здесь оно, в отличии от других языков программирования, не является типом, это просто конкретное значение со специальным смыслом и логикой работы. Начнем с примера:
Явное преобразование типов
В программировании регулярно встречаются задачи, когда один тип данных нужно преобразовать в другой. Простейший пример – работа с формами на сайтах. Данные формы всегда приходят в текстовом виде, даже если значение число. Вот как его можно преобразовать:
Если нужно конвертировать из примитивного типа в примитивный, то все проще. Достаточно перед значением в скобках указать желаемый тип. В результате значение справа преобразуется в значение другого типа, указанного слева:
Преобразование типов можно использовать внутри составных выражений:
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты.
Типы и функции
Это третья статья в цикле «Теория категорий для программистов».
Категория типов и функций играет важную роль в программировании, так что давайте поговорим о том, что такое типы, и зачем они нам нужны.
Кому нужны типы?
В сообществе есть некоторое несогласие о преимуществах статической типизации против динамической и сильной типизации против слабой. Позвольте мне проиллюстрировать выбор типизации с помощью мысленного эксперимента. Представьте себе миллионы обезьян с клавиатурами, радостно жмущих случайные клавиши, которые пишут, компилируют и запускают программы.
С машинным языком, любая комбинация байтов производимая обезьянами будет принята и запущена. Но в высокоуровневых языках, высоко ценится то, что компилятор способен обнаружить лексические и грамматические ошибки. Многие программы будут просто отвергнуты, а обезьяны останутся без бананов, зато остальные будут иметь больше шансов быть осмысленными. Проверка типов обеспечивает еще один барьер против бессмысленных программ. Кроме того, в то время как в динамически типизированных языках несоответствия типов будут обнаружены только во время выполнения, в строго типизированных статически проверяемых языках несоответствия типов обнаруживаются во время компиляции, что отсеивает множество некорректных программ, прежде чем у них есть шанс быть запущенными.
Итак, вопрос в том, хотим ли мы, чтобы обезьяны были счастливы, или создавать корректные программы?
(прим. переводчика: не стоит оскорбляться, автор просто любит менее скучные метафоры, чем ГСЧ и «случайные последовательности байт», а не называет программистов обезьянами).
Обычно цель мысленного эксперимента с печатающими обезьянами — создание полного собрания сочинений Шекспира (прим. переводчика: или Война и Мир Толстого). Проверка орфографии и грамматики в цикле резко увеличит шансы на успех. Аналог проверки типов пойдет еще дальше: после того, как Ромео объявлен человеком, проверка типов убедится, что на нем не растут листья и что он не ловит фотоны своим мощным гравитационным полем.
Типы нужны для компонуемости
Теория категорий изучает композиции стрелок. Не любые две стрелки могут быть скомпонованы: целевой объект одной стрелки должен совпадать с исходным обьектом следующей. В программировании мы передаем результаты из одной функции в другую. Программа не будет работать, если вторая функция не может правильно интерпретировать данные, полученные с помощью первой. Обе функции должны подходить друг к другу, чтобы их композиция заработала. Чем сильнее система типов языка, тем лучше это подхождение можно описать и автоматически проверить.
Единственный серьезный аргумент, который я слышу против строгой статической типизации: она может отвергнуть некоторые программы, которые семантически верны. На практике это случается крайне редко (прим. переводчика: во избежания срача замечу, что тут автор не учел, или несогласен, что есть много стилей, и привычный программсистом на скриптовых языках duck-typing тоже имеет право на жизнь. С другой стороны, duck-typing возможен и в строгой системе типов через templates, traits, type classes, interfaces, много есть технологий, так что мнение автора нельзя считать строго неверным.) и, в любом случае, каждый язык содержит какой-то черный ход, чтобы обойти систему типов, когда это действительно необходимо. Даже Haskell имеет unsafeCoerce. Но такие конструкции должны использоваться разумно. Персонаж Франца Кафки, Грегор Замза, нарушает систему типов, когда он превращается в гигантского жука, и мы все знаем, как это кончилось (прим. переводчика: плохо 🙂.
Другой аргумент, который я часто слышу, в том, что строгая типизация накладывает слишком много нагрузки на программиста. Я могу сочувствовать этой проблеме, так как сам написал несколько обьявлений итераторов в С++, только вот есть технология, вывод типов, которая позволяет компилятору вывести большинство типов из контекста, в котором они используются. В С++, вы можете объявить переменную auto, и компилятор выведет тип за вас.
В Haskell, за исключением редких случаев, аннотации типа являются опциональными. Программисты, как правило, все равно их используют, потому что типы могут многое рассказать о семантике кода, и обьявления типов помогают понимать ошибки компиляции. Обычная практика в Haskell — начинать проект с разработки типов. Позже, аннотации типов являются основой для реализации и становятся гарантированными компилятором комментариями.
Строгая статическая типизация часто используется в качестве предлога для нетестирования кода. Иногда вы можете услышать, как Haskell-программисты говорят: «Если код собирается, он правильный.» Конечно, нет никакой гарантии, что программа, корректная с точки зрения типов, коректна в смысле правильного результата. В результате такого отношения в ряде исследований Haskell не стал сильно опережать остальные языки по качеству кода, как можно было бы ожидать. Кажется, что в коммерческих условиях необходимость чинить баги существует только до определенного уровня качества, что в основном связано с экономикой разработки программного обеспечения и толерантности конечного пользователя, и очень слабо связано с языком программирования или методологией разработки. Лучшим критерием было бы измерить, сколько проектов отстает от графика или поставляется с сильно сниженным функционалом.
Теперь, что касается утверждения, что модульное тестирование может заменить строгую типизацию. Рассмотрим общую практику рефакторинга в строго типизированных языках: изменение типа аргумента какой-либо функции. В сильно типизированных языках достаточно изменить декларацию этой функции, а затем исправить все ошибки сборки. В слабо типизированных языках, тот факт, что функция в теперь ожидает другие данные не может быть связан с вызывающей стороной.
Модульное тестирование может поймать некоторые из несоответствий, но тестирование практически всегда вероятностный, а не детерминированный процесс (прим. переводчика: возможно, имелся ввиду набор тестов: вы покрываете не все возможные входы, а некую репрезентативную выборку.) Тестирование — плохая замена доказательству корректности.
Что такое типы?
Простейшее описание типов: они представляют собой множества значений. Типу Bool (помните, конкретные типы начинаются с заглавной буквы в Haskell) соответствует множество из двух элементов: True и False. Тип Char — множество всех символов Unicode, например ‘a’ или ‘ą’.
Множества могут быть конечными или бесконечными. Тип String, который, по сути, синонимом списка Char, — пример бесконечного множества.
Когда мы обьявляем x, как Integer:
мы говорим, что это элемент множества целых чисел. Integer в Haskell — бесконечное множество, и может быть использовано для арифметики любой точности. Есть и конечное множество Int, которое соответствует машинному типу, как int в C++.
Есть некоторые тонкости, которые делают приравнивание типов к множествам сложным. Есть проблемы с полиморфными функциями, которые имеют цикличные определения, а также с тем, что вы не можете иметь множество всех множеств; но, как я и обещал, я не буду строгим математиком. Важно то, что есть категория множеств, которая называется Set, и мы с ней будем работать.
В Set, объекты — это множества, а морфизмы (стрелки) — функции.
Set — особая категория, потому что мы можем заглянуть внутрь ее объектов и это поможет многое интуитивно понять. Например, мы знаем, что пустое множество не имеет элементов. Мы знаем, что существуют специальные множества из одного элемента. Мы знаем, что функции отображают элементы одного множества в элементы другого. Они могут отображать два элемента в один, но не один элемент в два. Мы знаем, что тождественная функция отображает каждый элемент множества в себя, и так далее. Я планирую постепенно забывать всю эту информацию и вместо этого выразить все эти понятия в чисто категорийной форме, то есть в терминах объектов и стрелок.
В идеальном мире мы могли бы просто сказать, что типы в Haskell — множества, а функции в Haskell — математические функции между ними. Существует только одна маленькая проблема: математическая функция не выполняет какой-либо код — она знает только ответ. Функция в Haskell должна ответ вычислять. Это не проблема, если ответ может быть получен за конечное число шагов, каким бы большим оно ни было. Но есть некоторые вычисления, которые включают рекурсию, и те могут никогда не завершиться. Мы не можем просто запретить незавершающиется функции в Haskell потому, что различить, завершается функция, или нет — знаменитая проблема остановки — неразрешима. Вот почему ученые-компьютерщики придумали гениальную идею, или грязный хак, в зависимости от вашей точки зрения, — расширить каждый тип специальным значением, называнным bottom (прим. переводчика: этот термин (bottom) слышится как-то по-дурацки на русском, если кто знает хороший вариант, пожалуйста, предлагайте.), которое обозначается _|_ или в Unicode ⊥. Это «значение» соответствует незавершающемуся вычислению. Так функция, объявленная как:
может вернуть True, False, или _|_; последнее значит, что функция никогда не завершается.
Интересно, что, как только вы принимаете bottom в систему типов, удобно рассматривать каждую ошибку времени исполнения за bottom, и даже позволить функции возвращать bottom явно. Последнее, как правило, осуществляется с помощью выражения undefined:
Это определение проходит проверку типов потому, что undefined вычисляется в bottom, которое включено во все типы, в том числе и Bool. Можно даже написать:
Функции, которые могут возвращать bottom, называются частичными, в отличие от обычных функций, которые возвращают правильные результаты для всех возможных аргументов.
Зачем нам математическая модель?
Как программист, вы хорошо знакомы с синтаксисом и грамматикой языка программирования. Эти аспекты языка, как правило, формально описываются в самом начале спецификации языка. Но смысл и семантику языка гораздо труднее описать; это описание занимает намного больше страниц, редко достаточно формально, и почти никогда не полно. Отсюда никогда не заканчивающиеся дискуссии среди языковых юристов, и вся кустарная промышленность книг, посвященных толкованию тонкостей языковых стандартов.
Есть формальные средства для описания семантики языка, но из-за их сложности они в основном используются для упрощенных, академических языков, а не реальных гигантов промышленного программирования. Один из таких инструментов называется операционная семантика и описывает механику исполнения программы. Он определяет формализованный, идеализированный интерпретатор. Семантика промышленных языков, таких как C++, как правило, описывается с помощью неформального рассуждения, часто в терминах «абстрактной машины».
Проблема в том, что что о программах, использующих операционную семантику, очень трудно что-то доказать. Чтобы показать некое свойство программы вы, по сути, должны «запустить ее» через идеализированный интерпретатор.
Не важно, что программисты никогда формально не доказывают корректность. Мы всегда «думаем», что мы пишем правильные программы. Никто не сидит за клавиатурой, говоря: «О, я просто напишу несколько строк кода и посмотрю, что происходит.» (прим. переводчика: ах, если бы. ) Мы считаем, что код, который мы пишем, будет выполнять определенные действия, которые произведут желаемые результаты. Мы, как правило, очень удивлены, если это не так. Это означает, что мы действительно думаем о программах, которые мы пишем, и мы, как правило, делаем это, запуская интерпретатор в наших головах. Просто, очень трудно уследить за всеми переменными. Компьютеры хороши для исполнения программ, люди — нет! Если бы мы были, нам бы не понадобились компьютеры.
Но есть и альтернатива. Она называется денотационной семантикой и основана на математике. В денотационной семантике для каждой языковой конструкции описана математичесая интерпретация. Таким образом, если вы хотите доказать свойство программы, вы просто доказываете математическую теорему. Вы думаете, что доказывание теорем трудно, но на самом деле мы, люди, строили математические методы тысячи лет, так что есть множество накопленных знаний, которые можно использовать. Кроме того, по сравнению с теоремами, которые доказывают профессиональные математики, задачи, с которыми мы сталкиваемся в программировании, как правило, довольно просты, если не тривиальны. (прим. переводчика: для доказательства, автор не пытается обидеть программистов.)
Рассмотрим определение функции факториала в Haskell, языке, легко поддающемуся денотационной семантике:
Выражение [1..n] — это список целых чисел от 1 до n. Функция product умножает все элементы списка. Точно так, как определение факториала, взятое из учебника. Сравните это с C:
Нужно ли продолжать? (прим. переводчика: автор слегка схитрил, взяв библиотечную функцию в Haskell. На самом деле, хитрить было не нужно, честное описание по определению не сложнее):
Хорошо, я сразу признаю, что это был дешевый прием! Факториал имеет очевидное математическое определение. Проницательный читатель может спросить: Какова математическая модель для чтения символа с клавиатуры, или отправки пакета по сети? Долгое время это был бы неловкий вопрос, ведущий к довольно запутанным объяснениям. Казалось, денотационная семантика не подходит для значительного числа важных задач, которые необходимы для написания полезных программ, и которые могут быть легко решаемы операционной семантикой. Прорыв произошел из теории категорий. Еугенио Моджи обнаружил, что вычислительные эффекты могут быть преобразованы в монады. Это оказалось важным наблюдением, которое не только дало денотационной семантике новую жизнь и сделало чисто функциональные программы более удобными, но и дало новую информацию о традиционном программировании. Я буду говорить о монадах позже, когда мы разработаем больше категорийных инструментов.
Одним из важных преимуществ наличия математической модели для программирования является возможность выполнить формальное доказательство корректности программного обеспечения. Это может показаться не столь важным, когда вы пишете потребительский софт, но есть области программирования, где цена сбоя может быть огромной, или там, где человеческая жизнь находится под угрозой. Но даже при написании веб-приложений для системы здравоохранения, вы можете оценить ту мысль, что функции и алгоритмы из стандартной библиотеки языка Haskell идут в комплекте с доказательствами корректности.
Чистые и Грязные функции
То, что мы называем функциями в C++ или любом другом императивном языке, не то же самое, что математики называют функциями. Математическая функция — просто отображение значений в значения.
Мы можем реализовать математическую функцию на языке программирования: такая функция, имея входное значение будет рассчитать выходное значение. Функция для получения квадрата числа, вероятно, умножит входное значение само на себя. Она будет делать это при каждом вызове, и гарантированно произведет одинаковый результат каждый раз, когда она вызывается с одним и тем же аргументом. Квадрат числа не меняется с фазами Луны.
Кроме того, вычисление квадрата числа не должно иметь побочного эффекта, вроде выдачи вкусного ништячка вашей собаке. «Функция», которая это делает, не может быть легко смоделирована математической функцей.
В языках программирования функции, которые всегда дают одинаковый результат на одинаковых аргументах и не имеют побочных эффектов, называются чистыми. В чистом функциональном языке, наподобие Haskell, все функции чисты. Благодаря этому проще определить денотационную семантику этих языков и моделировать их с помощью теории категорий. Что касается других языков, то всегда можно ограничить себя чистым подмножеством, или размышлять о побочных эффектах отдельно. Позже мы увидим, как монады позволяют моделировать все виды эффектов, используя только чистые функции. В итоге мы ничего не теряем, ограничиваясь математическими функциями.
Примеры типов
Как только вы решите, что типы — это множества, вы можете придумать некоторые весьма экзотические примеры. Например, какой тип соответствует пустому множеству? Нет, это не void в C++, хотя этот тип называется Void в Haskell. Это тип, который не наполнен ни одним значением. Вы можете определить функцию, которая принимает Void, но вы никогда не сможете ее вызвать. Чтобы ее вызвать, вам придется обеспечить значение типа Void, а его там просто нет. Что касается того, что эта функция может вернуть — не существует никаких ограничений. Она может возвращать любой тип (хотя этого никогда не случится, потому что она не может быть вызвана). Другими словами, это функция, которая полиморфна по возвращаемому типу. Хаскеллеры назвали ее:
(прим. переводчика: на С++ такую функцию определить невозможно: в С++ у каждого типа есть хотя бы одно значение.)
(Помните, что a — это переменная типа, которая может быть любым типом.) Это имя не случайно. Существует более глубокая интерпретация типов и функций с точки зрения логики под названием изоморфизм Карри-Говарда. Тип Void представляет неправдивость, а функция absurd — утверждение, что из ложности следует что-нибудь, как в латинской фразе «ex falso sequitur quodlibet.» (прим. переводчика: из ложности следует что угодно.)
Далее идет тип, соответствующий одноэлементному множеству. Это тип, который имеет только одно возможное значение. Это значение просто «есть». Вы могли сразу его не признать, но это void в C++. Подумайте о функциях от и в этот тип. Функция из void всегда может быть вызвана. Если это чистая функция, она всегда будет возвращать один и тот же результат. Вот пример такой функции:
Вы можете подумать что эта функция принимает «ничего», но, как мы только что видели, функция, которая принимает «ничего» не может быть вызвана, потому что нет никакого значения, представляющего тип «ничего». Итак, что же эта функция принимает? Концептуально, она принимает фиктивное значение, у которого есть только единственный экземпляр, так что мы можем явно его не указывать в коде. В Haskell, однако, есть символ этого значения: пустая пара скобок (). Таким образом, из за забавного совпадения (или не совпадения?), вызов функции от void выглядит одинаково и в C++ и в Haskell. Кроме того, из-за любви Хаскеля к лаконичности, тот же символ () используется и для типа, конструктора и единственного значения, соответствующего одноэлементному множеству. Вот эта функция в Haskell:
Первая строка обьявляет, что f44 преобразует тип (), названный «единица», в тип Integer. Вторая строка определяет, что f44 с помощью паттерн-матчинга преобразует единственный конструктор для единицы, а именно () в число 44. Вы вызываете эту функцию, предоставляя значение ():
Обратите внимание, что каждая функция от единицы эквивалентна выбору одного элемента из целевого типа (здесь, выбирается Integer 44). На самом деле, вы можете думать о f44, как ином представлении числа 44. Это пример того, как мы можем заменить прямое упоминание элементов множества на функцию (стрелку). Функции из единицы в некий тип А находятся во взаимно-однозначном соответствии с элементами множества A.
А как насчет функций, возвращающих void, или, в Haskell, возвращающих единицу? В C++ такие функции используются для побочных эффектов, но мы знаем, что такие функции — не настоящие, в математическом смысле этого слова. Чистая функция, которая возвращает единицу, ничего не делает: она отбрасывает свой аргумент.
Математически, функция из множества А в одноэлементное множество отображает каждый элемент в единственный элемент этого множества. Для каждого А есть ровно одна такая функция. Вот она для Integer:
Вы даете ей любое целое число, и она возвращает единицу. Следуя духу лаконичности, Haskell позволяет использовать символ подчеркивания в качестве аргумента, который отбрасывается. Таким образом, не нужно придумывать для него название. Код выше можно переписать в виде:
Обратите внимание, что выполнение этой функции не только не зависит от значения, ей переданного, но и от типа аргумента.
Функции, которые могут быть определены одной и той же формулой для любого типа называются параметрически полиморфными. Вы можете реализовать целое семейство таких функций одним уравнением, используя параметр вместо конкретного типа. Как назвать полиморфную функцию из любого типа в единицу? Конечно, мы назовем ее unit:
В C++ вы бы реализовали ее так:
(прим. переводчика: дабы помочь компилятору оптимизировать ее в noop, лучше так):
Далее в «типологии типов» набор из двух элементов. В C++ он называется bool, а в Haskell, что не удивительно, Bool. Разница в том, что в C++ bool является встроенным типом, в то время как в Haskell он может быть определен следующим образом:
(Читать это определение стоит так: Bool может быть или True или False.) В принципе, можно было бы описать этот тип и в C++:
Но C++ перечисление на самом деле целое число. Можно было бы использовать C++11 «class enum», но тогда пришлось бы уточнять значение именем класса: bool::true или bool::false, не говоря уже о необходимости включать соответствующий заголовок в каждом файле, который его использует.
Чистые функции из Bool просто выбирают два значения из целевого типа, одно, соответствующее True и другое — False.



