Функции в Haskell. Первые вопросы

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

Продолжая эту просветительскую работу, я бы хотел остановиться сегодня на — замечательном функциональном языке программирования. Мне уже трижды прислали один и тот же вопрос: с чего начать (продолжить) изучать Haskell?

Наверное, пора дать короткий ответ-совет. Вот общий алгоритм захода в эту тему от меня.

0 этап — Введение. Haskell? Чо за хрень?

Хорошо известный в среде рекрутеров программистов парадокс, часто называемый как « », и он формулируется примерно так:

Если компания выбирает в качестве основного языка программирования некий малораспространенный эзотерический язык программирования, то у такой компании наибольшие шансы получить самых лучших программистов на рынке. Почему? Дело в том, что наняться в такую «странную компанию» захотят в первую очередь те программисты, для которых изучение нового не проблема; те, для которых малоизвестное и труднодоступное не является препятствием; и, наконец, те, у кого достаточно высокая самооценка, чтобы предлагать себя в таких заведомо жестких условиях.

И главное: есть два типа программистов: те, кто учатся, чтобы получить хорошую работу, и они всегда выбирают мэинстрим , ибо это значительно повышает их шансы на трудоустройство; и те — кому просто нравится учиться чему-то новому, развиваться, и они всегда выбирают самое лучшее , что зачастую далеко не самое выгодное , как делают их коллеги-карьеристы. Итак, Парадокс питона утверждает, что начиная разработку на передовой экзотике, вы подобно пылесосу притянете вторую категорию программеров (верно и обратное, насчет компаний предлагающих работу).

Могу привести в качестве отвлеченного примера полностью аналогичного скрытного таргетирования фокус-групп с заданными свойствами, историю из своей недавней юности. Когда я ещё учился, у нас был припод "со странностями", который демонстративно при изложении матанализа никогда не обращал внимание на правую сторону аудитории. То есть в аудитории было два ряда — левый и правый, — и вот он читает лекцию, объясняет что-то, но при этом НИКОГДА не смотрит на правый ряд — всё внимание только на студентов с левого ряда. Также и с ответами на вопросы — правого ряда для него не существовало. Оттуда он ни-че-го не слышит.

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

3. Этап — поиск глубины и чувства нового языка

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

Вот её выходные данные:

The Functional Programming Using Haskell course
(Language: English)
35 hours | 1280×720 | XviD — 1326Kbps
25.00fps | Mp3 — 96Kbps | 20.06 GB

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

4. Завершающий этап — практика

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

Вот, примерно такой вот, отчасти очевидный для многих алгоритм я хотел дать всем желающим изучить Haskell, и не только. Дерзайте!

И в заключение для приверженцев других языков программирования:

