Уважаемые пользователи Голос!
Сайт доступен в режиме «чтение» до сентября 2020 года. Операции с токенами Golos, Cyber можно проводить, используя альтернативные клиенты или через эксплорер Cyberway. Подробности здесь: https://golos.io/@goloscore/operacii-s-tokenami-golos-cyber-1594822432061
С уважением, команда “Голос”
GOLOS
RU
EN
UA
ninjas
7 лет назад

Академия: Введение в функциональное программирование. Конспект третьей недели курса

Всем привет. Я принимаю участие в проекте Академия и хочу представить вашему вниманию очередную часть конспекта курса Введение в функциональное программирование от Delft University of Technology. В прошлых частях мы узнали об основных преимуществах ФП и познакомились с некоторыми наиболее часто используемыми функциями из библиотеки Prelude. Сегодня же мы познакомимся с системой типов Хаскеля. Эта тема в принципе очень важна для любого языка программирования, но в Хаскеле она играет особую роль, потому как этот язык имеет продвинутую систему алгебраических типов данных, позволяющую очень легко создавать новые типы на основе уже существующих

Система типов 

Начать стоит с того, что Хаскель - это статически типизированный язык. Это означает, что все ошибки типизации отлавливаются еще на процессе компиляции, что, по мнению многих, позволяет создавать более безопасный код и исправлять ошибки еще до запуска программы.

Как и все остальные языки, Хаскель имеет несколько встроенных типов данных. Ниже я перечислю наиболее часто используемые варианты

  • Числовые типы Int и Integer. Оба этих типа используются для представления целых чисел. В чем же разница между ними? В том, что Int имеет наименьшее и наибольшее значения, зависящие от разрядности машины, на которой запускается код. Integer же не имеет таких ограничений, однако, производительность операций с этим типом значительно ниже
  • Числовой тип Float. Используется для представления чисел с плавающей точкой
  • Char и String. Тип Char используется для представления юникод-символов. Строки, в свою очередь, представляют собой просто список одиночных символов. Для того, чтобы в этом убедиться, создадим файл second-lesson.hs и попытаемся применить к строке функцию map, работающую со списками:

import Data.Char

tupper str = map (toUpper) str

После этого запустим GHCi, загрузим наш файл командой :load и увидим следующий результат:

Стало быть, функция map отнеслась к строке как к списку и применила к его элементам заданную нами функцию toUpper

  • Тип Bool. Имеет два значения - true и false
  • Тип List. Хорошо знакомые нам последовательности значений одного типа
  • Тип Tuple (кортеж). Подобно спискам, кортежи представляют собой последовательность значений, однако, между ними есть ряд отличий. Самое главное - кортежи могут включать значения разных типов

Итак, мы рассмотрели основные типы данных, и можем сказать, что в сущности, типы - это просто шаблоны для разных видов информации. Теперь пришло время поговорить про вторую часть системы типов Хаскеля - классы 

Классы типов

Подобное название может сбивать с толку людей, привыкших к ООП, поэтому я сразу скажу, что классы из Хаскеля не имеют ничего общего с традиционными классами

Так что же такое класс типов? По сути, это набор функций, определяющих поведение тех или иных типов. Каждый тип, встроенный или созданный нами, может быть экземпляром одного или нескольких типов. Например, экземплярами класса Eq являются типы, значения которых можно сравнить. Самый очевидный образец таких типов - это числа. Стало быть, любые числа можно сравнить между друг другом с помощью функций, входящих в состав этого класса: == или /=

Классов в Хаскеле довольно много; кроме того, мы в любой момент можем создать свой собственный с необходимым нам поведением. Однако, мы оставим эту тему на потом, а сейчас подведем итоги тому, что уже успели узнать

  • Система типов Хаскеля состоит из двух частей
  • Первая часть - это собственно типы данных (встроенные или алгебраические). Они определяют структуру используемых данных
  • Вторая часть - это классы типов. Они представляют собой набор функций, применимые для определенных типов

А вот так это выглядит в виде схемы:


Типы функций

Еще одна важная вещь, связанная с типами - это то, что при определении функции ее тип обычно явно указывается. Для примера возьмем нашу функцию customProduct. Описание ее типа выглядит следующим образом:

customProduct :: [Int] -> Int

Это означает, что функция принимает в качестве аргумента список, элементы которого принадлежат к типу  Int, осуществляет некоторое преобразование и возвращает значение типа Int.

