Скрытые возможности Haskell
Каковы менее известные, но полезные функции языка программирования Haskell. (Я понимаю, что сам язык менее известен, но работайте со мной. Даже объяснения простых вещей в Хаскеле, такие как определение последовательности Фибоначчи с помощью одной строки кода, будут поддержаны мной.)
- Попробуйте ограничить ответы ядром Haskell
- Одна особенность за ответ
- Приведите пример и краткое описание функции, а не просто ссылку на документацию
- Пометьте объект, используя жирный заголовок в качестве первой строки
25 ответов
Мой мозг только что взорвался
Если вы попытаетесь скомпилировать этот код:
{-# LANGUAGE ExistentialQuantification #-}
data Foo = forall a. Foo a
ignorefoo f = 1 where Foo a = f
Вы получите это сообщение об ошибке:
$ ghc Foo.hs Foo.hs: 3: 22: Мой мозг просто взорвался. Я не могу обработать привязки к шаблону для конструкторов, определяемых количественно. Вместо этого используйте case-выражение или do-notation, чтобы распаковать конструктор. В связующей группе для Фу В привязке шаблона: Foo a = f В определении "ignorefoo": ignorefoo f = 1 где Foo a = f
Пользовательские структуры управления
На Хаскеле нет сокращенного троичного оператора. Встроенный if
-then
-else
всегда троичный, и является выражением (императивные языки, как правило, имеют ?:
= Выражение, if
= Утверждение). Если вы хотите, однако,
True ? x = const x
False ? _ = id
определит (?)
быть троичным оператором:
(a ? b $ c) == (if a then b else c)
Вам придется прибегать к макросам в большинстве других языков, чтобы определить свои собственные логические операторы с коротким замыканием, но Haskell - полностью ленивый язык, так что он просто работает.
-- prints "I'm alive! :)"
main = True ? putStrLn "I'm alive! :)" $ error "I'm dead :("
Hoogle
Гугл твой друг. Признаюсь, это не часть "ядра", поэтому cabal install hoogle
Теперь вы знаете, как "если вы ищете функцию более высокого порядка, она уже есть" ( комментарий ephemient). Но как вы найдете эту функцию? С гуглом!
$ hoogle "Num a => [a] -> a"
Prelude product :: Num a => [a] -> a
Prelude sum :: Num a => [a] -> a
$ hoogle "[Maybe a] -> [a]"
Data.Maybe catMaybes :: [Maybe a] -> [a]
$ hoogle "Monad m => [m a] -> m [a]"
Prelude sequence :: Monad m => [m a] -> m [a]
$ hoogle "[a] -> [b] -> (a -> b -> c) -> [c]"
Prelude zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
Программист hoogle-google не может самостоятельно писать свои программы на бумаге так же, как с помощью компьютера. Но он и машина вместе вынуждены не * считаться.
Кстати, если вам понравился Google, не забудьте проверить подсказку!
Бесплатные теоремы
Фил Уодлер познакомил нас с понятием свободной теоремы, и с тех пор мы злоупотребляем ими в Хаскеле.
Эти замечательные артефакты систем типов в стиле Хиндли-Милнера помогают с помощью эквациональных рассуждений, используя параметричность, чтобы рассказать вам о том, что функция не будет делать.
Например, есть два закона, которым должен удовлетворять каждый экземпляр Functor:
- на всем протяжении fmap f. fmap g = fmap (f. g)
- fmap id = id
Но теорема о свободе говорит нам, что нам не нужно доказывать первое, но, учитывая второе, оно приходит "бесплатно" только из сигнатуры типа!
fmap :: Functor f => (a -> b) -> f a -> f b
Вы должны быть немного осторожнее с ленью, но это частично описано в оригинальной статье и в более поздней статье Яниса Фойгтлаендера о свободных теоремах в присутствии seq
,
Сокращение для обычной операции со списком
Следующее эквивалентно:
concat $ map f list
concatMap f list
list >>= f
редактировать
Поскольку более подробная информация была запрошена...
concat :: [[a]] -> [a]
concat
берет список списков и объединяет их в один список.
map :: (a -> b) -> [a] -> [b]
map
отображает функцию над списком.
concatMap :: (a -> [b]) -> [a] -> [b]
concatMap
эквивалентно (.) concat . map
: сопоставить функцию со списком и объединить результаты.
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
Monad
имеет операцию связывания, которая называется >>=
в Haskell (или его засахаренный do
-эквивалентно). Список ака []
, это Monad
, Если мы заменим []
за m
в приведенном выше:
instance Monad [] where
(>>=) :: [a] -> (a -> [b]) -> [b]
return :: a -> [a]
Что является естественным для Monad
операции делать в списке? Мы должны удовлетворить законы монады,
return a >>= f == f a
ma >>= (\a -> return a) == ma
(ma >>= f) >>= g == ma >>= (\a -> f a >>= g)
Вы можете проверить, что эти законы соблюдаются, если мы используем реализацию
instance Monad [] where
(>>=) = concatMap
return = (:[])
return a >>= f == [a] >>= f == concatMap f [a] == f a
ma >>= (\a -> return a) == concatMap (\a -> [a]) ma == ma
(ma >>= f) >>= g == concatMap g (concatMap f ma) == concatMap (concatMap g . f) ma == ma >>= (\a -> f a >>= g)
Это, на самом деле, поведение Monad []
, В качестве демонстрации,
double x = [x,x]
main = do
print $ map double [1,2,3]
-- [[1,1],[2,2],[3,3]]
print . concat $ map double [1,2,3]
-- [1,1,2,2,3,3]
print $ concatMap double [1,2,3]
-- [1,1,2,2,3,3]
print $ [1,2,3] >>= double
-- [1,1,2,2,3,3]
Обобщенные алгебраические типы данных. Вот пример интерпретатора, в котором система типов позволяет охватить все случаи:
{-# LANGUAGE GADTs #-}
module Exp
where
data Exp a where
Num :: (Num a) => a -> Exp a
Bool :: Bool -> Exp Bool
Plus :: (Num a) => Exp a -> Exp a -> Exp a
If :: Exp Bool -> Exp a -> Exp a -> Exp a
Lt :: (Num a, Ord a) => Exp a -> Exp a -> Exp Bool
Lam :: (a -> Exp b) -> Exp (a -> b) -- higher order abstract syntax
App :: Exp (a -> b) -> Exp a -> Exp b
-- deriving (Show) -- failse
eval :: Exp a -> a
eval (Num n) = n
eval (Bool b) = b
eval (Plus e1 e2) = eval e1 + eval e2
eval (If p t f) = eval $ if eval p then t else f
eval (Lt e1 e2) = eval e1 < eval e2
eval (Lam body) = \x -> eval $ body x
eval (App f a) = eval f $ eval a
instance Eq a => Eq (Exp a) where
e1 == e2 = eval e1 == eval e2
instance Show (Exp a) where
show e = "<exp>" -- very weak show instance
instance (Num a) => Num (Exp a) where
fromInteger = Num
(+) = Plus
Вложенные многострочные комментарии.
{- inside a comment,
{- inside another comment, -}
still commented! -}
Шаблоны в привязках верхнего уровня
five :: Int
Just five = Just 5
a, b, c :: Char
[a,b,c] = "abc"
Как это круто! Спасает вас этот призыв к fromJust
а также head
время от времени.
Дополнительный макет
Вы можете использовать явные скобки и точки с запятой вместо пробелов (или макет), чтобы разделить блоки.
let {
x = 40;
y = 2
} in
x + y
... или эквивалентно...
let { x = 40; y = 2 } in x + y
... вместо...
let x = 40
y = 2
in x + y
Поскольку макет не требуется, программы на Haskell могут быть непосредственно созданы другими программами.
Оператор Fixity
Вы можете использовать ключевые слова infix, infixl или infixr для определения ассоциативности и приоритета операторов. Пример взят из ссылки:
main = print (1 +++ 2 *** 3)
infixr 6 +++
infixr 7 ***,///
(+++) :: Int -> Int -> Int
a +++ b = a + 2*b
(***) :: Int -> Int -> Int
a *** b = a - 4*b
(///) :: Int -> Int -> Int
a /// b = 2*a - 3*b
Output: -19
Число (от 0 до 9) после инфикса позволяет вам определить приоритет оператора, будучи 9 самым сильным. Infix означает отсутствие ассоциативности, тогда как infixl ассоциирует left, а infixr ассоциирует right.
Это позволяет вам определять сложные операторы для выполнения операций высокого уровня, написанных в виде простых выражений.
Обратите внимание, что вы также можете использовать бинарные функции в качестве операторов, если размещаете их между обратными галочками:
main = print (a `foo` b)
foo :: Int -> Int -> Int
foo a b = a + b
И как таковой, вы также можете определить приоритет для них:
infixr 4 `foo`
seq
а также ($!)
Достаточно только оценить, чтобы проверить, что что-то не снизу.
Следующая программа будет печатать только "там".
main = print "hi " `seq` print "there"
Для тех, кто не знаком с Haskell, Haskell в целом не является строгим, что означает, что аргумент функции оценивается только в случае необходимости.
Например, следующие распечатки "игнорируются" и завершаются успешно.
main = foo (error "explode!")
where foo _ = print "ignored"
seq
Известно, что изменить это поведение путем оценки снизу, если его первый аргумент является снизу.
Например:
main = error "first" `seq` print "impossible to print"
... или, что то же самое, без инфикса...
main = seq (error "first") (print "impossible to print")
... взорвется с ошибкой на "первый". Он никогда не напечатает "невозможно напечатать".
Так что может быть немного удивительно, что даже если seq
Строгий, он не оценит что-то, как оценивают нетерпеливые языки. В частности, он не будет пытаться форсировать все натуральные числа в следующей программе. Вместо этого он проверит, что [1..]
не внизу (что можно найти сразу), выведите "done" и выйдите.
main = [1..] `seq` print "done"
Милые охранники
Prelude
определяет otherwise = True
, делая полное состояние охраны читать очень естественно.
fac n
| n < 1 = 1
| otherwise = n * fac (n-1)
Перечисления в стиле C
Объединение сопоставления с шаблоном верхнего уровня и арифметических последовательностей дает нам удобный способ определения последовательных значений:
foo : bar : baz : _ = [100 ..] -- foo = 100, bar = 101, baz = 102
Как избежать скобок
(.)
а также ($)
функции в Prelude
иметь очень удобные исправления, позволяющие избежать скобок во многих местах. Следующее эквивалентно:
f (g (h x))
f $ g $ h x
f . g $ h x
f . g . h $ x
flip
также помогает, следующие эквивалентны:
map (\a -> {- some long expression -}) list
flip map list $ \a ->
{- some long expression -}
Удобочитаемая композиция функций
Prelude
определяет (.)
быть математической композицией функций; то есть, g . f
первый применяется f
, то применяется g
к результату.
если ты import Control.Arrow
следующие являются эквивалентными:
g . f
f >>> g
Control.Arrow
обеспечивает instance Arrow (->)
, и это хорошо для людей, которые не любят читать приложения функций в обратном направлении.
Бесконечные списки
Поскольку вы упомянули Фибоначчи, существует очень элегантный способ генерации чисел Фибоначчи из бесконечного списка, например:
fib@(1:tfib) = 1 : 1 : [ a+b | (a,b) <- zip fib tfib ]
Оператор @ позволяет вам использовать сопоставление с шаблоном в структуре 1:tfib, при этом ссылаясь на весь шаблон как на fib.
Обратите внимание, что список понимания входит в бесконечную рекурсию, генерируя бесконечный список. Тем не менее, вы можете запросить элементы или управлять ими, если вы запрашиваете конечную сумму:
take 10 fib
Вы также можете применить операцию ко всем элементам, прежде чем запрашивать их:
take 10 (map (\x -> x+1) fib)
Это благодаря ленивой оценке Haskell параметров и списков.
Гибкая спецификация модуля импорта и экспорта
Импорт и экспорт это хорошо.
module Foo (module Bar, blah) -- this is module Foo, export everything that Bar expored, plus blah
import qualified Some.Long.Name as Short
import Some.Long.Name (name) -- can import multiple times, with different options
import Baz hiding (blah) -- import everything from Baz, except something named 'blah'
Эквациональное рассуждение
Haskell, будучи чисто функциональным, позволяет читать знак равенства как реальный знак равенства (при отсутствии неперекрывающихся шаблонов).
Это позволяет вам подставлять определения непосредственно в код и с точки зрения оптимизации дает компилятору большую свободу действий в отношении того, когда что-то происходит.
Хороший пример такой формы рассуждений можно найти здесь:
http://www.haskell.org/pipermail/haskell-cafe/2009-March/058603.html
Это также хорошо проявляется в форме законов или прагм ПРАВИЛ, ожидаемых для действительных членов экземпляра, например законов Монады:
- вернуться >> >> = f == fa
- м >> = возврат == м
- (m >>= f) >>= g == m >>= (\x -> f x >>= g)
часто может использоваться для упрощения монадического кода.
Если вы ищете список или функцию более высокого порядка, она уже есть
В стандартной библиотеке так много удобных функций и функций высшего порядка.
-- factorial can be written, using the strict HOF foldl':
fac n = Data.List.foldl' (*) 1 [1..n]
-- there's a shortcut for that:
fac n = product [1..n]
-- and it can even be written pointfree:
fac = product . enumFromTo 1
Параллельное понимание списка
(Специальная функция GHC)
fibs = 0 : 1 : [ a + b | a <- fibs | b <- tail fibs ]
Улучшенное сопоставление с образцом
- Ленивые узоры
Неопровержимые закономерности
let ~(Just x) = someExpression
Смотрите сопоставление с образцом
Лень
Вездесущая лень означает, что вы можете делать такие вещи, как определение
fibs = 1 : 1 : zipWith (+) fibs (tail fibs)
Но это также дает нам более тонкие преимущества с точки зрения синтаксиса и аргументации.
Например, из-за строгости ML имеет дело с ограничением значения и очень тщательно отслеживает циклические привязки let, но в Haskell мы можем позволить каждому let быть рекурсивным и не нужно различать val
а также fun
, Это удаляет основные синтаксические бородавки из языка.
Это косвенно порождает наш прекрасный where
пункт, потому что мы можем безопасно перемещать вычисления, которые могут или не могут быть использованы, из основного потока управления и позволить лени иметь дело с разделением результатов.
Мы можем заменить (почти) все те функции стиля ML, которые должны принять () и вернуть значение, просто ленивым вычислением значения. Есть причины избегать этого время от времени, чтобы избежать утечки пространства с CAF, но такие случаи редки.
Наконец, это позволяет неограниченное eta-сокращение (\x -> f x
можно заменить на е). Это делает программирование, ориентированное на комбинатор, для таких вещей, как комбинаторы парсера, намного приятнее, чем работа с аналогичными конструкциями на строгом языке.
Это помогает вам при рассуждении о программах в стиле без точек или о переписывании их в стиле без точек и уменьшает шум аргументов.
Перечисления
Любой тип, который является экземпляром Enum, может использоваться в арифметической последовательности, а не только в числах:
alphabet :: String
alphabet = ['A' .. 'Z']
Включая ваши собственные типы данных, просто наследуйте от Enum, чтобы получить реализацию по умолчанию:
data MyEnum = A | B | C deriving(Eq, Show, Enum)
main = do
print $ [A ..] -- prints "[A,B,C]"
print $ map fromEnum [A ..] -- prints "[0,1,2]"
Монады
Они не такие скрытые, но они просто везде, даже там, где вы о них не думаете (списки, может быть, типы) ...