Haskell Tutorial с другой стороны

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

Я отношу себя к категории ленивых фотолюбителей. У меня есть неплохая «беззеркальная зеркалка», иногда на меня нападает желание пощёлкать чего-нибудь вокруг себя. Однако я ленив, и ковыряться потом в полученном фотоархиве у меня ни времени, ни желания нет. Как правило фотографии просматриваются один-два раза сразу после съемки путём подключения фотоаппарата к телевизору через HDMI кабель. Затем фотографии отправляются в небытие каталог ~/Pictures/Photos/Unsorted и, как правило, остаются там навсегда. С различным спец. ПО я как-то не сдружился, посему этот бардак просуществовал почти два года. И вот, на волне изучения Haskell, я созрел для решения проблемы.

Отказ от ответственности


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

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

Перед тем, как начать


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

Во-вторых, необходимые инструменты. Далее предполагается, что все события разворачиваются на Linux-машине, однако с небольшими изменениями всё это верно и для Windows, и уж тем более для Mac OS X. Для того, чтобы суметь собрать полученную программу необходимо сделать три вещи:
  1. скачать и установить Haskell platform из репозитория вашего дистрибутива;
  2. установить Haskell-библиотеку работы с EXIF: sudo cabal install --global exif;
  3. установить GTK+\Glade и соответствующий биндинг для Haskell: sudo cabal install --global glade.


Постановка задачи


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

Как будем решать


Всё просто. Начнём с того, что необходимо поддержать два режима:
  • «автоматический» — когда в качестве аргумента командной строки передается директория, которую необходимо «просканировать»;
  • «ручной» — когда запускается простенький GUI фронтенд, в котором мы самостоятельно выбираем директорию для обработки.

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

Далее, независимо от того, каким образом нам был подсунут каталог, мы обходим этот каталог (включая обход всех субкаталогов, потому что многие фотоаппараты (если не все) заботливо кладут фотографии в папочку PHOTO, а затем в какие-нибудь 100PANA, 101PANA и т.п.) и обрабатываем каждый отдельный файл.

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

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


Давайте добавим теперь немного исходников.

Ближе к коду


Лирическое отступление. Программы на языке Haskell имеют модульную структуру, прямо как в C, однако разумеется без ужасных header-файлов. Экспортируемые (внешние) функции задаются в описании модуля, далее идут импорты других модулей и код. Программы на языке Haskell состоят только из описания типов данных и функций. Всё.

Итак, начнем сначала. А именно с функции main модуля Main (ведь именно с неё начинается выполнение программы). Вот код всего модуля
Main:

  1. module Main where
  2.  
  3. import System( getArgs )
  4. import Manager
  5. import GUI
  6.  
  7. main :: IO ()
  8. main = getArgs >>= launch
  9.  
  10. --должен быть единственный аргумент с путём до фотографий, иначе запускаем gui
  11. launch :: [String] -> IO ()
  12. launch [x] = processDirectory x
  13. launch _ = startGUI
* This source code was highlighted with Source Code Highlighter.


На описании первого нашего модуля я остановлюсь чуть подробнее, с пояснением основных вещей. Итак, первая строка — описание модуля. Далее строки импорта (из модуля System импортируется только функция getArgs, возвращающая список аргументов командной строки без имени исполняемого файла). Модули Manager и GUI будут описаны позднее.

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

Лирическое отступление. Формально все функции можно разделить на чистые и «грязные», имеющие побочные эффекты. Попробую объяснить на пальцах.
Функция является чистой, если независимо ни от чего при одинаковых входных данных она генерирует одинаковые выходные данные, при этом она никогда не меняет никаких состояний системы и не зависит от них (будь-то состояние файловой системы, или наличия открытого TCP соединения). Возьмем для примера функцию сложения. Она принимает два аргумента, и если оба эти аргумента равны числу 2, то на выходе мы всегда (абсолютно всегда) будем получать число 4. И да, функция сложения TCP соединений не открывает.
Функции с побочными эффектами — разговор отдельный, они как раз придуманы для того, чтобы обозначить зависимость функций от каких-либо внешний проявлений посредством модели, называемой монада ввода\вывода (IO monad). Для простоты восприятия на данный момент будем думать о монадах лишь в контексте «ввода\вывода» (хотя на самом деле это понятие гораздо шире). Функция с побочными эффектами представляет собой описание последовательности действий, в результате выполнения которых мы получим некое значение-результат. Пример:
readFile :: FilePath -> IO (String) — это функция чтения файла по заданному пути к этому файлу. Точнее это не функция, это её тип. Тип функций читать очень просто, сперва идёт имя функции, затем после двух двоеточий символом -> разделены аргументы функций и после крайнего -> идёт тип возвращаемого значения. Это примитивное описание, но его пока достаточно. То есть видно, что функция принимает некий FilePath и возвращает действие ввода/вывода, выполнение которого даст значение String.
Здесь важно понять, что последовательность действий != последовательность вычислений. На самом деле мы вообще точно не знаем в какой на самом деле последовательности будут происходить вычисления, мы определяем лишь порядок действий, за которыми они стоят. Поэтому наличие такой абстракции, как монады, не далет Haskell императивным.