Haskell — священный язык программирования, дарованный шаманам Бубенлэнд их верховным божеством Комонада как универсальное средство для общения и духовного очищения, подходящее как божественным сущностям, так и (некоторым) простым смертным, переболевшим тяжёлыми стадиями интеллекта. Из-за своего происхождения язык всегда был функционально чист. В среднем обучение Haskell’у начинается в 10-12 лет. Своевременное начало обучения гарантирует, что вы достигнете третьего уровня Силы уже к 75 годам. Не стоит откладывать на следующую жизнь то, что можно по крайней мере начать в этой.

    Типы

    Программы на языке Haskell представляют собой выражения, вычисление которых приводит к значениям. Каждое значение имеет тип. Интуитивно тип можно понимать просто как множество допустимых значений выражения. Для того, чтобы узнать тип некоторого выражения, можно использовать команду интерпретатора:type (или:t). Кроме того, можно выполнить команду:set +t, для того, чтобы интерпретатор автоматически печатал тип каждого вычисленного результата.
    Основными типами языка Haskell являются:
    Типы Integer и Int используется для представления целых чисел, причем значения типа Integer не ограничены по длине.
    Типы Float и Double используется для представления вещественных чисел.
    Тип Bool содержит два значения: True и False, и предназначен для представления результата логических выражений.
    Тип Char используется для представления символов. Имена типов в языке Haskell всегда начинаются с заглавной буквы.
    Язык Haskell является сильно типизированным языком программирования. Тем не менее в большинстве случаев программист не обязан объявлять, каким типам принадлежат вводимые им переменные. Интерпретатор сам способен вывести типы употребляемых пользователем переменных.
    Однако, если все же для каких-либо целей необходимо объявить, что некоторое значение принадлежит некоторому типу, используется конструкция вида: переменная:: Тип. Если включена опция интерпретатора +t, он печатает значения в таком же формате.
    Ниже приведен пример протокола сессии работы с интерпретатором. Предполагается, что текст, следующий за приглашением Prelude>, вводит пользователь, а следующий за этим текст представляет ответ системы.

    Prelude>:set +t
    Prelude>1
    1:: Integer
    Prelude>1.2
    1.2:: Double
    Prelude>’a’
    ’a’ :: Char
    Prelude>True
    True:: Bool

    Из данного протокола можно сделать вывод, что значения типа Integer, Double и Char задаются по тем же правилам, что и в языке Си.
    Развитая система типов и строгая типизация делают программы на языке Haskell безопасными по типам. Гарантируется, что в правильной программе на языке Haskell все типы используются правильно. С практической точки зрения это означает, что программа на языке Haskell при выполнении не может вызвать ошибок доступа к памяти (Access violation). Также гарантируется, что в программе не может произойти использование неинициализированных переменных. Таким образом, многие ошибки в программе отслеживаются на этапе ее компиляции, а не выполнения.

    Арифметика

    Интерпретатор Hugs можно использовать для вычисления арифметических выражений. При этом можно использовать операторы +, -, *, / (сложение, вычитание, умножение и деление) с обычными правилами приоритета.
    Кроме того, можно использовать оператор ^ (возведение в степень). Таким образом, сеанс работы может выглядеть следующим образом:

    Prelude>2*2
    4:: Integer
    Prelude>4*5 + 1
    21:: Integer
    Prelude>2^3
    8:: Integer
    Кроме того, можно использовать стандартные математические функции sqrt (квадратный корень), sin, cos, exp и т.д. В отличие от многих других языков программирования, в Haskell при вызове функции не обязательно помещать аргумент в скобки. Таким образом, можно просто писать sqrt 2, а не sqrt(2). Пример:

    Prelude>sqrt 2
    1.4142135623731:: Double
    Prelude>1 + sqrt 2
    2.4142135623731:: Double
    Prelude>sqrt 2 + 1
    2.4142135623731:: Double
    Prelude>sqrt (2 + 1)
    1.73205080756888:: Double

    Из данного примера можно сделать вывод, что вызов функции имеет более высокий приоритет, чем арифметические операции, так что выражение sqrt 2 + 1 интерпретируется как (sqrt 2) + 1, а не sqrt (2 + 1). Для задания точного порядка вычисления следует использовать скобки, как в последнем примере. (В действительности вызов функции имеет более высокий приоритет, чем любой бинарный оператор.)
    Также следует заметить, что в отличие от большинства других языков программирования, целочисленные выражения в языке Haskell вычисляются с неограниченным числом разрядов (Попробуйте вычислить выражение 2^5000.) В отличие от языка Си, где максимально возможное значение типа int ограничено разрядностью машины (на современных персональных компьютерах оно равно 231-1 = 2147483647), тип Integer в языке Haskell может хранить целые числа произвольной длины.

    Кортежи
    Помимо перечисленных выше простых типов, в языке Haskell можно определять значения составных типов. Например, для задания точки наплоскости необходимы два числа, соответствующие ее координатам. В языке Haskell пару можно задать, перечислив компоненты через запятую и взяв их в скобки: (5,3). Компоненты пары не обязательно должны принадлежать одному типу: можно составить пару, первым элементом которой будет строка, а вторым - число и т.д.
    В общем случае, если a и b - некоторые произвольные типы языка Haskell, тип пары, в которой первый элемент принадлежит типу a, а второй - типу b, обозначается как (a,b). Например, пара (5,3)имеет тип (Integer, Integer); пара (1, ’a’) принадлежит типу (Integer, Char). Можно привести и более сложный пример: пара((1,’a’),1.2) принадлежит типу ((Integer,Char),Double). Проверьте это с помощью интерпретатора. Следует обратить внимания, что хотя конструкции вида (1,2) и (Integer,Integer) выглядят похоже, в языке Haskell они обозначают совершенно разные сущности. Первая является значением, в то время как последняя - типом. Для работы с парами в языке Haskell существуют стандартные функции fst и snd, возвращающие, соответственно, первый и второй элементы пары (названия этих функций происходят от английских слов «first» (первый) и «second» (второй)). Таким образом, их можно использовать следующим образом

    Prelude>fst (5, True)
    5:: Integer
    Prelude>snd (5, True)
    True:: Bool
    Кроме пар, аналогичным образом можно определять тройки, четверки и т.д. Их типы записываются аналогичным образом.
    Prelude>(1,2,3)
    (1,2,3) :: (Integer,Integer,Integer)
    Prelude>(1,2,3,4)
    (1,2,3,4) :: (Integer,Integer,Integer,Integer)
    Такая структура данных называется кортежем. В кортеже может хранится фиксированное количество разнородных данных. Функции fst и snd определены только для пар и не работают для других кортежей. При попытке использовать их, например, для троек, интерпретатор выдает сообщение об ошибке. Элементом кортежа может быть значение любого типа, в том числе и другой кортеж. Для доступа к элементам кортежей, составленных из пар, может использоваться комбинация функций fst и snd. Следующий пример демонстрирует извлечение элемента ’a’ из кортежа
    (1, (’a’, 23.12)):
    Prelude>fst (snd (1, (’a’, 23.12)))
    ’a’ :: Integer

    Списки
    В отличие от кортежей, список может хранить произвольное количество элементов. Чтобы задать список в Haskell, необходимо в квадратных скобках перечислить его элементы через запятую. Все эти элементы должны принадлежать одному и тому же типу. Тип списка с элементами, принадлежащими типу a, обозначается как [a].

    Prelude>
    ::
    Prelude>[’1’,’2’,’3’]
    [’1’,’2’,’3’] ::
    В списке может не быть ни одного элемента. Пустой список обозначается как .
    Оператор: (двоеточие) используется для добавления элемента в начало списка. Его левым аргументом должен быть элемент, а правым - список:
    Prelude>1:
    ::
    Prelude>’5’:[’1’,’2’,’3’,’4’,’5’]
    [’5’,’1’,’2’,’3’,’4’,’5’] ::
    Prelude>False:
    ::
    С помощью оператора (:) и пустого списка можно построить любой список:
    Prelude>1:(2:(3:))
    :: Integer
    Оператор (:) ассоциативен вправо, поэтому в приведенном выше выражении можно опустить скобки:
    Prelude>1:2:3:
    :: Integer
    Элементами списка могут быть любые значения - числа, символы, кортежи, другие списки и т.д.
    Prelude>[(1,’a’),(2,’b’)]
    [(1,’a’),(2,’b’)] :: [(Integer,Char)]
    Prelude>[,]
    [,] :: []
    Для работы со списками в языке Haskell существует большое количество функций. В данной лабораторной работе рассмотрим только некоторые из них.
    Функция head возвращает первый элемент списка.
    Функция tail возвращает список без первого элемента.
    Функция length возвращает длину списка.
    Функции head и tail определены для непустых списков. При попытке применить их к пустому списку интерпретатор сообщает об ошибке. Примеры работы с указанными функциями:
    Prelude>head
    1:: Integer
    Prelude>tail
    ::
    Prelude>tail
    :: Integer
    Prelude>length
    3:: Int
    Заметьте, что результат функции length принадлежит типу Int, а не типу Integer.
    Для соединения (конкатенации) списков в Haskell определен оператор ++.
    Prelude>++
    :: Integer

    Строки
    Строковые значения в языке Haskell, как и в Си, задаются в двойных кавычках. Они принадлежат типу String.
    Prelude>"hello" "hello" :: String
    В действительности строки являются списками символов; таким образом, выражения "hello", [’h’,’e’,’l’,’l’,’o’] и

    ’h’:’e’:’l’:’l’:’o’: означают одно и то же, а тип String является синонимом для . Все функции для работы со списками можно использовать при работе со строками:
    Prelude>head "hello"
    ’h’ :: Char
    Prelude>tail "hello"
    "Hello" ::
    Prelude>length "hello"
    5:: Int
    Prelude>"hello" ++ ", world"
    "hello, world" ::
    Для преобразования числовых значений в строки и наоборот существуют функции read и show:
    Prelude>show 1
    "1" ::
    Prelude>"Formula " ++ show 1
    "Formula 1" ::
    Prelude>1 + read "12"
    13:: Integer
    Если функция show не сможет преобразовать строку в число, она сообщит об ошибке.

    Функции
    До сих пор мы использовали встроенные функции языка Haskell. Теперь пришла пора научиться определять собственные функции. Для этого нам необходимо изучить еще несколько команд интерпретатора (напомним, что эти команды могут быть сокращены до одной буквы):
    Команда:load позволяет загрузить в интерпретатор программу на языке Haskell, содержащуюся в указанном файле.
    Команда:edit запускает процесс редактирования последнего загруженного файла.
    Команда:reload перечитывает последний загруженный файл. Определения пользовательских функций должны находиться в файле, который нужно загрузить в интерпретатор Hugs с помощью команды:load.
    Для редактирования загруженной программы можно использовать команду:edit. Она запускает внешний редактор (по умолчанию это Notepad) для редактирования файла. После завершения сеанса редактирования редактор необходимо закрыть; при этом интерпретатор Hugs перечитает содержимое изменившегося файла. Однако файл можно редактировать и непосредственно из оболочки Windows. В этом случае, для того чтобы интерпретатор смог перечитать файл, необходимо явно вызывать команду:reload.
    Рассмотрим пример. Создайте в каком-либо каталоге файл lab1.hs. Пусть полный путь к этому файлу - с:\labs\lab1.hs (это только пример, ваши файлы могут называться по-другому). В интерпретаторе Hugs выполните следующие команды:

    Prelude>:load "c:\\labs\\lab1.hs"
    Если загрузка проведена успешно, приглашение интерпретатора меняется на Main>. Дело в том, что если не указано имя модуля, считается, что оно равно Main.
    Main>:edit
    Здесь должно открыться окно редактора, в котором можно вводить текст программы. Введите:
    x =
    Сохраните файл и закройте редактор. Интерпретатор Hugs загрузит файл
    с:\labs\lab1.hs и теперь значение переменной x будет определено:
    Main>x
    ::
    Обратите внимание, что при записи имени файла в аргументе команды:load символы \ дублируются. Также, как и в языке Си, в Haskell символ \ служит индикатором начало служебного символа (’\n’ и т.п.) Для того, чтобы ввести непосредственно символ \, необходимо, как и в Си, экранировать его еще одним символом \.
    Теперь можно перейти к определению функций. Создайте, в соответствие с процессом, описанным выше, какой-либо файл и запишите в него следующий текст:

    square:: Integer -> Integer
    square x = x * x

    Первая строка (square:: Integer -> Integer) объявляет, что мы определяем функцию square, принимающую параметр типа Integer и возвращающую результат типа Integer. Вторая строка (square x = x * x) является непосредственно определением функции. Функция square принимает один аргумент и возвращает его квадрат. Функции в языке Haskell являются значениями «первого класса». Это означает, что они «равноправны» с такими значениями, как целые и вещественные числа, символы, строки, списки и т.д. Функции можно передавать в качестве аргументов в другие функции, возвращать их из функций и т.п. Как и все значения в языке Haskell, функции имеют тип. Тип функции, принимающей значения типа a и возвращающей значения типа b обозначается как a->b.
    Загрузите созданный файл в интерпретатор и выполните следующие команды:

    Main>:type square
    square:: Integer -> Integer
    Main>square 2
    4:: Integer
    Заметим, что в принципе объявление типа функции square не являлось необходимым: интерпретатор сам мог вывести необходимую информацию о типе функции из ее определения. Однако, во-первых, выведенный тип был бы более общим, чем Integer -> Integer, а во-вторых, явное указание типа функции является «хорошим тоном» при программировании на языке Haskell, поскольку объявление типа служит своего рода документацией к функции и помогает выявлять ошибки программирования.
    Имена определяемых пользователем функций и переменных должны начинаться с латинской буквы в нижнем регистре. Остальные символы в имени могут быть прописными или строчными латинскими буквами, цифрами или символами _ и ’ (подчеркивание и апостроф). Таким образом, ниже перечислены примеры правильных имен переменных:

    var
    var1
    variableName
    variable_name
    var’

    Условные выражения

    В определении функции в языке Haskell можно использовать условные выражения. Запишем функцию signum, вычисляющую знак переданного ей аргумента:

    signum:: Integer -> Integer
    signum x = if x > 0 then 1
    else if x else 0

    Условное выражение записывается в виде:
    if условие then выражение else выражение. Обратите внимание, что хотя по виду это выражение напоминает соответствующий оператор в языке Си или Паскаль, в условном выражении языка Haskell должны присутствовать и then-часть и else-часть. Выражения в then-части и в else-части условного оператора должны принадлежать одному типу. Условие в определении условного оператора представляет собой любое выражение типа Bool. Примером таких выражений могут служить сравнения. При сравнении можно использовать следующие операторы:
    , = - эти операторы имеют такой же смысл, как и в языке Си (меньше, больше, меньше или равно, больше или равно).
    == - оператор проверки на равенство.
    /= - оператор проверки на неравенство.
    Выражения типа Bool можно комбинировать с помощью общепринятых логических операторов && и || (И и ИЛИ), и функции отрицания not.
    Примеры допустимых условий:
    x >= 0 && x x > 3 && x /= 10
    (x > 10 || x Разумеется, можно определять свои функции, возвращающие значения типа Bool, и использовать их в качестве условий. Например, можно определить функцию isPositive, возвращающую True, если ее аргумент неотрицателен и False в противном случае:
    isPositive:: Integer -> Bool
    isPositive x = if x > 0 then True else False

    Теперь функцию signum можно определить следующим образом:
    signum:: Integer -> Integer
    signum x = if isPositive x then 1
    else if x else 0
    Отметим, что функцию isPositive можно определить и проще:
    isPositive x = x > 0

    Информация бралась с: http://sguprog.narod.ru/

Мэтью Гриффин

После продолжительного изучения Haskell я набрался достаточно опыта, чтобы сейчас дать вам пару советов. Кроме того, я хотел бы усвоить для себя кое-какие принципы, прежде чем идти дальше.

И хотя я иногда прибегаю к помощи Python, большую часть работы в вебе я теперь делаю на Haskell.

В первую очередь - данные

Я думал о переходе от динамического языка к статическому, а в Haskell’e структура данных, с которыми вы работаете, четко описывается при объявлении. В Python в большинстве случаев эту задачу выполняет код.

Когда я впервые увидел функции в Haskell, я задался вопросом: «Что представляют собой данные? Эта функция что-то берет на вход и выдает что-то на выходе?» А при работе с Python у меня возникал вопрос: «WHAT DOES THE CODE SAY?»

Структура данных Haskell сформировала качественно новый образ моего мышления, который я принес и в свою работу на Python. Мой код стал заметно лучше. И хотя очень часто мне казалось, что форма описанных мною данных менялась без причины, однако на деле все становилось ясно при небольшом изучении вопроса. Ограничения свободы в изменении данных также делают код менее сложным и более понятным.

Читаемость кода

Python привлекал меня возможностью писать действительно читаемый код. Примеры кода же на Haskell выглядели просто ужасно, за исключением некоторых сниппетов, которые, казалось, были подобраны специально, чтобы не напугать новичков. И хотя некоторые куски кода смотрелись очень даже приятно, большая часть исходников была наполнена чем-то страшным. А за этим «страшным» как раз и скрывалась вся мощь языка.

Я определенно не хотел писать «умный» код - тот, который способен продемонстрировать впечатляющие возможности языка, но совершенно неудобен.

Однако я оценивал читаемость языка Haskell в сравнении с другими популярными языками программирования. Это было похоже на то, как если бы я оценивал китайский язык, будучи носителем английского языка.

Я понял, что Haskell - это язык не «умный», но с хитринкой. Написать «умный» код на Haskell, конечно, можно, но это не распространенный случай. В то же время, мощь «умного» кода ограничена строгой типизацией. Если функция должна вернуть Int , она вернет Int . Ну или выкинет ошибку компиляции в крайнем случае.

А более мощные абстракции в Haskell и вовсе напоминают некую магию, которой я пытался избегать при работе с Python.

Про читаемость я говорю серьезно

Сначала поверьте и убедите себя, что, да, люди действительно читают листинги на этом новом для вас языке, причем делают это быстро и регулярно. Разобравшись наконец с большей частью известных нюансов, я начал спокойнее относиться к коду на Haskell.

Комментарии. Они занимают верхнюю строчку в одной из глав нашей «книги».

Эта глава описывает то, как Томми пошел в магазин и купил утку.

chapter:: Tommy -> Market -> Duck

Функции из другой, уменьшенной функции, в общей картине сокращают код по максимуму.

Краткость. Вам не потребуется тонна кода для воплощения ваших идей.

Вставные символы

Я также хотел упомянуть о вставных функциях, которые распространены в языке Haskell. Вставные функции (или операторы) - это те функции, которые используют between для двух аргументов вместо before . Простым примером является «+».

В языке Haskell мы имеем несколько вставных символов, которые используются по умолчанию: $, <$>, <-, ->, др. Они могут вас немного смутить на начале пути.

Не переживайте! Как только вы научитесь применять их, вы поймете, как они полезны и просты. Я насчитал около 5 символов, которые я использую регулярно.

Я не советую сразу пользоваться библиотекой lens, так как она содержит тонну таких символов. Данная библиотека очень полезна, но поначалу вы сможете добиться больших успехов и без нее. Дождитесь того момента, когда вы сможете спокойно писать программы средней сложности на Haskell, затем начните использовать lens.

Нужно полностью обновить знания

При изучении Haskell вы будете попутно сталкиваться с новыми терминами, например, функтор или монада.

Вам может показаться, что эти слова сложно запомнить и выучить. Когда вы начинаете знакомиться с популярным языком программирования, многие термины вам понятны и знакомы из изученных вами языков.

Мы запоминаем информацию путем ассоциирования ее с другой информацией. Например, у меня нет ассоциаций со словом функтор , поэтому выучить этот термин мне будет не легко.

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

Например, функтор.

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

Map (+1) -- results in

Так я дал название этому - мапа. Слово «мапа» является очень простым для запоминания. Список - это функтор. Список - это мапа.

Моя система проверки ошибок

Когда я писал на Python, моим инструментом отладки были операторы печати.

В Haskell я пользовался систематическими инструментами.

Но! Вы можете применить Debug.Trace . Данный прием схож с тем, как в Python функция печати не зависит от Haskell IO. И данный модуль может вначале принести пользу. Когда вы только начинали работать с Haskell, вы думали о том, как много вы будете его использовать?

Если вы не будете использовать операторы трассировки в вашем коде после проверки ошибок, то вы сразу заметите, что ваш код в Haskell выглядит хуже, чем в Python.

Лучший учебник по монадам

Каждый раз, когда вы слышите об успехе человека, программирующего на Haskell, вы задумываетесь: «Как этот человек до конца усвоил монады?» Итак, все по порядку.

Я должен провести синтаксический анализ. Я кое-что знал об этом, когда писал на Python. Но, в силу моей неопытности в этой области, сейчас сделать анализ достаточно сложно.

Окей, сейчас я расскажу подробнее. Я буду объяснять на Haskell.

Я нашел видео на YouTube, «Parsing Stuff in Haskell », в котором описывалось, как сделать JSON анализ в Haskell, используя при этом библиотеку Parsec.

Это видео помогло мне ненароком понять, как можно, используя монады и аппликативные функторы, создать то, что мне требовалось. Я понял, как их функции (har, har) соединяются друг с другом.

После написания синтаксического анализа при помощи видео, я начал понимать весь код. Я также начал понимать всю его «природу». Но на начальном этапе это не пригодится.

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

Польза от ваших знаний

Haskell является моим основным языком по нескольким причинам:

  1. Выбор технологий, которыми я буду пользоваться.
  2. Я могу писать свои программы быстрее, и чаще всего эти программы я и продаю.
  3. Не приходится иметь дело с мелкими багами.
  4. Даже сталкиваясь с несколькими ошибками, я быстро решаю их, так как они более-менее понятны.
  5. Python не делал акцент на скорости работы. Haskell делает то же самое, но выбор все же за мной.
  6. Рефакторинг, по сути, достаточно «ветреный». В Python я иногда сильно ругал себя, когда забывал править небольшие ошибки в коде, которые позже вызывали огромные затруднения.
  7. Замечательные библиотеки. Основная особенность Haskell заключается в высоком качестве библиотек.
  8. Сообщество , всегда готовое помочь.
  9. Простота масштабирования кода до нужного ядра.
  10. Haskell часто обновляется. В прошлом году, когда GHC (компилятор для Haskell) был обновлен до версии 7.8, делая при этом написание кода в два раза удобнее, были ускорены многие веб-сервера.

И в заключение хочу сказать, что Haskell доставил мне уйму удовольствия. Это был лучший опыт за всю мою жизнь.

Используется рекурсивное определение факториала. Пример состоит из трех частей:

  • определение функции factorial , принимающей на вход один аргумент типа Integer (целое число неограниченной точности) и имеющей выход того же типа. Функция определяется рекурсивно, тип параметров задан в явном виде, чтобы избежать неоднозначности их определения.
  • определение функции line , которая выводит на печать число и его факториал в нужном формате. Использование команды printf аналогично языку C++ .
  • собственно вывод чисел и их факториалов. Для этого командой создается список чисел от 0 до 16, включительно. Функция двух аргументов map применяет первый аргумент (функцию line) к каждому элементу второго аргумента (списка ) и в результате создает список так называемых действий вывода (являющихся в Haskell обычными значениями). Для того, чтобы объединить эти действия в одно, используется команда sequence_ , которая, будучи применена к списку действий, выполняет первое действие из списка и затем рекурсивно применяет себя к хвосту списка.

Факториал:

Пример для версий GHC 6.6.1

Функция fac определена как произведение чисел от 1 до n через встроенную функцию языка product.

Hello, World!:

Пример для версий GHC 6.10.4

Числа Фибоначчи:

Пример для версий GHC 6.10.4

Этот пример использует одну из основных особенностей языка Haskell — возможность ленивых вычислений и использования бесконечных списков. Бесконечный список чисел Фибоначчи fibs определяется при помощи фунции zipWith , которая применяет первый аргумент (функцию двух переменных, в данном случае +) к парам соответствующих элементов второго и третьего аргументов (списков). tail fibs возвращает хвост списка fibs (т.е. все элементы, кроме первого). Таким образом первый элемент списка, возвращаемого zipWith , является суммой первого и второго элементов списка fibs и становится третьим его элементом.

Числа Фибоначчи:

Пример для версий GHC 6.10.4

Этот пример использует рекурсивное определение чисел Фибоначчи через пары соседних чисел в последовательности. На печать выводятся только первые элементы пар.

module Main where import Text.Printf fibNextPair :: (Int , Int ) -> (Int , Int ) fibNextPair (x , y ) = (y , x + y ) fibPair :: Int -> (Int , Int ) fibPair n | n == 1 = (1 , 1 ) | otherwise = fibNextPair (fibPair (n - 1 )) line n = printf "%d, " $ (fst . fibPair ) n main = do sequence_ $ map line [ 1 .. 16 ] putStrLn "..."

Квадратное уравнение:

Пример для версий GHC 6.10.4

Haskell предоставляет тип данных для работы с комплексными числами. Функция quadratic принимает в качестве аргумента список трех комплексных чисел (коэффициентов уравнения) и возвращает список корней уравнения. Запись вида root sign позволяет передать знак операции в качестве аргумента и таким образом обобщить запись двух знаков при квадратном корне из дискриминанта.

module Main where import Data.Complex import System.IO (hFlush , stdout ) quadratic :: (Complex Double , Complex Double , Complex Double ) -> [ Complex Double ] quadratic (0 , _ , _ ) = quadratic (a , b , c ) | d == 0 = [ root (+ )] | otherwise = [ root (+ ), root (- )] where d = b * b - 4 * a * c root sign = sign (- b ) (sqrt d ) / (2 * a ) main = do putStr "A = " hFlush stdout a <- readLn :: IO Double putStr "B = " hFlush stdout b <- readLn :: IO Double putStr "C = " hFlush stdout c <- readLn :: IO Double print $ quadratic (realToFrac a , realToFrac b , realToFrac c )

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

Большинство современных функциональных языков так же предоставляют некоторые гарантии касательно программ. В частности, Haskell в большинстве случаев гарантирует, что программа не завершится с ошибкой доступа к памяти, как, скажем C++, или с ошибкой преобразования типов, как, скажем, Python или Ruby. Большинство подобных ошибок будут обнаружены на этапе компиляции программы.

В основе функционального программирования лежит простое представление о том, что функции в языке ничем не отличаются от переменных. Обычно к этому добавляют, что функции в целом должны быть функциями в математическом смысле, то есть не должны иметь некого внутреннего состояния и не должны неявно изменять внешнего состояния (окружения). Прямым следствием отсутствия состояния является тот факт, что функция, вызванная с одними и теми же аргументами возвращает одно и то же значение. Следствием неизменности внешнего состояния является отсутствие побочных эффектов. Например, printf в C/C++ принимает некие аргументы, и возвращает число. Но кроме этого, printf выводит символы в терминал. Последнее является побочным эффектом. В общем случае, неявные изменения внешнего (по отношнению к программе) состояния считаются побочными эффектами.

Но если внешнее состояние неизменно, программа неизбежно превращается в некую Кантовскую “вещь в себе” – она не может читать/записывать файлы, терминал, и т.д. Это не выглядит очень полезным. На самом деле, ФЯП выходят из положения, вводя некую псевдопеременную “мир”, которая может явно быть передана главной функции программы (main), и ее измененное состояние явно возвращается главной функцией программы. Таким образом, функциональную программу можно алгебраически представить как некую функцию:

\[ W_{n+1} = P(W_{n}), \]

где \(W\) – состояние окружения (“мир”), а \(P\) – это программа.

Другая особенность ФЯП, прямо следующая из того, что функции не имеют и не изменяют состояния заключается в том, что в ФЯП очень часто нет “переменных”. Любые выражения являются константами, если смотреть с точки зрения императивных языков. Это свойство называется “ссылочной прозрачностью”: если вы видите, что функция принимает переменную, вы всегда точно знаете, какое значение имеет эта переменная.

Подобный подход значительно упрощает “думание” о программах: больше не нужно держать в голове состояние – код работает ровно так, как читается – никаких скрытых параметров.

Использование чистых функций, помимо прочего, позволяет значительно упростить реализацию функций высших порядков, т.е. функций, оперирующих с функциями (которые тоже могут оперировать с функциями, и так далее). Поэтому, большинство ФЯП имеют поддержку функций высшего порядка “встроенными” в язык интуитивным образом. В качестве небольшого примера, приведу сравнение реализаций функции for_each , работающей со списком на C++ и Haskell.

Функции высшего порядка

На Haskell аналогичный код будет выглядеть так:

На самом деле, вызов функции с аргументами в Haskell является настолько фундаментальным, что не имеет специального синтаксиса, поэтому аналогичный код можно записать так:

На самом деле в языке уже есть такая функция, и она называется map . Вернемся к ней чуть позже.

Каррирование

Очень интересным мометном здесь является использование (1+) для добавления 1. Для этого придется рассказать про нотацию Хаскеля Брукса Карри (американского математика, 1900-1982), в честь которого назван язык. Эта нотация называется частичной аппликацией функций или каррирование .

Вкратце, любая функция нескольких аргументов \(f(x,y)\) может быть разложена на последовательное применение нескольких функций одного аргумента. Например, \(f(2,3)\) может быть разложена как:

\[ g(y) = f(2,y) \] \[ f(2,3) = h(g(3)) \]

Можно заметить, что функция двух аргументов \(f(x,y)\) превращается в функцию одного аргумента \(g(y)=f(x,y)\) . Это и есть каррирование. Если записать то же самое в нотации Haskell:

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

Какое отношение это имеет к (1+) ? Самое прямое. (1+) . Сложение является функцией двух аргументов и может быть записано как x+y = (+) x y . Тогда применение к одному аргументу (+) 1 даст нам функцию одного аргумента, которая прибавляет к аргументу 1. (1+) – это просто синтаксическое соглашение (“сахар”), которое сводится к (+) 1 .

Порядок применения функции

Чтобы каррирование работало, аргументы функции применяются слева направо (или лево-ассоциативно). Таким образом, выражение вида

Означает то же самое, что

Это отвечает на вопрос, почему нельзя записать print for_each (1+) и ожидать адекватных результатов (на самом деле такой код просто не скомпилируется, поскольку print for_each не может принимать аргументы)

Существует оператор $ , который является право -ассоциативным применением функции. Поэтому код можно записать так:

Это позволяет слегка экономить на скобках, однако применимость зависит от конкретного случая.

Эта-редукция, композиция функций и pointfree-нотация

Другой способ экономии – это эта-редкукция.

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

Пока не обращая внимания на последнюю строчку (считаем что там магия), рассмотрим функцию revlines. Очевидно, что x здесь вполне избыточен. В математике есть нотация композиции функций, обозначаемая точкой:

\[ f(g(x)) = (f\cdot g)(x) \]

Haskell имеет аналогичную нотацию: f (g x) = (f . g) x . Тогда мы можем переписать revlines в виде

Или, применяя эта-редукцию:

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

Аннотации типов

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

По соглашению, типы начинаются с заглавной буквы. Если тип обозначен маленькой буквой, то Haskell автоматически выводит его исходя из контекста. Так, например, мы использовали for_each с целыми. Тогда a=Int , b=Int . С тем же успехом, мы могли бы использовать ее со строками и т.д.

Оператор -> – право -ассоциативен (ввиду каррирования), поэтому

т.е. “функция принимает два аргумента”, эквивалентно

т.е. “функция принимает один аргумент и возвращает функцию одного аргумента”

map как функция высшего порядка

Попробуем написать свою функцию для работы со строками. Допустим, мы пишем программу, автоматически форматирующую код.

Получаем чудовищное, по виду, сообщение об ошибке

Couldn"t match type ‘Char’ with ‘’ Expected type: -> Actual type: -> In the first argument of ‘withLines’, namely ‘indent’ In the expression: withLines indent

Почему это происходит? Давайте запишем типы всех функций.

Ошибка становится вполне очевидна: withLines ожидает функцию типа -> , а мы ему даем String->String . Вообще, String определен как , т.е. список символов. Это объясняет сообщение компилятора.

Вспомним про функцию map , которая производит операцию над каждым элементом массива. Ее тип

Подставляя a,b=String , обнаруживаем то, что ищем:

Оказывается, map – это функция высшего порядка, которая преобразует функцию над элементами в функцию над массивами (списками). Подобные операции называются “поднятием” (lift), поскольку “поднимают” функцию из более простой категории типов в более сложную. Вообще, система типов Haskell сильно основана на теории категорий. Однако про теорию категорий сейчас не будем.

Секционирование операторов

Еще один пример использования функций высшего порядка

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

Вообще, `op` – это использование бинарной функции op как бинарного оператора

C тем же успехом можно частично применить второй аргумент:

Этот синтаксический сахар называется “секционирование операторов”

. – это бинарная функция высшего порядка:

Соответственно, если мы перепишем, скажем, withLines с аргументом, то получим:

Типы данных

В примерах раннее мы использовали тип данных списка (массива). Как этот тип устроен? Давайте попробуем сделать пользовательский тип списка.

Теперь мы можем записать что-то такое, например:

В Haskell, естественно, есть встроенный тип списка. Он определен следующим образом:

Можно просмотреть параллели. : – это Добавить, или cons , – это Пусто, или nil , а Список а – это [a] . Мы можем легко определить собственные операторы:

Или использовать их сразу в определении типа.

Единственное, что встроено в язык – это синтаксис вида , который автоматически преобразуется в x:y:...: .

Здесь Добавить и Пусто называются конструкторами типа Список . Это функции, которые “конструируют” значение типа Список. Не имеют практически ничего общего с конструкторами объектов в C++/Java.

Несколько примеров с нашим пользовательским типом:

С mystery1 , mystery2 все должно быть понятно. mystery3 – это бесконечный список, и Haskell все устраивает (поскольку это ленивый язык – значения вычисляются только при использовании), mystery4 – это ошибка типа. Int и String не могут находиться в одном списке.

Аналогично пишутся прочие типы данных.

Шаблоны аргументов

Для простоты, переопределим наш тип списка в латинице и сразу используем оператор:

Напишем несколько функций для нашего пользовательского списка:

Выглядит как какая-то черная магия. На самом деле все просто, мы определяем функцию, которая работает только для конкретного конструктора . _ означает только что мы игнорируем это поле. Мы так же могли бы конкретизировать аргументы, например:

Шаблоны проверяются в порядке объявления, поэтому во втором случае мы можем просто игнорировать значение.

Если мы не указываем все возможные варианты, то компилятор сообщает об этом, например:

Приводит к сообщению

Pattern match(es) are non-exhaustive In an equation for ‘isOne’: Patterns not matched: Nil

Нужно иметь ввиду, что программа аварийно завершится, если такой частично определенной функции передать значение, которое она не может обработать.

Тип Maybe

Немного странно кажется, что наша функция takeOne возвращает список, а не значение. Аналогичная библиотечная функция head возвращает значение, однако на пустом списке кидает исключение (да, в Haskell есть исключения, но их стараются избегать по очевидным причинам). Ее можно было бы записать в виде

undefined – ключевое слово, которое подходит под любой тип и аварийно завершает программу при попытке вычисления. Другой вариант undefined – это error x , где x имеет вид String . error выведет строку x перед завершением.

На помощь приходит тип Maybe . Он определен так:

В целом похоже на список, только без списка. takeOne можно переписать в виде:

Выглядит несколько более осмыслено, чем вариант со списком и гораздо более безопасно, чем undefined .

Попробуем написать поиск символа в строке с использованием типа Maybe:

Pattern guards

Предыдущий код можно переписать несколько короче:

Этот синтаксис называется “pattern guards”, или ограничители шаблонов в вольном переводе на русский. По сути, если выражение после | вычисляется как True , то шаблон срабатывает. Если нет – то проверяется следующий ограничитель, или следующий шаблон, если это был последний. otherwise – это просто более читаемый синоним для True .

В ограничителях так же возможно производить сравнения шаблонов. Синтаксис для этого:

Тогда код выше можно переписать в виде:

Классы типов

На самом деле мы можем записать более общий вариант поиска элемента в списке , практически ничего не делая:

Мы изменили только сигнатуру типа, остальное осталось прежним. Сигнатура выглядит странновато: в ней появилось выражение Eq a => . Оно означает, что тип a принадлежит классу Eq – классу типов, для которых определена эквивалентность, т.е. опреатор (==) .

Eq определен следующим образом:

Видно, что это просто сигнатура типа. Чтобы определить реализацию для конкретного типа, определяется экземпляр класса:

Каким конкретно образом реализовано сравнение символов – довольно долгая история. Примитивные типы типа Int , Char или скажем Double достаточно глубоко встроены в язык. Мы, однако, можем определить экземпляры для более сложных типов:

Здесь мы опять вводим ограничение на класс. По сути мы утверждаем, что для всех типов a в классе Eq существует тип Maybe a , так же в классе Eq . Ничто не мешает добавлять собственные типы.

Классы типов позволяют писать обобщенные, и при этом типобезопасные функции.

Опять про Maybe

Тип Maybe достаточно простой и при этом удивительно полезный. Очень многие функции, которые возможно определены могут использовать этот тип в качестве возвращаемого значения. Например:

Это гораздо лучше, чем возвращать NULL , как в C/C++ . Во-первых, глядя на сигнатуру типа, сразу становится ясно, чего можно ожидать от функции. Во-вторых, компилятор не позволит производить вычисления с несуществующим значением. Если в “классических” языках типы в основном служат компилятору, то в функциональных типы служат в первую очередь программисту.

Очевидный вопрос с Maybe заключается в следующем: допустим у нас есть функция

Рассмотрим такой код:

Каков результат такого кода? Правильный ответ – ошибка типа. 1 имеет тип Int , в то время как takeOne возвращает тип Maybe Int . Проблема очевидна. Наивное решение этой проблемы в том, чтобы написать условие:

Однако ясно, что для сколько-нибудь сложных выражений это становится весьма трудоемкой задачей.

Теперь давайте вспомним, что, когда я говорил про map , я сказал, что эта функция “поднимает” свой первый аргумент в категорию списков. Эта же концепция применима ко всем производным типам. В языке сущесвтует обобщенная функция fmap . Посмотрим, как она работает:

Это выражение возвращает Just 2 . fmap “поднимает” свой аргумент в категорию производных типов. Каких именно – компилятор выводит автоматически из контекста.

Тип fmap определен как

Здесь используется класс Functor , единственное требование которого – чтобы был определен fmap . Для Maybe экземпляр определяется тривиально:

В общем случае класс Functor – это класс типов, принимающих один аргумент типа. Другой пример функтора – список. Таким образом, map является всего лишь частным случаем fmap .

Монады

Пока скажу только, что Maybe , и список являются монадами. Понятие монады происходит из теории категорий. Это понятие в большинстве функциональных языков является ключевым, поскольку, например, Haskell, инкапсулирует состояние окружения (то, которое функции не изменяют) в монаде IO (ввод-вывод). Наконец мы можем записать сигнатуру для main:

Функция main возвращает “пустоту” (на самом деле читается как unit, и аналогично по смыслу void в С) в монаде IO . IO , в свою очередь, где-то “внутри” содержит информацию о состоянии окружения.

Сточка, которая часто нам встречалась

использует следующие функции:

return и (>>=) (читается как bind) определены в классе Monad . return просто “поднимает” значение в монаду, а bind берет значение в монаде и передает его в функцию, возвращающую монаду. При этом некое “состояние”, инкапсулированное монадой (если оно есть), может неявно передаваться из функции в функцию через оператор bind.

Для интересующихся, хорошее объяснение:

Если есть интерес, выделим лекцию на продолжение.