Когда большинство людей думают о Haskell, они думают о чистом функциональном программировании. Однако, если бы он был «полностью» функциональным, он вообще не мог бы производить никаких эффектов. Да, он по-прежнему будет полным по Тьюрингу (во многом подобно лямбда-исчислению Алонзо Черча), но станет бесполезным, поскольку компьютеры в основном используются для эффективной среды. Итак, чтобы Haskell был полезен, он должен уметь выполнять эффекты.

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

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

В теории категорий функтор — это отображение одной категории в другую. Что это значит? Категории имеют объекты. В Haskell есть типы (Int, Bool, Char и т. д.). Функтор в теории категорий отображает каждый объект (в том числе и морфизмы) одной категории в другую категорию (иногда включая самого себя). Точно так же функтор в Haskell принимает тип и возвращает новый тип. Например, список является функтором. У вас никогда не может быть просто списка. Список всегда будет нуждаться в типе для его определения: [Int], [Bool], [Char -> Char], и т. д. Большинство структур данных являются функторами. Бинарное дерево является функтором точно так же, как список является функтором. Однако функтор еще более абстрактен, чем абстрактные структуры данных! Функторы могут представлять не только абстрактные структуры данных, но и сами эффекты!

Это вводит функтор IO. Именно этот функтор отвечает за большую часть эффективности в Haskell. Давайте посмотрим на тип функции печати:

print :: Показать a =› a -> IO ()

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

Или давайте также посмотрим на getLine:

getLine :: IO String

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

В Prelude есть такая функция:

const :: a -> b -> a

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

const’ :: a -> b -> b

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

(>>) :: f a -> f b -> f b

Поскольку IO является функтором, мы можем ограничить эту функцию еще сильнее, чтобы она работала только для эффективных вычислений как таковых:

(>>) :: IO a -> IO b -> IO b

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

распечатать «Hello World» ›› getLine

Эта строка кода выводит «Hello World» в командную строку, а затем ожидает ввода данных пользователем.

К счастью, создатели Haskell знали, насколько утомительным и нечитаемым для большинства программистов будет написание (››) снова и снова, поэтому они создали (хорошо, они скопировали) вещь, называемую do-notation. Do-нотация выглядит так:

do action1
   action2
   action3

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

do {
    action1;
    action2;
    action3;
   }

Выглядит знакомо?

Это то, с чего можно начать писать императивный код на Haskell!

Do-нотация также позволяет программисту делать то, что обычно он сделать не может. Помните, как getLine имеет тип IO String? Что ж, с IO String сложнее работать, чем с простой старой строкой. В операторе do мы можем извлечь значение типа String из getLine как таковое.

do {str <- getLine;}

Теперь строковое значение, созданное getLine, может находиться под именем «str» с типом String, а не IO String. Новые имена, создаваемые в операторах такого типа, также не подчиняются тем же правилам, что и обычные значения в haskell, вне оператора do. Вот полностью действующая программа на Haskell:

main = do {str1 <- getLine;
           str2 <- getLine;
           str1 <- return $ str1 ++ str2;
           print str1;  } 

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