Итак, возвращаемся к нашей main. Она не принимает аргументов и возвращает, очевидно, действие ввода/вывода. Выполнение этого действия и есть выполнение всей нашей программы. Забавно, но во всей полученной программе не написано ни одной чистой функции, что разительно отличается ото всех имеющихся туториалов, однако просто такова задача, решаемая программой. Последовательность выполнения действий определяется функциями >>= и >>, принимающих два аргумента. В первом случае, результат выполнения действия слева передается как аргумент в вызов функции справа, а во втором — действие слева просто тихо выполняется, после чего выполняется действие справа.

В данном случае выполнение действия, определенного функцией getArgs, даёт в результате выполнения список строк-аргументов приложения, который передается на выполнение функции launch. Функция launch описана чуть ниже. Она принимает в качестве аргумента список строк, возвращает же она также действие ввода/вывода, не возвращающего никакого значения. Как я уже говорил, функции в Haskell описываются системами уравнений, что наглядно представлено в определении launch.

Лирическое отступление. То, какое уравнение будет выбрано при каждом конкретном вызове функции определяется процедурой pattern matching, то есть сопоставление с шаблоном. Шаблон функции задается частью уравнения, определенного слева. Процедура сопоставления с шаблоном выбирает управнения в порядке их появления в коде. Универсальный шаблон, обозначающий «что угодно», представляет собой символ _. В шаблонах можно (и нужно!) декомпозировать различные структуры данных, например запись foo ["a","б"] = 0 говорит о том, что если в функцию foo придет список из двух строк «а» и «б», то она вернет 0.

Итак, если в функцию launch приходит список из единственного значения x, то считаем, что это и есть путь к директории и вызываем функцию processDirectory из модуля Manager с аргументом равным этому самому х (заметим, что нет нужды явно указывать, из какого модуля функция, если при этом не получается двойной трактовки, т.е. в двух модулях есть функция с одним названием). А если что-то другое (обратите внимание на шаблон _ ) — то запускаем GUI.

Вот и весь модуль Main! Мы только что реализовали функцию №1 нашей программы, мы ее запустили!
Да, я прекрасно понимаю, как жутко всё это выглядит для непосвященных, однако теперь вновь посмотрите на исходник модуля Main. Вновь прочтите все лирические отступления и скажите себе, так ли это сложно? Может просто непривычно? Для тех, кто мужественно и стоически выдержал все испытания, предлагается продолжить чтение.

EXIF данные


Оставим пока основной вычислительный процесс и напишем функцию извлечения данных о времени фотографии из EXIF. Для этого был создан отдельный модуль Photo. Почему отдельный? Потому что это было продиктовано мне моим внутренним чутьем, предполагается, что это модуль работы с фотографией, вдруг мой софт разрастется дополнительным функционалом? Посмотрим на его исходник.

  1. module Photo (
  2.      getTime
  3.     ) where
  4.  
  5. import Data.Time
  6. import System.Locale
  7. import Graphics.Exif
  8.  
  9. getTime :: String -> IO (Maybe Day)
  10. getTime filePath =
  11.     fromFile filePath >>= (\e -> getTag e "DateTime") >>= (return . parseDateTime)
  12.     where
  13.         parseDateTime (Just str) = parseTime defaultTimeLocale "%Y:%m:%d %H:%M:%S" str
  14.         parseDateTime Nothing = Nothing
* This source code was highlighted with Source Code Highlighter.


Страшно? Вовсе нет. Заметим, что на этот раз в определении модуля мы в скобках указали имя функции getTime. Здесь указываются функции, которые доступны извне модуля. Вот такое просто и изящное решение вместо проевших плеш header-файлов в C\C++.

