Каковы менее известные, но полезные функции языка программирования Haskell. (Я понимаю, что сам язык менее известен, но работайте со мной. Даже за объяснения простых вещей в Haskell, таких как определение последовательности Фибоначчи с помощью одной строчки кода, я буду поддерживать.)
Считаются ли «простые вещи» скрытыми функциями Haskell? Является ли этот вопрос списком обсуждения каждой функции Haskell?
Хорошо, почему это закрывается, когда есть то, что 103 некоторые другие вопросы с названием «Скрытая функция *» не закрыты, давай!





Дополнительный макет
Вы можете использовать явные фигурные скобки и точки с запятой вместо пробелов (также называемых макетом) для разделения блоков.
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
Вы можете использовать ключевые слова инфиксный, инфиксный или инфиксный для определения ассоциативности и приоритета операторов. Пример взят из Справка:
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 связывает левое, а infixr - право.
Это позволяет вам определять сложные операторы для выполнения высокоуровневых операций, записанных в виде простых выражений.
Обратите внимание, что вы также можете использовать двоичные функции в качестве операторов, если поместите их между обратными кавычками:
main = print (a `foo` b)
foo :: Int -> Int -> Int
foo a b = a + b
Таким образом, вы также можете определить для них приоритет:
infixr 4 `foo`
Разве это не обратные кавычки? Prelude> print (a foo b) 6
Да, это должны быть обратные кавычки. Неужели есть люди, которые считают это скрытой функцией?
Поменял галочки на обратные, моя ошибка. И считается ли это скрытой функцией или нет, это зависит от того, насколько хорошо вы знаете язык. Извините, если вам это не понравилось.
«Infix означает отсутствие ассоциативности» - как это возможно? Если я использую infix 6 +++, то как будет оцениваться 1 +++ 2 +++ 3? Мне кажется, это должен быть либо (1 +++ 2) +++ 3, либо 1 +++ (2 +++ 3). Даже те, которые могут оценивать одно и то же, Haskell должен выбрать то или иное, чтобы на самом деле делать, верно?
Ошибка анализа приоритета не может смешивать +++' [infix 6] and +++ '[инфикс 6] в одном и том же инфиксном выражении
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..] не внизу (что можно найти сразу), напечатает «готово» и выйдет.
main = [1..] `seq` print "done"
Что значит «не дно»?
Добавил объяснение. Это ответ на ваш вопрос?
По сути, «дно» - это любое неопределенное значение, такое как результат ошибки вызова. То, что зацикливается вечно, также называется дном.
Ах, спасибо, это проясняет! Как он может проверить, что что-то повторяется вечно? Разве это не проблема остановки, которую невозможно решить?
Кроме того, разве [1 ..] не зацикливается вечно? (По крайней мере, он генерирует бесконечное количество целых чисел
Я считаю, что если что-то зацикливается вечно, вызов seq также будет бесконечным. [1 ..] генерирует бесконечное количество целых чисел. Из-за лени они фактически не будут вычислены, пока вы не попытаетесь взять один, а seq этого не делает.
[1 ..] просто оценивается как 1: [2 ..], то есть список, который начинается с 1, за которым следует другой список. Он будет повторяться вечно, только если вы попытаетесь добраться до конца списка.
Кроме того, GHC обнаруживает (некоторые) бесконечные циклы - вы можете просто определить «loop = loop», но если вы попытаетесь распечатать значение «loop», это приведет к ошибке «<<loop>>».
больше информации о нижнем значении: haskell.org/haskellwiki/Bottom
Бесконечные списки
Поскольку вы упомянули фибоначчи, существует очень элегантный способ генерация чисел Фибоначчи из бесконечного списка, например:
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.
Расширения синтаксиса в GHC позволяют записать это сейчас: fib = 1: 1: [a + b | a <- fib | b <- хвостик]; однако я бы написал это 1: 1: zipWith (+) fib (tail fib)
Ваш первый пример на самом деле демонстрирует еще более скрытую особенность Haskell: вы можете использовать полные шаблоны при определении значений / функций!
Сокращение для общей операции со списком
Следующие варианты эквивалентны:
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]
Должен ли я разделить это на несколько ответов?
Вы бы предпочли получить 1 голос "за" (или, возможно, 1 "против" за несоблюдение правил вопроса) или 1 голос "за" за каждую функцию?
Ну тогда. А вот и наводнение ...
Я не совсем понимаю, как это работает - не могли бы вы объяснить это немного подробнее?
Список - это экземпляр Monad. >> = для конкатенации списков.
Это должно быть (>> =) = flip concatMap.
@Martijn: Верно. Думаю, никто другой не заметил, что (>>=) = concatMap не проверяет типы;)
После более подробного ознакомления с ней я думаю, что монада списка немного проблематична ... Жалоба в том, что это не единственный возможный способ заставить список следовать монадическим законам. Обратите внимание, что ответ гласит: «Если мы будем использовать реализацию» - но почему мы должны быть вынуждены использовать эту реализацию? Другая допустимая реализация была бы похожа на монаду Maybe, где пустой список эквивалентен Nothing, а непустой список эквивалентен Just first element ... Может быть, не лучший пример, но я думаю, что суть остается.
List Monad определенно не единственная возможная или разумная реализация, удовлетворяющая правилам Monad, но она задокументирована в отчете Haskell и является наиболее полезной. Если бы он работал как Maybe, [x | x <- xs] и do x <- xs; return x больше не были бы эквивалентными.
Читаемый функциональный состав
Prelude определяет (.) как композицию математической функции; то есть g . f сначала применяет f, а затем применяет g к результату.
Если у вас import Control.Arrow, следующие эквиваленты:
g . f
f >>> g
Control.Arrow предоставляет instance Arrow (->), и это хорошо для людей, которые не любят читать функциональное приложение задом наперед.
Отметим также, что (<<<) = (.) :)
Избегайте скобок
Функции (.) и ($) в 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 -}
Я предпочитаю скобки. Они более ясны.
Я предпочитаю исключать круглые скобки везде, где это возможно, даже когда я работаю на менее удобных языках, таких как C (при условии, что все читают мой код должен знать, все приоритеты операторов). Дело вкуса.
бессмысленные обозначения становятся по-настоящему выразительными, когда к ним привыкаешь. Это просто список последовательных глаголов.
Я пришел с Java, поэтому не использовать круглые скобки в Haskell - одно удовольствие.
Скобки - как раз правильный символ для вложенности. Зачем искать что-то другое?
Вложенные многострочные комментарии.
{- inside a comment,
{- inside another comment, -}
still commented! -}
Как насчет вложенных однострочных комментариев? ;-)
Довольно охранники
Prelude определяет otherwise = True, что делает считывание полных условий защиты очень естественным.
fac n
| n < 1 = 1
| otherwise = n * fac (n-1)
Если я правильно помню, GHC на самом деле рассматривает otherwise несколько иначе, чем True: используя True, вы получаете предупреждение о том, что вы не охватили все возможные случаи; с otherwise вы этого не сделаете.
@MatrixFrog Я не получаю предупреждения с True, но если я использую not False, True || False, True || True, True || undefined и т. д., Он предупреждает. (GHC 6.12.3, с параметром компиляции -Wall).
@Longpoke они, вероятно, пошли дальше и сделали то же самое для True, что они ранее сделали для otherwise. Я бы сказал, что, вероятно, было бы хорошо продолжать использовать otherwise просто потому, что он читается немного лучше.
Если вы ищете список или функцию более высокого порядка, она уже есть
В стандартной библиотеке так много удобных и высокоуровневых функций.
-- 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
Гибкая спецификация импорта и экспорта модулей
Импорт и экспорт - это хорошо.
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'
Мой мозг просто взорвался
Если вы попытаетесь скомпилировать этот код:
{-# 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
Я думаю, что следующее должно по-прежнему выдавать эту ошибку: ignorefoo f = 1 where (Foo a) = f
Ах, ты прав, который заставляет мозг GHC взорваться. Я не считать они еще исправили, но я не мог сломать мой простой пример ...
@ephemient Они не могут это исправить, потому что "исправление" нарушит систему типов Haskell. Подумайте об этом так: все в Haskell имеет тип. В строке where Foo a = f какой типа a?
Определяемые пользователем управляющие структуры
В Haskell нет сокращенного тернарного оператора. Встроенный 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 :("
Определенный таким образом, мне нужно поставить a или b в скобки, когда с, если я этого не сделаю. Здесь может помочь инфикс?
@ gorlum0: Конечно, просто дайте ему более высокую степень фиксации, чем $, например infixr 1 ? или что-то в этом роде.
Обобщенные алгебраические типы данных. Вот пример интерпретатора, в котором система типов позволяет охватить все случаи:
{-# 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
+1, Полностью зависимый типизированный язык программирования, такой как Agda, довольно сложно понять / работать с ним (по крайней мере, для меня :-). Но GADT уже приносят в Haskell некоторые из больших преимуществ зависимых типов, не добавляя слишком много умственной нагрузки для моего мозга.
В экземпляре Num вы имеете в виду "fromInteger = Num. FromInteger"?
Шаблоны в привязках верхнего уровня
five :: Int
Just five = Just 5
a, b, c :: Char
[a,b,c] = "abc"
Как это круто! Время от времени спасает вас от звонков в fromJust и head.
Google - ваш друг. Признаюсь, это не часть "ядра", поэтому cabal install 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, обязательно посмотрите hlint!
Hoogle также доступен в Интернете по адресу haskell.org/hoogle
Это лучший инструмент поиска API, с которым я когда-либо сталкивался. Потому что в большинстве случаев вы примерно знаете, что ищете; как в том, что он должен делать. Это позволяет вам искать все, что похоже на это.
Рассуждение по уравнениям
Haskell, будучи чисто функциональным, позволяет вам читать знак равенства как действительный знак равенства (при отсутствии неперекрывающихся шаблонов).
Это позволяет вам подставлять определения непосредственно в код, а с точки зрения оптимизации дает компилятору большую свободу действий в отношении того, когда что-то происходит.
Хороший пример такой формы рассуждений можно найти здесь:
http://www.haskell.org/pipermail/haskell-cafe/2009-March/058603.html
Это также хорошо проявляется в форме законов или прагм RULES, ожидаемых для действительных членов экземпляра, например, законы Монад:
часто можно использовать для упрощения монадического кода.
Бесплатные теоремы
Фил Уодлер познакомил нас с понятием свободная теорема, и с тех пор мы злоупотребляем ими в Haskell.
Эти замечательные артефакты систем типов в стиле Хиндли-Милнера помогают с эквациональными рассуждениями, используя параметричность, чтобы рассказать вам о том, что делает функция не будет.
Например, есть два закона, которым должен удовлетворять каждый экземпляр Functor:
Но теорема о свободе говорит нам, что нам не нужно беспокоиться о доказательстве первой, но, учитывая вторую, она приходит «бесплатно» только из сигнатуры типа!
fmap :: Functor f => (a -> b) -> f a -> f b
Вам нужно быть немного осторожнее с ленью, но это частично освещено в исходной статье и в более свежая статья Яниса Фойгтлендера о бесплатных теоремах при наличии seq.
+1 Совершенно не знал об этом, но это действительно круто; почему это не выше. Это избавляет от множества проверок, если вы можете получить результат прямо из сигнатуры типа. Читаю об этом сейчас.
Улучшенное сопоставление с образцом
Неопровержимые закономерности
let ~(Just x) = someExpression
Это дает ошибку компилятора: Происходит проверка: невозможно построить бесконечный тип: t = Возможно t
Монады
Они не так уж и скрыты, но они просто повсюду, даже там, где вы о них не думаете (Списки, Может быть-Типы) ...
Это действительно так, и монады, и другие классы типов, которые их окружают, настолько мощны, что было бы глупо использовать их на всех возможных языках. Я считаю, что в тринадцатом выпуске «The Monad.Reader» есть действительно хорошее объяснение того, как все это происходит вместе.
Понимание параллельного списка
(Специальная функция GHC)
fibs = 0 : 1 : [ a + b | a <- fibs | b <- tail fibs ]
Лень
Повсеместная лень означает, что вы можете делать такие вещи, как определять
fibs = 1 : 1 : zipWith (+) fibs (tail fibs)
Но он также дает нам гораздо более тонкие преимущества с точки зрения синтаксиса и рассуждений.
Например, из-за строгости ML должен иметь дело с ограничение стоимости и очень осторожно отслеживать циклические привязки let, но в Haskell мы можем позволить каждому let быть рекурсивным и не нужно различать val и fun. Это устраняет серьезную синтаксическую бородавку в языке.
Это косвенно приводит к нашему прекрасному предложению where, потому что мы можем безопасно перемещать вычисления, которые могут или не могут использоваться, из основного потока управления и позволять лени разобраться с разделением результатов.
Мы можем заменить (почти) все те функции в стиле ML, которым требуется take () и возвращать значение, просто с помощью ленивого вычисления значения. Есть причины не делать этого время от времени, чтобы избежать утечки места с помощью CAFs, но такие случаи редки.
Наконец, он позволяет неограниченное восстановление эта (\x -> f x можно заменить на f). Это делает комбинаторно-ориентированное программирование для таких вещей, как комбинаторы синтаксического анализатора, намного более приятным, чем работа с аналогичными конструкциями на строгом языке.
Это поможет вам при рассуждении о программах в бессмысленном стиле или о переписывании их в безточечный стиль и снижает шум аргументов.
Ссылка ограничения значения теперь мертва.
let 5 = 6 in ... - это действительный Haskell.
Позволяет ли 5 = 6 ... иметь какой-либо эффект?
Его постоянный образец 5 сопоставлен со значением 6, но поскольку let-привязки полностью ленивы, сопоставление никогда не будет выполнено.
Перечисления
В арифметической последовательности можно использовать любой тип, являющийся экземпляром 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]"
Когда вы впервые видите это, удивительно, что [A ..] на самом деле является конечным перечислением.
Перечисления в стиле C
Сочетание сопоставления с образцом верхнего уровня и арифметических последовательностей дает нам удобный способ определения последовательных значений:
foo : bar : baz : _ = [100 ..] -- foo = 100, bar = 101, baz = 102
См. Сообщения meta.stackexchange.com/questions/56669/…, meta.stackexchange.com/questions/57226/… и связанные с ними мета-сообщения.