Статьи

Почему Хаскелл?

Будучи чисто функциональным языком, Haskell ограничивает вас от многих традиционных методов программирования на объектно-ориентированном языке. Но действительно ли ограничение возможностей программирования дает нам какие-либо преимущества перед другими языками?

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


Хаскель — это совсем другой язык.

Haskell — это совсем другой тип языка, чем вы могли бы привыкнуть, в том смысле, как вы упорядочиваете свой код в «чистые» функции. Чистая функция — это та, которая не выполняет никаких внешних задач, кроме возврата вычисленного значения. Эти внешние задачи обычно называют «побочными эффектами».

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

Теперь вы можете задаться вопросом: «Что хорошего в программе, если она не может взаимодействовать с внешним миром?» Ну, Хаскелл решает эту проблему с помощью специального вида функции, называемой функцией ввода-вывода. По сути, вы разделяете все части кода, обрабатывающие данные, на чистые функции, а затем помещаете части, которые загружают и выводят данные, в функции ввода-вывода. «Основная» функция, которая вызывается при первом запуске вашей программы, является функцией ввода-вывода.

Давайте рассмотрим быстрое сравнение между стандартной Java-программой и ее эквивалентом на Haskell.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
import java.io.*;
 
class Test{
    public static void main(String[] args)
    {
        System.out.println(«What’s Your Name: «);
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String name = null;
        try {
            name = br.readLine();
        }
        catch(IOException e) {
            System.out.println(«There Was an Error»);
        }
        System.out.println(«Hello » + name);
    }
}
1
2
3
4
5
6
welcomeMessage name = «Hello » ++ name
 
main = do
       putStrLn «What’s Your Name: «
       name <- getLine
       putStrLn $ welcomeMessage name

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

1
putStrLn $ welcomeMessage name

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

Помимо того факта, что программа на Haskell значительно короче, чем реализация Java, основное отличие состоит в том, что мы разделили обработку данных на pure функцию, тогда как в версии Java мы только распечатали ее. Это ваша работа в Haskell в двух словах: разделить ваш код на его компоненты. Почему ты спрашиваешь? Что ж. Есть несколько причин; давайте рассмотрим некоторые из них.

Этот код не может быть взломан.

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

В математике функция вычисляет результат; это все. Например, если я напишу математическую функцию, например, f(x) = 2x + 4 , то, если я передам x = 2 , я получу 8 . Если я вместо этого передам x = 3 , я получу 10 в результате. Этот код не может быть взломан. Кроме того, поскольку все разделено на небольшие функции, модульное тестирование становится тривиальным; Вы можете проверить каждую отдельную часть вашей программы и двигаться дальше, зная, что она на 100% безопасна.

Еще одним преимуществом разделения вашего кода на несколько функций является возможность повторного использования кода. Представьте, что все стандартные функции, такие как min и max , также выводят значение на экран. Тогда эти функции будут актуальны только в очень уникальных обстоятельствах, и в большинстве случаев вам придется писать свои собственные функции, которые только возвращают значение, не распечатывая его. То же самое относится и к вашему пользовательскому коду. Если у вас есть программа, которая преобразует измерения из сантиметров в сантиметры, вы можете поместить реальный процесс преобразования в чистую функцию, а затем использовать ее повсеместно. Однако, если вы жестко закодируете его в своей программе, вам придется каждый раз вводить его заново. Сейчас это кажется довольно очевидным в теории, но, если вы помните сравнение Java сверху, есть некоторые вещи, к которым мы привыкли просто жестко программировать.

Кроме того, Haskell предлагает два способа объединения функций: оператор точки и функции более высокого порядка.

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

Вот быстрый пример, чтобы продемонстрировать эту идею:

1
2
3
4
5
6
7
8
9
cmToInches cm = cm * 0.3937
 
formatInchesStr i = show i ++ » inches»
 
main = do
    putStrLn «Enter length in cm:»
    inp <- getLine
    let c = (read inp :: Float)
    (putStrLn . formatInchesStr . cmToInches) c

Это похоже на последний пример на Haskell, но здесь я объединил вывод cmToInches с входом formatInchesStr и связал этот вывод с putStrLn . Функции высшего порядка — это функции, которые принимают другие функции в качестве входных данных, или функции, которые выводят функцию в качестве ее выходных данных. Полезным примером этого является встроенная в Haskell функция map . map принимает функцию, предназначенную для одного значения, и выполняет эту функцию для массива объектов. Функции более высокого порядка позволяют вам абстрагировать фрагменты кода, которые имеют несколько функций, а затем просто предоставлять функцию в качестве параметра для изменения общего эффекта.

В Haskell нет поддержки для изменения состояния или изменяемых данных.