Обратим внимание на функцию getTime. Она принимает в качестве аргумента путь к файлу, а возвращает действие, в результате выполнения которого будет может быть Day. Maybe здесь не какое-то ключевое слово, это вполне себе определенный тип, который может принимать два значения: Just Something или Nothing. Этот тип данных применяется, если в результате функция может не вернуть какого-либо конкретного значения из области значений. Например, поиск элемента в списке, отвечающего заданным условиям. Такого элемента может и не быть.

Здесь наличие Maybe говорит нам о том, что в данном файле может и не быть данных о времени. Day же это просто один из типов данных, представляющих время в Haskell. Определение функции getTime (то есть правая часть уравнения) сразу же вводит еще два новых для нас понятия. Но обо всем по порядку. Видно, что getTime состоит из трех действий:
  • сперва функция fromFile из модуля Graphics.Exif считывает объект типа Exif, выполнение соответствующего действия даёт нам этот объект и он передается в вызов следующей функции;
  • следующая функция представляет собой лямбда-выражение \ e -> getTag e "DateTime", здесь обратный слэш неуловимо напоминает греческий символ «лямбда», это функция одного аргумента, которая и принимает полученный ранее Exif, которая возвращает действие IO (Maybe String), возвращаемое функцией getTag, принимающей Exif и имя тэга;
  • результат выполнения предыдущего действия предается в завершающую конструкцию, опишем ее подробнее.


Во-первых, страшное и спорное слово return. На самом деле return в Haskell ничего не возвращает, и к определению функции относится очень косвенно. return — это простая функция, которая переданное ей значение превращает в действие. Дело в том, что функция, выполнение которой представляет собой выполнение цепочки действий, не может вернуть «чистое» значение. Она не может вернуть «просто String», потому что получение этого String возможно лишь при выполнении определенной цепочки действий, то есть само по себе получение этого String — тоже действие, поэтому нам необходимо оборачивать результирующее значение в монаду, т.е. «возвращать его» (return) туда, откуда оно было получено. Оно было получено из монады, нам нужно его опять вернуть (завернуть?) в монаду. А что касается возвращаемого значения: монадическая функция возвращает последнее действие, определенное в ней, т.е. в данном случае как раз return.
Во-вторых, символ точки — это композиция функций. Пусть f y = z; g x = y;, тогда (f . g) x == f( g x ) == z. В результате выполнения второго действия мы получаем может быть строку с датой, а функция сама по себе возвращает может быть Day. Поэтому мы эту может быть строку сперва парсим, в результате, очевидно, получается может быть дата, её-то мы и возвращаем. Фактически последнее действие можно записать так:
\ str -> return (parseDateTime str)


Ну и, наконец, блок where, который поясняет, что еще за parseDateTime мы использовали в определении getTime. В блоке where определяются, скажем так, функции контекста. Их область видимости ограничина конкретным определением, в котором встречается этот блок. В этих функциях контекста можно и нужно обращаться к входным аргументам вышестоящей функции. В данном случае такой функцией контекста выступает функция парсинга значений. Мы вынесли ее в отдельный блок, потому что она должна быть представлена двумя уравнениями (в противном случае можно было бы подставить ее определение напрямую в определение getTime) для двух шаблонов — с Just и Nothing. В первом случае мы парсим значение, а во втором опять возвращаем Nothing. Согласитесь, довольно наглядно?

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

Функция №2 — обход каталога



Лирическое отступление. Haskell заставляет мыслить немного иначе, не так, как мы привыкли решать задачи на императивных языках. Когда заходит речь, скажем, об обходе каталога с файлами, программист на С сказал бы следующее: «я пробегусь в цикле по файлам и проверю А, сделаю Б». Однако программист на функциональном языке скажет так: «Я применю функцию обработки к каждому файлу в директории».

Немного перестроив свое мышление перестаешь жалеть об утеряных циклах. В функциональном языке они успешно заменяются свертками и «отображениями» (map). Свертка нам здесь не понадобится, не буду утруждать себя объяснениями. Остановлюсь на функции map. Самое близкое по смыслу русское слово — это «отображение», и это действительно так. Функция map отображает один набор данных в другой. Она принимает на вход список элементов типа a, функцию типа a -> b (а как же, это ведь функциональный язык, здесь можно в качестве аргумента передать функцию!) и на выходе мы получаем список типа b. Вот тип функции map:

map :: (a -> b) -> [a] -> [b]


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

add :: Int -> Int -> Int