А вот как описание типа будет выглядеть для функции tupper:

tupper :: [Char] -> [Char]

Зачем это нужно? Дело в том, что обычно функции в Хаскеле значительно крупнее, чем наши примеры, и чтобы разобраться в том, что они делают, необходимо время. Однако, при наличии явного указания типа понять это становится гораздо проще, поэтому такое указание может сэкономить много времени людям, которые будут читать ваш код (в том числе и вам самим)

Еще один важная деталь, связанная с функциями в Хаскеле, заключается в том, что все они могут принимать только один аргумент. Имея это в виду, попробуем написать функцию, складывающую два числа

sumTwo :: (Int, Int) -> Int

sumTwo (a, b) = a + b

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

Добавим приправ

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

Рассмотрим это на примере нашей функции, складывающей два числа. Отбросим на время идею использования кортежей и запишем ее следующим образом:

sumTwo :: Int -> (Int -> Int)

sumTwo a b = a + b

В теле функции мало что изменилось, однако, посмотрим внимательнее на определение типа. Как мы видим, функция принимает одно значение типа Int и возвращает не число, а другую функцию (в скобках), которая принимает второй аргумент и возвращает результат сложения своего аргумента и первого числа. Если отбросить синтаксический сахар, добавленный в Хаскель для удобства создания каррированых функций, и записать нашу функцию так, как будто мы пишем ее например на JS, то она будет выглядеть так:

function sumTwo(a, b) = {

   return function(b) {

      return a + b

}

Здесь, я думаю, наглядно видно, что каррированная функция в качестве возвращаемого значения использует другую функцию

Для закрепления результата разберем еще один пример из книги "Learn you a Haskell for a great good":

multThree :: Int -> (Int -> (Int -> Int))

multThree x y z = x * y * z

((multThree 2) 3) 5

Значение 2 применяется к multThree, что дает функцию, умножающую свой аргумент на 2,  затем к полученной функции применяется аргумент 3, в результате чего возвращается функция, принимающая последний аргумент и умножающая его на 6 (результат предыдущей функции)

Из каррирования логично вытекает другая техника, называемая частичное применение функции. Суть ее в том, что мы передает каррированной функции неполное число аргументов, и таким образом легко получаем новую функцию. Например, представим, что у нас есть функция, которая рисует кривые Безье,  принимая 4 аргумента. Первые два аргумента - это это контрольные точки, отвечающие за изгиб прямой, а оставшиеся два - координаты начала и конца

cubicBezier control1 control2 start end = ...

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

line = cubicBezier (15, 22) (43, 21)

После этого нам останется только вызвать функцию line с нужными координатами начала и конца. Если же у нас возникнет необходимость создать функцию, рисующую линии с другим изгибом, то нам не придется ничего менять в исходной функции cubicBezier - мы просто снова воспользуемся частичным применением

Полиморфные функции

 Для функций, которые мы создавали, был крайне важен тип передаваемых аргументов. Например, если бы  мы вызвали функцию multThree с аргументами относящимися к типу Char, то это очевидно не имело бы смысла. Однако, подобная типозависимость есть не у всех функций. Так, если мы возьмем стандартную функцию map, то будет очевидно, что ей безразлично, какой тип будет у элементов в полученном списке - она все так же применит заданную функцию ко всем элементам. Подобное утверждение справедливо и для остальных функций, работающих со списками, вроде length или foldl

Такие функции называются полиморфными и при их создании вместо названия типа мы используем переменную типа:

length :: [a] -> Int

В этом случае переменная типа обозначается буквой a

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

Перегрузка функций

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

sum :: Num a => [a] -> a

Что показалось самым важным в этой неделе

В этой части конспекта мы начали знакомство с одной из важнейших тем в мире Хаскеля: системой типов. Мы узнали, какими встроенными типами мы можем воспользоваться, увидели, насколько полезным бывает указание типа функции, а также познакомились с несколькими полезными функциональными техниками: каррированием, частичным применением функций, полиморфизмом и перегрузкой функций

Анонс следующей части

В конспекте четвертой недели мы рассмотрим разные способы определения функций: условные и охранные выражения, а также лямбда-функции

0
361.391 GOLOS
На Golos с May 2017
Комментарии (6)
Сортировать по:
Сначала старые