Миран Липовача - Изучай Haskell во имя добра!
["827–91-62","943–29-29","493–29-28"]
Превосходно!
Ещё примеры. Допустим, мы делаем отображение из ассоциативного списка чисел и при обнаружении повторяющегося ключа хотим, чтобы сохранилось наибольшее значение. Это можно сделать так:
ghci> Map.fromListWith max [(2,3),(2,100),(3,29),(3,11),(4,22),(4,15)]
fromList [(2,100),(3,29),(4,22)]
Или хотим, чтобы значения с повторяющимися ключами складывались:
ghci> Map.fromListWith (+) [(2,3),(2,100),(3,29),(3,11),(4,22),(4,15)]
fromList [(2,103),(3,40),(4,37)]
Ну что ж, модуль Data.Map, да и другие модули из стандартной библиотеки языка Haskell довольно неплохи. Далее посмотрим, как написать свой собственный модуль.
Написание собственных модулей
Практически все языки программирования позволяют разделять код на несколько файлов, и Haskell – не исключение. При написании программ очень удобно помещать функции и типы, служащие схожим целям, в отдельный модуль. Таким образом, можно будет повторно использовать эти функции в других программах, просто импортировав нужный модуль.
Мы говорим, что модуль экспортирует функции. Это значит, что когда мы его импортируем, то можем использовать экспортируемые им функции. Модуль может определить функции для внутреннего использования, но извне модуля мы видим только те, которые он экспортирует.
Модуль Geometry
Давайте разберём процесс создания модулей на простом примере. Создадим модуль, который содержит функции для вычисления объёма и площади поверхности нескольких геометрических фигур. И начнём с создания файла Geometry.hs.
В начале модуля указывается его имя. Если мы назвали файл Geometry.hs, то имя нашего модуля должно быть Geometry. Затем следует перечислить экспортируемые функции, после чего мы можем писать сами функции:
module Geometry
( sphereVolume
, sphereArea
, cubeVolume
, cubeArea
, cuboidArea
, cuboidVolume
) where
Как видите, мы будем вычислять площади и объёмы для сфер (sphere), кубов (cube) и прямоугольных параллелепипедов (cuboid). Сфера – это круглая штука наподобие грейпфрута, куб – квадратная штука, похожая на кубик Рубика, а прямоугольный параллелепипед – точь-в-точь пачка сигарет. (Дети, курить вредно!)
Продолжим и определим наши функции:
module Geometry
( sphereVolume , sphereArea
, cubeVolume
, cubeArea
, cuboidArea
, cuboidVolume
) where
sphereVolume :: Float –> Float
sphereVolume radius = (4.0 / 3.0) * pi * (radius 3)
sphereArea :: Float –> Float
sphereArea radius = 4 * pi * (radius 2)
cubeVolume :: Float –> Float
cubeVolume side = cuboidVolume side side side
cubeArea :: Float –> Float
cubeArea side = cuboidArea side side side
cuboidVolume :: Float –> Float –> Float –> Float
cuboidVolume a b c = rectArea a b * c
cuboidArea :: Float –> Float –> Float –> Float
cuboidArea a b c = rectArea a b * 2 + rectArea a c * 2 + rectArea c b * 2
rectArea :: Float –> Float –> Float
rectArea a b = a * b
Довольно стандартная геометрия, но есть несколько вещей, на которые стоит обратить внимание. Так как куб – это разновидность параллелепипеда, мы определили его площадь и объём, трактуя куб как параллелепипед с равными сторонами. Также мы определили вспомогательную функцию rectArea, которая вычисляет площадь прямоугольника по его сторонам. Функция очень проста – она просто перемножает стороны. Заметьте, мы используем функцию rectArea в функциях модуля (а именно в функциях cuboidArea и cuboidVolume), но не экспортируем её, так как хотим создать модуль для работы только с трёхмерными объектами.
При создании модуля мы обычно экспортируем только те функции, которые служат интерфейсом нашего модуля, и скрываем реализацию. Использующий наш модуль человек ничего не должен знать о тех функциях, которые мы не экспортируем. Мы можем полностью их поменять или удалить в следующей версии (скажем, удалить определение функции rectArea и просто использовать умножение), и никто не будет против – в первую очередь потому, что эти функции не экспортируются.
Чтобы использовать наш модуль, запишем:
import Geometry
Файл Geometry.hs должен находиться в той же папке, что и импортирующая его программа.
Иерархия модулей
Модулям можно придать иерархическую структуру. Каждый модуль может иметь несколько подмодулей, которые в свою очередь также могут содержать подмодули. Давайте разделим наш модуль Geometry таким образом, чтобы в него входили три подмодуля, по одному на каждый тип объекта.
Сначала создадим папку с именем Geometry. В этой папке мы разместим три файла: Sphere.hs, Cuboid.hs и Cube.hs. Посмотрим, что должно находиться в каждом файле.
Вот содержимое файла Sphere.hs:
module Geometry.Sphere
( volume
, area
) where
volume :: Float –> Float
volume radius = (4.0 / 3.0) * pi * (radius 3)
area :: Float –> Float
area radius = 4 * pi * (radius 2)
Файл Cuboid.hs выглядит так:
module Geometry.Cuboid
( volume
, area
) where
volume :: Float –> Float –> Float –> Float
volume a b c = rectArea a b * c
area :: Float –> Float –> Float –> Float
area a b c = rectArea a b * 2 + rectArea a c * 2 + rectArea c b * 2
rectArea :: Float –> Float –> Float
rectArea a b = a * b
А вот и содержимое файла Cube.hs:
module Geometry.Cube
( volume
, area
) where
import qualified Geometry.Cuboid as Cuboid
volume :: Float –> Float
volume side = Cuboid.volume side side side
area :: Float –> Float
area side = Cuboid.area side side side
Обратите внимание, что мы поместили файл Sphere.hs в папку с именем Geometry и определили имя модуля как Geometry.Sphere. То же самое мы сделали для куба и параллелепипеда. Также отметьте, что во всех трёх модулях определены функции с одинаковыми именами. Мы вправе так поступать, потому что функции находятся в разных модулях.
Итак, если мы редактируем файл, который находится на одном уровне с папкой Geometry, то запишем:
import Geometry.Sphere
после чего сможем вызывать функции area и volume, которые вычислят площадь и объём сферы. Если нам потребуется использовать несколько наших модулей, мы должны выполнить квалифицированный импорт, потому что они экспортируют функции с одинаковыми именами. Делаем так:
import qualified Geometry.Sphere as Sphere
import qualified Geometry.Cuboid as Cuboid
import qualified Geometry.Cube as Cube
Затем мы сможем вызывать функции Sphere.area, Sphere.volume, Cuboid.area и т. д., и каждая функция вычислит площадь или объём соответствующего объекта.
В следующий раз, когда вы поймаете себя за написанием огромного файла с кучей функций, попытайтесь выяснить, какие функции служат некоей общей цели, и можно ли включить их в отдельный модуль.
Позднее при написании программы со схожей функциональностью вы сможете просто импортировать свой модуль.
7
Создание новых типов и классов типов
В предыдущих главах мы изучили некоторые типы и классы типов в языке Haskell. Из этой главы вы узнаете, как создать и заставить работать свои собственные!
Введение в алгебраические типы данных
До сих пор мы сталкивались со многими типами данных – Bool, Int, Char, Maybe и др. Но как создать свой собственный тип? Один из способов – использовать ключевое слово data. Давайте посмотрим, как в стандартной библиотеке определён тип Bool:
data Bool = False | True
Ключевое слово data объявляет новый тип данных. Часть до знака равенства вводит идентификатор типа, в данном случае Bool. Часть после знака равенства – это конструкторы данных, которые также называют конструкторами значений. Они определяют, какие значения может принимать тип. Символ | означает «или». Объявление можно прочесть так: тип Bool может принимать значения True или False. И имя типа, и конструкторы данных должны начинаться с прописной буквы.
Рассуждая подобным образом, мы можем думать, что тип Int объявлен так:
data Int = –2147483648 | –2147483647 | ... | –1 | 0 | 1 | 2 | ... | 2147483647
Первое и последнее значения – минимальное и максимальное для Int. На самом деле тип Int объявлен иначе – видите, я пропустил уйму чисел – такая запись полезна лишь в иллюстративных целях.