Её тип совсем правильно нужно бы записать вот так:

add :: (Int -> (Int -> Int))


Чувствуете разницу? Итак, add 5 2 вернет значение 7. А вот add 5, вернет функцию, которая принимает значение типа Int и возращает значение типа Int! Именно на таких чудесах и построен принцип использования функции map. Вот так мы добавляем единицу ко всем элементам исходного списка:

map (+ 1) [1,2,3]


Но подобные примеры вы горами найдете в любом Haskell туториале. Вернемся к нашим баранам. Теперь становится ясно, что со всем списком элементов в каталоге нужно сделать совершенно однотипное действие и получить список результатов этих действий (на самом деле будет немного не так, но это не важно). Чувствуете? Здесь так и просится применение map. Но сперва определимся с тем, что нужно сделать с каждым элементом каталога.

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


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

  1. --обработка элемента: если каталог - входим в рекурсию, иначе обрабатываем фотографию
  2. processSingle :: (String, Bool) -> IO ()
  3. processSingle (path, True) = processDirectory path
  4. processSingle (path, False) = do
  5.     picturesDir <- getPicturesDir
  6.     maybeDate <- getTime path
  7.     copyPhoto picturesDir maybeDate
  8.         where
  9.             -- безопасное копирование
  10.             copyPhoto pictures Nothing = return ()
  11.             copyPhoto pictures (Just date) = do
  12.                 let newPath = pictures ++ "/" ++ (formatTime defaultTimeLocale "%Y/%B/%d" date)
  13.                 copyFile path newPath
* This source code was highlighted with Source Code Highlighter.


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

Лирическое отступление. До этого момента последовательность выполнения действий мы записывали «друг за другом», разделяя специальными значками. Это не всегда удобно, например, если нужно «на ходу» сделать сопоставление с шаблоном для извлекаемого из действия значения. Или, что случается чаще, необходимо выполнить два действия, и их результат передать третьей функции. Тут на помощь приходит do-нотация, которая на самом деле более дружественна к глазам новичков. Вот два эквивалентных кода:

  1. foobar = action1 >>= action2 >>= action3
  2.  
  3. foobar' = do
  4.   result1 <- action1
  5.   result2 <- action2 result1
  6.   action3 result2
* This source code was highlighted with Source Code Highlighter.


В данном случае как раз используется два результата двух разных действий в качестве аргумента к третьей функции. Функция копирования тривиальна: вновь сопоставление по шаблону, если нашли дату — то копируем файл из path в newPath. Заметим две особенности:
  • path — это аргумент самой «верхней» функции processSingle, т.е. copyPhoto — это контекстно-зависимая функция;
  • let — это ключевое слово языка, оно позволяет привязать некоторое значение к некоторому имени (прошу заметить, это не присваивание, его значение нельзя изменить, хотя данное имя и можно привязать к другому значению, но если вы передали его до этого в какую-то функцию, то там оно не изменится) — в данном случае оно применено просто для удобства.


formatTime — это, очевидно, функция принимающая локаль, шаблон и дату, и возвращающая строку с отформатированной по данному шаблону датой.
++ — это функция конкатенации списков.

ВНЕЗАПНО, мы только что описали функцию №3. Правда просто? Так как же будет выглядеть наша функция №2. Она будет выглядеть следующим образом:

  1. processDirectory :: String -> IO ()
  2. processDirectory dir =
  3.     getDirectoryContents dir >>= checkItems >>= (mapM_ processSingle)
  4.     where
  5.         --по заданному списку содержимого каталога возвращаем кортежи с маркером "каталог" для каждого элемента
  6.         checkItems xs = mapM singleCheck xs
  7.             where
  8.                 singleCheck path = do
  9.                     isDirectory <- (doesDirectoryExist path)
  10.                     return (path, isDirectory)
* This source code was highlighted with Source Code Highlighter.


Приглядитесь к этому повнимательнее, на самом деле это очень простая функция. Но сперва пару слов о mapM и mapM_.

Лирическое отступление. Функция map, которая была рассмотрена выше, работает только с чистым кодом. У нас же код монадический, поэтому мы должны использовать монадический map. Рассмотрим типы этих функций:
mapM :: (a -> IO b) -> [a] -> IO [b]
mapM_ :: (a -> IO b) -> [a] -> IO ()


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

