Скрытые черты Haskell

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

  • Попробуйте ограничить ответы на ядро ​​Haskell
  • Одна функция для каждого ответа
  • Приведите пример и краткое описание функции, а не ссылку на документацию
  • Обозначьте эту функцию, используя полужирный заголовок в качестве первой строки

Ответ 1

Мой мозг просто взорвался

Если вы попытаетесь скомпилировать этот код:

{-# LANGUAGE ExistentialQuantification #-}
data Foo = forall a. Foo a
ignorefoo f = 1 where Foo a = f

Вы получите это сообщение об ошибке:

$ ghc Foo.hs

Foo.hs:3:22:
    My brain just exploded.
    I can't handle pattern bindings for existentially-quantified constructors.
    Instead, use a case-expression, or do-notation, to unpack the constructor.
    In the binding group for
        Foo a
    In a pattern binding: Foo a = f
    In the definition of `ignorefoo':
        ignorefoo f = 1
                    where
                        Foo a = f

Ответ 2

Пользовательские структуры управления

У Haskell нет стенографического тройного оператора. Встроенный if - then - else всегда тернарный и является выражением (императивные языки имеют тенденцию иметь ?:= выражение, if= statement). Если вы хотите, тем не менее,

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 :("

Ответ 3

Hoogle

Hoogle - ваш друг. Я признаю, что это не часть "ядра", поэтому cabal install hoogle

Теперь вы знаете, как "если вы ищете функцию более высокого порядка, она уже существует" (эфемерный комментарий). Но как вы находите эту функцию? С hoogle!

$ 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 не может самостоятельно писать свои программы на бумаге так же, как с помощью компьютера. Но он и машина вместе вынуждены не считаться с *.

Кстати, если вам понравилось hoogle, не забудьте проверить hlint!

Ответ 4

Свободные теоремы

Фил Вадлер познакомил нас с понятием бесплатной теоремы , и с тех пор мы злоупотребляли ими в Haskell.

Эти замечательные артефакты систем типа типа Хиндли-Милнера помогают в эквациональном рассуждении, используя параметричность, чтобы рассказать вам о том, что функция не будет делать.

Например, существуют два закона, которые должны удовлетворять каждый экземпляр Functor:

forall f g. fmap f. fmap g = fmap (f. g) fmap id = id

Но, свободная теорема говорит нам, что нам не нужно доказывать первое, но учитывая второе, оно приходит для "свободного" только от сигнатуры типа!

fmap :: Functor f => (a -> b) -> f a -> f b

Вам нужно быть немного осторожным с лени, но это частично описано в оригинальной статье, а в Janis Voigtlaender на свободных теоремах в присутствии seq.

Ответ 5

Сокращение для общей операции списка

Следующие эквиваленты:

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

A Monad имеет операцию связывания, которая называется >>= в Haskell (или ее sugared do -эквивалент). Список, aka [], является 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]

Ответ 6

Вложенные многострочные комментарии.

{- inside a comment,
     {- inside another comment, -}
still commented! -}

Ответ 7

Обобщенные типы алгебраических данных. Вот пример интерпретатора, в котором система типов позволяет вам охватывать все случаи:

{-# 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

Ответ 8

Шаблоны в привязках верхнего уровня

five :: Int
Just five = Just 5

a, b, c :: Char
[a,b,c] = "abc"

Как здорово! Сохраняет ваш вызов fromJust и head время от времени.

Ответ 9

Дополнительный макет

Вы можете использовать явные фигурные скобки и точки с запятой вместо пробелов (ака макет), чтобы разделить блоки.

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 могут быть легко созданы другими программами.

Ответ 10

Фиксация оператора

Вы можете использовать ключевые слова infix, infixl или infixr для определения ассоциативности операторов и приоритета. Пример, взятый из reference:

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) после инфикса позволяет определить приоритет оператора, будучи самым сильным. Infix означает отсутствие ассоциативности, тогда как infixl связывает права слева и infixr.

Это позволяет вам определять сложные операторы для выполнения операций высокого уровня, написанных как простые выражения.

Обратите внимание, что вы также можете использовать двоичные функции как операторы, если вы поместите их между обратными окнами:

main = print (a `foo` b)

foo :: Int -> Int -> Int
foo a b = a + b

И как таковой, вы также можете определить приоритет для них:

infixr 4 `foo`

Ответ 11

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"

Ответ 12

Избегание круглых скобок

Функции (.) и ($) в 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 -}

Ответ 13

Довольно охранники

Prelude определяет otherwise = True, в результате чего условия полной защиты читаются очень естественно.

fac n
  | n < 1     = 1
  | otherwise = n * fac (n-1)

Ответ 14

Перечисления C-Style

Объединение совпадений шаблонов верхнего уровня и арифметических последовательностей дает нам удобный способ определения последовательных значений:

foo : bar : baz : _ = [100 ..]    -- foo = 100, bar = 101, baz = 102

Ответ 15

Считываемая функциональная композиция

Prelude определяет (.) как математическую функцию; то есть g . f сначала применяется f, затем применяет g к результату.

Если вы import Control.Arrow, то следующие эквиваленты:

g . f
f >>> g

Control.Arrow предоставляет instance Arrow (->), и это хорошо для людей, которые не любят читать приложение-приложение назад.

Ответ 16

let 5 = 6 in ... действителен Haskell.

Ответ 17

Бесконечные списки

Поскольку вы упомянули о фибоначчи, существует очень элегантный способ генерировать числа фибоначчи из бесконечного списка, подобного этому:

[email protected](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 ленивой оценке параметров и списков.

Ответ 18

Гибкая спецификация импорта и экспорта модулей

Импорт и экспорт приятный.

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'

Ответ 19

Если вы ищете список или функцию более высокого порядка, она уже существует

В стандартной библиотеке есть много удобных функций и функций более высокого порядка.

-- factorial can be written, using the strict HOF foldl':
fac n = Data.List.foldl' (*) 1 [1..n]
-- there a shortcut for that:
fac n = product [1..n]
-- and it can even be written pointfree:
fac = product . enumFromTo 1

Ответ 20

Рациональное рассуждение

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

Это позволяет подставлять определения непосредственно в код, а с точки зрения оптимизации дает много возможностей компилятору о том, когда происходит материал.

Хороший пример этой формы рассуждения можно найти здесь:

http://www.haskell.org/pipermail/haskell-cafe/2009-March/058603.html

Это также хорошо проявляется в виде законов или правил, которые ожидаются для действительных членов экземпляра, например законов Монады:

  • returrn a → = f == f a
  • m → = return == m
  • (m → = f) → = g == m → = (\ x → f x → = g)

часто можно использовать для упрощения монадического кода.

Ответ 21

Усовершенствованное сопоставление шаблонов

  • Ленивые шаблоны
  • Необычные шаблоны

    let ~(Just x) = someExpression
    

См. соответствие шаблонов

Ответ 22

Учет параллельного списка

(Специальная функция GHC)

  fibs = 0 : 1 : [ a + b | a <- fibs | b <- tail fibs ]

Ответ 23

Лень

Вездесущая лень означает, что вы можете делать такие вещи, как define

fibs = 1 : 1 : zipWith (+) fibs (tail fibs)

Но это также дает нам много более тонких преимуществ с точки зрения синтаксиса и рассуждений.

Например, из-за строгости ML приходится иметь дело с ограничение значения и очень осторожно отслеживать циклические привязки let, но в Haskell, мы можем позволить каждому let быть рекурсивным и не нужно различать val и fun. Это удаляет основную синтаксическую бородавку с языка.

Это косвенно приводит к нашему прекрасному предложению where, потому что мы можем безопасно перемещать вычисления, которые могут использоваться или не использоваться из основного потока управления, и пусть лень обсуждает результаты.

Мы можем заменить (почти) все те функции стиля ML, которые должны принимать() и возвращать значение, с ленивым вычислением значения. Есть причины не делать этого время от времени, чтобы избежать утечки пространства с помощью CAF, но такие случаи встречаются редко.

Наконец, он допускает неограниченное eta-сокращение (\x -> f x можно заменить на f). Это делает комбинаторно-ориентированное программирование для таких вещей, как комбинаторы парсеров, намного приятнее, чем работа с подобными конструкциями на строгом языке.

Это помогает вам, когда рассуждает о программах в стиле без ссылок, или о переписывании их в стиле без точек и уменьшает шум аргументов.

Ответ 24

Перечисления

Любой тип, который является экземпляром 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]"

Ответ 25

Монады

Они не скрыты, но они просто повсюду, даже если вы не думаете о них (списки, возможно-типы)...