Тезис здесь заключается в том, что 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.