Итак, будем читать код снизу вверх:
  • функция singleCheck — это функция, которая проверяет, находится ли по данному пути каталог, и вовзращает пару путь-маркер (это ровно та конструкция, которую мы обрабатывали выше!);
  • функция checkItems принимает список файлов (путей к ним) и на возвращает действие, которое при выполнении даст список пар путь-маркер (здесь нам интересен результат действия, поэтому mapM);
  • наконец, функция processDirectory получает список файлов, устанавливает каждому из них маркер и далее каждую полученную пару обрабатываем согласно определенной выше функции processSingle (здесь сама по себе обработка ничего существенного не возвращает, нам не интересен результат действия, нам нужно, чтобы оно просто выполнилось).


И это всё! Конец! Программа готова. Перед заключительной частью, я хочу сказать пару слов о GUI.

GUI на декларативных языках


Чем отличаются декларативные языки от императивных? В императивных языках нужно описать КАК произвести вычисления, в декларативных же нужно описать ЧТО ты хочешь получить. Ярчайший пример декларативного языка — это SQL. Теперь подумаем на секунду о графическом интерфейсе. По сути, необходимо лишь декларативно (в соответствии с правилами конкретного тулкита) описать, что мы хотим видеть, и тут же рядом описать, что должен делать тот или иной объект при взаимодействии с ним.

Теперь вернемся к Haskell — как функциональный декларативный язык он чудесно ложится на эту структуру, ведь, например, для создания обработчика нажатия кнопки достаточно передать в качестве аргумента некую функцию обработчик! И это очень естественно. Для создания интерфейсов на Haskell я использую GTK. Делается это так: рисую формочки в Glade, а в Haskell коде размещаю обработчики.

Вот упрощенный код нашей задачи:

  1. prepareGUI mainWindow startButton fileChooser =
  2.     do
  3.     onDestroy mainWindow mainQuit
  4.     onClicked startButton (processClick fileChooser)
  5.     where
  6.         processClick fileChooser = fileChooserGetFilename fileChooser >>= processDirectory
* This source code was highlighted with Source Code Highlighter.


Я думаю, он не нуждается в комментариях.

Заключение


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

