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

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

Что определяет язык программирования? С точки зрения теории категорий в каждом языке программирования есть два основных компонента: типы данных и преобразования между ними — в терминологии теории категорий объекты и морфизмы.

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

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

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

Например, простая функция sequence, которая определяется как:

sequence :: Monad m => [m a] -> m [a]
sequence ms = foldr k (return []) ms
            where
              k m m' = do { x <- m; xs <- m'; return (x:xs) }

может означать разные вещи в зависимости от контекста, который определяется m.

Применительно к IO это может означать последовательное выполнение действий IO:

sequence [getLine, getLine]

возвращает одно действие ввода-вывода, которое преобразуется в список из двух строк.

Применительно к списку экземпляров типа Maybe он проверит, что все они содержат некоторое значение, и либо вернет Just список этих значений, либо Nothing, если какой-либо из них Nothing:

sequence [Just 1, Just 2, Just 3] = Just [1, 2, 3]
sequence [Just 1, Just 2, Nothing] = Nothing

Применительно к списку из 2 списков он выполнит неопределенное вычисление и вернет все возможные перестановки элементов списка, где первый элемент происходит из первого списка, а второй — из второго списка:

sequence [[1,2],[3,4]] = [[1,3],[1,4],[2,3],[2,4]]
sequence [[1,2],[3,4], []] = [] -- [] is "undefined" in this context

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

В некоторых книгах по Haskell (например, LYAH) и лекциях (например, Курс Пенсильвании) это фундаментальное различие недостаточно хорошо отражено. Вместо этого они сосредотачиваются на функциональной природе Haskell и представляют Monad почти как обходной путь, позволяющий использовать чистые функции для контекстно-зависимых вычислений (IO, State, индетерминизм и т. д.). К сожалению, это создает барьер для входа для новых разработчиков, потому что, когда людей просят сделать больше, чем обычно, вложить деньги, чтобы изучить еще один функциональный язык программирования с причудливым синтаксисом, эти инвестиции трудно оправдать, не поняв сначала, что Haskell — это более мощная парадигма программирования. Сколько людей отказались от Haskell, прежде чем осознали его мощь?

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

Если вы хотите увидеть относительно простую магию, которую вы можете делать с помощью Haskell, посмотрите выступление Paweł Szulc на Lambda World’19, особенно где он говорит о Servant — библиотеке для создания REST API в Haskell. Прежде чем написать одну строку кода реализации, вы можете получить полное определение API из одного определения типа (здесь я заменяю альпак с фермы Павла пользователями):

type User = User {
    name  :: String
}
 
type UserAPI =
       "user" :> Get '[JSON] (M.Map Int User)
  :<|> "user" :> Capture "userId" Int
              :> Get '[JSON] User
  :<|> "user" :> Capture "userId" Int
              :> ReqBody '[JSON] User
              :> PutCreated '[JSON] NoContent
-- UserAPI type defines this API:
-- GET /user   - Response: {"1":{"name":"jane"}}, 200
-- GET /user/1 - Response: {"name":"jane"}, 200
-- PUT /user/2 Body: {"name":"John D."} - Response: NoContent, 201

И еще до того, как вы начнете реализовывать этот API, вы можете заставить клиентские функции вызывать этот API с помощью нескольких строк кода:

userApi :: Proxy UserAPI
userApi = Proxy
getAll :<|> getUser :<|> putUser = client userApi
 
-- client functions types:
getAll :: ClientM (M.Map Int User)
getUser :: Int -> ClientM User
putUser :: Int -> User -> ClientM User

С помощью всего нескольких аннотаций вы можете создавать документы API из типа UserAPI:

instance ToCapture (Capture "userId" Int) where
  toCapture _ =
    DocCapture "userId"
               "Id that uniquely identifies a user in the system"
 
instance ToSample (User) where
  toSamples _ = singleSample $ User "Jane"
 
instance ToSample (M.Map Int User) where
  toSamples _ = singleSample $ M.singleton 1 (User "Jane")
 
apiDocs :: API
apiDocs = docs userApi
main :: IO ()
main = (writeFile "docs.md" . markdown) apiDocs

Чтобы запустить этот сервер, вам просто нужно его реализовать, фиктивная реализация очень проста, но система типов Haskell гарантирует, что тип реализации правильный (Server UserAPI, основанный на типе UserAPI):

dummy = User "Jane" "[email protected]"
 
fetchAll :: Monad m => m (M.Map Int User)
fetchAll = pure $ M.singleton 1 dummy
 
fetch :: Monad m => Int -> m User
fetch id = pure dummy
 
insert :: Monad m => Int -> User -> m NoContent
insert id user = pure NoContent
 
server :: Server UserAPI
server = fetchAll :<|> fetch :<|> insert
 
app :: Application
app = serve userApi server
 
main :: IO ()
main = run 8080 app

Вышеупомянутое действительно похоже на волшебство!

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

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