В Haskell нет поддержки для изменения состояния или изменяемых данных, поэтому, если вы попытаетесь изменить переменную после того, как она была установлена, вы получите ошибку во время компиляции. Поначалу это может показаться не очень привлекательным, но делает вашу программу «прозрачной по ссылкам». Это означает, что ваши функции всегда будут возвращать одни и те же значения при условии одинаковых входных данных. Это позволяет Haskell упростить вашу функцию или полностью заменить ее кэшированным значением, и ваша программа продолжит нормально работать, как и ожидалось. Опять же, хорошая аналогия с этим — математические функции — так как все математические функции являются ссылочно прозрачными. Если бы у вас была функция, такая как sin(90) , вы могли бы заменить ее на число 1 , потому что они имеют одинаковое значение, экономя ваше время на его вычисление каждый раз. Другое преимущество, которое вы получаете с этим видом кода, состоит в том, что, если у вас есть функции, которые не зависят друг от друга, вы можете запускать их параллельно — снова повышая общую производительность вашего приложения.

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

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

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

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

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

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

Давайте начнем с некоторого планирования.


Ранее я упоминал, что в Haskell вы не планируете свою программу в стиле обзорного типа. Вместо этого вы организуете отдельные функции, в то же время не забывая разделять код на pure и IO-функции. Первое, что нужно сделать этой программе, это подключиться к базе данных и получить список таблиц. Обе функции ввода-вывода, потому что они получают данные из внешней базы данных.

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

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

В этой программе я буду использовать библиотеку HDBC MySQL, которую вы можете установить, запустив cabal install HDBC и cabal install HDBC-mysql если у вас установлена ​​платформа Haskell. Давайте начнем с первых двух функций в списке, поскольку они обе встроены в библиотеку HDBC:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
import Control.Monad
import Database.HDBC
import Database.HDBC.MySQL
import System.IO
import System.Directory
import Data.Time
import Data.Time.Calendar
 
main = do
    conn <- connectMySQL defaultMySQLConnectInfo {
        mysqlHost = «127.0.0.1»,
        mysqlUser = «root»,
        mysqlPassword = «pass»,
        mysqlDatabase = «test»
    }
    tables <- getTables conn

Эта часть довольно проста; мы создаем соединение и затем помещаем список таблиц в переменную, называемую tables . Следующая функция перебирает список таблиц и получает все строки в каждой, быстрый способ сделать это — создать функцию, которая обрабатывает только одно значение, а затем использовать функцию map чтобы применить ее к массиву. Поскольку мы отображаем функцию ввода-вывода, мы должны использовать mapM . После этого ваш код должен выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
getQueryString name = «select * from » ++ name
 
processTable :: IConnection conn => conn -> String -> IO [[SqlValue]]
processTable conn name = do
        let qu = getQueryString name
        rows <- quickQuery’ conn qu []
        return rows
 
main = do
        conn <- connectMySQL defaultMySQLConnectInfo {
                mysqlHost = «127.0.0.1»,
                mysqlUser = «root»,
                mysqlPassword = «pass»,
                mysqlDatabase = «test»
        }
        tables <- getTables conn
        rows <- mapM (processTable conn) tables

getQueryString — это чистая функция, которая возвращает запрос на выборку, и тогда у нас есть фактическая функция processTable , которая использует эту строку запроса для извлечения всех строк из указанной таблицы. Haskell — это строго типизированный язык, что означает, что вы не можете, например, поместить int туда, куда должна идти string . Но Haskell также является «выводом типа», что означает, что вам обычно не нужно писать типы, и Haskell поймет это. Здесь у нас есть собственный тип conn , который мне нужно было объявить явно; это то, что делает строка над функцией processTable .

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

1
2
3
4
unSql x = (fromSql x) :: String
 
sqlToArray [n] = (unSql n) : []
sqlToArray (n:n2) = (unSql n) : sqlToArray n2

Затем добавьте следующую строку в основную функцию:

1
let stringRows = map (map sqlToArrays) rows

Возможно, вы заметили, что иногда переменные объявляются как var , and at other times, as let var = function . The rule is essentially, when you are attempting to run a IO function and place the results in a variable, you use the method; to store a pure function's results within a variable, you would instead use let . var , and at other times, as let var = function . The rule is essentially, when you are attempting to run a IO function and place the results in a variable, you use the method; to store a pure function's results within a variable, you would instead use let .

Следующая часть будет немного хитрой. У нас есть все строки в строковом формате, и теперь мы должны заменить каждую строку значений строкой вставки, понятной MySQL. Проблема в том, что имена таблиц находятся в отдельном массиве; поэтому функция двойной map в этом случае не будет работать. Мы могли бы использовать map один раз, но тогда нам пришлось бы объединить списки в один — возможно, используя кортежи, потому что map принимает только один входной параметр — поэтому я решил, что было бы проще просто написать новые рекурсивные функции. Поскольку у нас есть трехуровневый массив, нам понадобятся три отдельные рекурсивные функции, чтобы каждый уровень мог передавать свое содержимое на следующий уровень. Вот три функции вместе с вспомогательной функцией для генерации фактического запроса SQL:

01
02
03
04
05
06
07
08
09
10
flattenArgs [arg] = «\»» ++ arg ++ «\»»
flattenArgs (arg1:args) = «\»» ++ arg1 ++ «\», » ++ (flattenArgs args)
 
iQuery name args = «insert into » ++ name ++ » values (» ++ (flattenArgs args) ++ «);\n»
                                                                                                                                                                 
insertStrRows name [arg] = iQuery name arg
insertStrRows name (arg1:args) = (iQuery name arg1) ++ (insertStrRows name args)
                                                                                                                                                                 
insertStrTables [table] [rows] = insertStrRows table rows : []
insertStrTables (table1:other) (rows1:etc) = (insertStrRows table1 rows1) : (insertStrTables other etc)

Снова добавьте следующее к основной функции:

1
let insertStrs = insertStrTables tables stringRows

Функции flattenArgs и iQuery работают вместе для создания фактического запроса вставки SQL. После этого у нас есть только две рекурсивные функции. Обратите внимание, что в двух из трех рекурсивных функций мы вводим массив, но функция возвращает строку. Делая это, мы удаляем два из вложенных массивов. Теперь у нас есть только один массив с одной выходной строкой на таблицу. Последний шаг — записать данные в соответствующие файлы; это значительно проще, теперь, когда мы имеем дело с простым массивом. Вот последняя часть вместе с функцией для получения даты:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
dateStr = do
    t <- getCurrentTime
    return (showGregorian . utctDay $ t)
 
filename name time = «Backups/» ++ name ++ «_» ++ time ++ «.bac»
 
writeToFile name queries = do
    let output = (deleteStr name) ++ queries
    time <- dateStr
    createDirectoryIfMissing False «Backups»
    f <- openFile (filename name time) WriteMode
    hPutStr f output
    hClose f
 
writeFiles [n] [q] = writeToFile nq
writeFiles (n:n2) (q:q2) = do
    writeFiles [n] [q]
    writeFiles n2 q2

Функция dateStr преобразует текущую дату в строку в формате YYYY-MM-DD . Затем есть функция имени файла, которая объединяет все части имени файла. Функция writeToFile заботится о выводе в файлы. Наконец, функция writeFiles выполняет writeFiles по списку таблиц, поэтому вы можете иметь один файл на таблицу. writeFiles завершить основную функцию вызовом writeFiles и добавить сообщение, информирующее пользователя о завершении. После завершения ваша main функция должна выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
12
13
main = do
    conn <- connectMySQL defaultMySQLConnectInfo {
        mysqlHost = «127.0.0.1»,
        mysqlUser = «root»,
        mysqlPassword = «pass»,
        mysqlDatabase = «test»
    }
    tables <- getTables conn
    rows <- mapM (processTable conn) tables
    let stringRows = map (map sqlToArray) rows
    let insertStrs = insertStrTables tables stringRows
    writeFiles tables insertStrs
    putStrLn «Databases Sucessfully Backed Up»

Теперь, если какая-либо из ваших баз данных когда-либо потеряет свою информацию, вы можете вставить запросы SQL прямо из файла резервной копии в любой терминал или программу MySQL, которая может выполнять запросы; это восстановит данные к этому моменту времени. Вы также можете добавить задание cron, которое будет запускаться ежечасно или ежедневно, чтобы обновлять резервные копии.


Есть отличная книга Миран Липовача, которая называется «Учим тебя на Хаскеле» .

Это все, что у меня есть для этого урока! Двигаясь вперед, если вы заинтересованы в полном изучении Haskell, есть несколько хороших ресурсов для ознакомления. У Мирана Липовача есть отличная книга под названием «Learn you a Haskell» , в которой даже есть бесплатная онлайн-версия. Это было бы отличным началом.

Если вы ищете определенные функции, вам следует обратиться к Hoogle , которая является Google-подобной поисковой системой, которая позволяет осуществлять поиск по имени или даже по типу. Итак, если вам нужна функция, которая преобразует строку в список строк, вы должны ввести String -> [String] , и она предоставит вам все применимые функции. Существует также сайт с именем hackage.haskell.org , который содержит список модулей для Haskell; Вы можете установить их через клику.

Я надеюсь, вам понравился этот урок. Если у вас есть какие-либо вопросы, не стесняйтесь оставлять комментарии ниже; Я сделаю все возможное, чтобы вернуться к вам как можно скорее!