Проект на github
Чудеснейшая книжка по Haskell
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 27
  • +11
    Формально все функции можно разделить на чистые и монадические.
    О да, то есть если мы используем монаду Maybe, мы сразу теряем чистоту.
    Монадические же функции — разговор отдельный, они как раз придуманы для того, чтобы обозначить зависимость функций от каких-либо внешний проявлений посредством модели, называемой монада.
    Простите, но бред.
    Как я уже говорил, функции в Haskell описываются системами уравнений, что наглядно представлено в определении launch.
    Э-э… по-моему, у вас в голове перемешалась каша между pattern matching и уравнениями.
    Дело в том, что функция, выполнение которой представляет собой выполнение цепочки действий (то есть монадическая функция), не может быть чистой.
    Опять бред. Определите же правильно понятие «чистота».

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

    Лично я просто порекомендую желающим прочесть «Yet Another Haskell Tutorial», «Real World Haskell», «Learn You A Haskell For Greater Good» — в каком-нибудь порядке.
    • +1
      Да, я знаю, что такое монады. Знаю и о Maybe, и о списках… Я указал, что монады более широкое понятие, но здесь следует думать о них только в контексте I\O. Вы правы, наверное, это не очень корректно.
      Отсюда и некорректность определения чистоты. Завтра попробую «пофиксить» текст, сегодня уже глаза слипаются…

      А вот про pattern matching не очень понял. Вы имеете ввиду, что процедура выбора нужного уравнения лишь один из случаев применения сопоставления с шаблоном?
      • +4
        Я к тому, что если преследовать цель объяснить несведущим Haskell, то называть клозы паттерн-матчинга «системой уравнений» — значит еще больше запутать жизнь. Если, конечно, вы не хотите углубиться внутрь в реализацию и рассказать, что такое «вывод типов по Хиндли-Милнеру» и «унификация».
        Проще привести несколько примеров применения паттерн-матчинга по жизни (не только в определении функции) и пойти дальше.
        • +3
          паттерн матчинг — это, скорее, не система уравнений, а одно уравнение, описывающее функцию, по разному определенную на разных участках :)



          или для рекурсивного определения функций:




        • 0
          Ну вот, вопиющие неточности вроде как устранил…
        • –6
          монадах — это что? может «командах»
          • +1
            • –2
              Автору бы поправить, не только я это слово не знал.
              • +1
                Что поправить? Он вполне адекватно ввел это слово в процессе изложения.
                И вообще, говорить о Haskell, не говоря о монадах, невозможно.
                • –6
                  я про то, что слово отметить ссылкой на википедию, т.к. «Хорошо бы иметь представления о монадах»

                  логично, не зная данного слова, прочитать «командах»
                  • +8
                    Логично, не зная какое-то слово, поискать его в интернете
                    • +4
                      Знаете, слово «монада» в тексте упоминается 10 раз(на русском). Все 10 опечатки??
                      • 0
                        А слова «репозиторий», «дистрибутив» не надо в линки на википедию превратить?

                        Вроде хабра, а не сайт знакомств, в чём проблема самому вгуглить слово «монада», которое, я уверен, больше 70% хабровчан знают?
                        • 0
                          Слово созвучно со словом команда, и чтение топика, тем 30% несчастных, которых, судя по минусам, все так презирают, будет гораздо комфортней.
                          • НЛО прилетело и опубликовало эту надпись здесь
                            • 0
                              По рзелульаттам илссеовадний одонго анлигйсокго унвиертисета, не иеемт занчнеия, в кокам пряокде рсапожолены бкувы в солве. Галвоне, чотбы
                              преавя и пслоендяя бквуы блыи на мсете. Осатьлыне бкувы мгоут селдовтаь в плоонм бсепордяке, все-рвано ткест чтаитсея без побрелм. Пичрионй
                              эгото ялвятеся то, что мы не чиатем кдаужю бкуву по отдльенотси, а все солво цликеом. (с)
                              • НЛО прилетело и опубликовало эту надпись здесь
                                • 0
                                  Речь не о том, что могу или не могу сделать я.
                                  речь о том, что слово созвучно и единичные вкрапления, при чтении, могут быть не замечаны…
                                  • НЛО прилетело и опубликовало эту надпись здесь
              • +2
                Отличная статья, автор побудил прямо сейчас поставить и начать смотреть haskell.
                • +2
                  А скажите, чем вы пользовались при написании программы: просто текстовым реактором и компилятором в командной строке или какой-то средой? Я пользовался плагином для Эклипса и остался доволен, несмотря на то, что он весьма сыроват.
                  • +1
                    Я пробовал и Leksah (специальная IDE для Haskell, написанная на Haskell), и плагин к Eclipse смотрел, но пришел к выводу, что лично мне это всё не нужно. Поскольку у меня Linux, то я пользуюсь текстовым редактором gedit с плагином терминала. Gedit умеет подсвечивать код, поддерживать отступы и т.п., а в терминале в это время запущен GHCi интерпретатор, этого хвататет, ну во всяком случае хватало для моих относительно небольших проектов.

                    В итоге получается как-то так.
                    • +1
                      gedit наше все :) А чтобы освободить больше места под код, можно юзать guake или аналоги.
                      • +2
                        На самом деле у меня разрешение 1920х1080, поэтому проблем с недостатком места не ощущаю :) А окошко сжал специально для скриншота просто для удобства восприятия.
                  • 0
                    Если честно, статья наоборот оттолкнула от изучения функциональных языков прог-ния. Ибо всё это делается на том же питоне за 2 минуты. Не поймите неправильно, я вовсе не говорю, что «надо всё двумя строчками делать!» или «слишком много букв», но хотелось бы увидеть практическое преимущество этого языка перед другими. Если статья была направлена на тех, кому уже интересен этот язык и требуется помощь в написании чего-то бытового, то мой коммент ни в коем случае не осуждение…
                    • +3
                      Если вы хотите увидеть преимущество Хаскеля перед другими языками, то эта статья не совсем подходит. Она скорее иллюстрирует, что Хаскель, в общем-то, это тоже может, и даже не очень извратно.
                      Хаскель, в общем, удобнее других функциональных языков при решении традиционно функциональных задач. Например работать со списками на нем гораздо удобнее, чем на OCaml'е в силу присутствия list comprehensions и кучи полезных функций. Хаскель ленив, это дает определенные преимущества, в т.ч. работа с бесконечными списками на том же OCaml'е — изврат, а на Хаскеле совершенно естественное занятие. Чистота тоже полезная вещь. А еще в Хаскеле строки представляются не в виде массива, а в виде списка, что иногда удобнее. Однако Хаскель менее универсален, многие вещи на нем, увы, пишутся хуже, чем на других языках.
                      • 0
                        В Питоне тоже частично реализовано функциональное программирование

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