Оригинал статьи расположен тут
Примечание переводчика:
Прошу отнестись снисходительно к качеству перевода. Буду благодарен за конструктивные советы по переводу терминологии. Я не нашел адекватного и короткого перевода термину desugar, поэтому позволил себе изменить заголовок.
Ядро языка Haskell маленькое и большая часть Haskell кода преобразуется в следующее:
- лямбды/применение функций
- алгебраические типы данных/case выражения
- рекурсивные let связывания
- классы типов и специализация, или
- вызов сторонних функций
Однажды разобравшись с этими концепциями у вас будет основа для понимания всего остального в языке. Как результат, язык будет восприниматься компактным и последовательным.
Я хочу показать как многие высокоуровневые возможности преобразуются в низкоуровневые примитивы.
if
if эквивалентен case выражению:
if b then e1 else e2
-- ... эквивалентно:
case b of
True -> e1
False -> e2
Это работает, так как Bool определено в языке как:
data Bool = False | True
Лямбда-функции нескольких аргументов
Лямбды от нескольких аргументов эквивалентны вложенным лямбдам от одного аргумента:
\x y z -> e
-- ... эквивалентно:
\x -> \y -> \z -> e
Функции
Функции эквивалентны лямбдам:
f x y z = e
-- ... эквивалентно:
f = \x y z -> e
-- ... что на самом деле выглядит как:
f = \x -> \y -> \z -> e
В результате, все функции от нескольких аргументов, на самом деле - вложенные функции от одного аргумента. Эта уловка называется каррирование.
В результате, все функции от нескольких аргументов, на самом деле - вложенные функции от одного аргумента. Эта уловка называется каррирование.
Инфиксные функции
Вы можете написать инфиксную функцию, минимум от двух аргументов, используя обратные кавычки:
x `f` y
-- ... эквивалентно:
f x y
Операторы
Операторы - это просто инфиксные функции от двух аргументов, не требующие записи с обратными кавычками. Вы можете записать их и в префиксной форме, окружив скобками:
x + y
-- ... эквивалентно:
(+) x y
Компилятор отделяет операторы от функций, резервируя специальный набор пунктуационных символов, только для операторов.
Параметризированные операторы
Скобки для операторов так же работают и в других контекстах. Вы можете привязать параметры, используя оператороподобные имена , окружив их скобками:
let f (%) x y = x % y
in f (*) 1 2
-- ... эквивалентно:
(\(%) x y -> x % y) (*) 1 2
-- ... сворачивается в:
1 * 2
Оператор сечения
Вы можете частично применить оператор к одному аргументу, используя сечения:
(1 +)
-- ... преобразуется:
\x -> 1 + x
Так же это работает и по другому:
(+ 1)
-- ... преобразуется:
\x -> x + 1
Это так же работает с инфиксными функциями, взятыми в обратные кавычки:
(`f` 1)
-- ... преобразуется:
\x -> x `f` 1
-- ... преобразуется:
\x -> f x 1
Сопоставление с образцом
Сопоставление с образцом эквивалентно case-выражениям:
f (Left l) = eL
f (Right r) = eR
-- ... преобразуется:
f x = case x of
Left l -> eL
Right r -> eR
Сопоставление с образцом числовых и строковых литералов эквивалентно тесту на равенство:
f 0 = e0
f _ = e1
-- ... преобразуется:
f x = if x == 0 then e0 else e1
-- ... преобразуется:
f x = case x == 0 of
True -> e0
False -> e1
Не рекурсивные let / where
Не рекурсивные let эквивалентны лямбда-функциям:
let x = y in z
-- ... эквивалентно:
(\x -> z) y
Тоже самое и с where:
z where x = y
-- ... эквивалентно:
(\x -> z) y
На самом деле, это не совсем правда из-за обобщения, но очень близко к правде.
Рекурсивные let/where не могут быть так преобразованы, поэтому должны рассматриваться как примитивы.
Функции верхнего уровня
Несколько функций верхнего уровня могут быть представлены как один большой рекурсивный let:
f0 x0 = e0
f1 x1 = e1
main = e2
-- ... эквивалентно:
main = let f0 x0 = e0
f1 x1 = e1
in e2
На практике Haskell не делает такое преобразование, но это стоит рассматривать как полезную умозрительную модель.
Импорт
Импортирование модулей просто добавляет функции верхнего уровня. Импорт в Haskell не имеет побочных эффектов(в отличие от многие языков), конечно если вы не используете Template Haskell.
Классы типов
Классы типов эквивалентны записям функций, которые компилятор подставляет неявно в вашем коде.
class Monoid m where
mappend :: m -> m -> m
mempty :: m
instance Monoid Int where
mappend = (+)
mempty = 0
f :: Monoid m => m -> m
f x = mappend x x
-- ... преобразуется:
data Monoid m = Monoid
{ mappend :: m -> m -> m
, mempty :: m
}
intMonoid :: Monoid Int
intMonoid = Monoid
{ mappend = (+)
, mempty = 0
}
f :: Monoid m -> m -> m
f (Monoid p z) x = p x x
... и специализирующаяся функция соответствующего класса типов просто подставляет функции с соответствующей записью:
g :: Int -> Int
g = f
-- ... преобразуется:
g = f intMonoid
Двухстрочная do нотация
Двухстрочный блок do эквивалентен инфиксному оператору (>>=):
do x <- m
e
-- ... преобразуется:
m >>= (\x -> e)
Однострочная do нотация
В однострочном do блоке, do можно просто удалить:
main = do putStrLn "Hello, world!"
-- ... преобразуется:
main = putStrLn "Hello, world!"
Многострочная do нотация
do нотация в блоках где более двух строк, эквивалентна вложенным do:
do x <- mx
y <- my
z
-- ... эквивалентно:
do x <- mx
do y <- my
z
-- ... преобразуется:
mx >>= (\x -> my >>= (\y -> z ))
let в do нотации
Не рекурсивный let в блоке do преобразуется в лямбда функцию:
do let x = y
z
-- ... преобразуется:
(\x -> z) y
ghci
Интерактивный REPL ghci аналогичен одному большому блоку do, правда с большими оговорками:
$ ghci
>>> str <- getLine
>>> let str' = str ++ "!"
>>> putStrLn str'
-- ...экивалентно следующей Haskell программе:
main = do
str <- getLine
let str' = str ++ "!"
putStrLn str'
-- ...преобразуется:
main = do
str <- getLine
do let str' = str ++ "!"
putStrLn str'
-- ...преобразуется:
main =
getLine >>= (\str ->
do let str' = str ++ "!"
putStrLn str' )
-- ...преобразуется:
main =
getLine >>= (\str ->
(\str' -> putStrLn str') (str ++ "!") )
-- ...сворачивается в:
main =
getLine >>= (\str ->
putStrLn (str ++ "!") )
Генераторы списков
Генераторы списков эквивалентны do нотации:
[ (x, y) | x <- mx, y <- my ]
-- ...эквивалентно:
do x <- mx
y <- my
return (x, y)
-- ...преобразуется:
mx >>= (\x -> my >>= \y -> return (x, y))
-- ... specialization to lists:
concatMap (\x -> concatMap (\y -> [(x, y)]) my) mx
Реально преобразованный код еще более эффективен, но все еще эквивалентен.
Расширение языка MonadComprehensions обобщает синтаксис генераторов списков для работы с любым типом относящимся к классу Monad. К примеру , вы можете написать пример с IO:
>>> :set -XMonadComprehensions
>>> [ (str1, str2) | str1 <- getLine, str2 <- getLine ]
Line1<Enter>
Line2<Enter>
("Line1", "Line2")
Числовые литералы
Целочисленные литералы полиморфны по-умолчанию и преобразуются к вызову fromIntegral для конкретного Integer:
1 :: Num a => a
-- преобразуется:
fromInteger (1 :: Integer)
Литералы с плавающей точкой ведутся себя аналогично, только преобразуются с помощью fromRational:
1.2 :: Fractional a => a
-- преобразуется:
fromRational (1.2 :: Rational)
IO
Вы можете думать о IO и вызовах всех сторонних функций как о построении синтаксического дерева, описывающего все планируемые побочные эффекты:
main = do
str <- getLine
putStrLn str
return 1
-- ...аналогично:
data IO r
= PutStrLn String (IO r)
| GetLine (String -> IO r)
| Return r
instance Monad IO where
(PutStrLn str io) >>= f = PutStrLn str (io >>= f)
(GetLine k ) >>= f = GetLine (\str -> k str >>= f)
Return r >>= f = f r
main = do
str <- getLine
putStrLn str
return 1
-- ...преобразуется и сворачивается в:
main =
GetLine (\str ->
PutStrLn str (
Return 1 ))
Эта умозрительная модель сильно отличается от того, как на самом деле реализована IO, но она хорошо работает для выработки начального наития при работе с IO.
К примеру, одно из первых наитий, которое может вырисоваться немедленно из этой модели, состоит в том, что порядок вычислений в Haskell коде не влияет на порядок IO эффектов, поскольку вычисление синтаксического дерева на самом деле не интерпретирует и не запускает само дерево. Единственный способ оживить синтаксическое дерево это определить его равным main.
Выводы
Я конечно не могу охватить всего, но надеюсь, что этот текст даст некоторое представление о том как Haskell преобразует высокоуровневые в низкоуровневые абстракции. Сохраняя ядро языка небольшим, Haskell может гарантировать , что особенности языка приятно сочетаются друг с другом.
Так же стоит обратить внимание на то, что Haskell имеет богатую систему экспериментальных расширений. Многие из них являются экспериментальными, так как каждое новое расширение должно быть проверено, чтобы понять как оно взаимодействует с существующими возможностями языка.
Комментариев нет:
Отправить комментарий