32

Haskell 编程语言有哪些鲜为人知但有用的特性。(我知道该语言本身鲜为人知,但请与我合作。即使是对 Haskell 中简单事物的解释,例如用一行代码定义斐波那契数列,也会得到我的支持。)

  • 尝试限制对 Haskell 核心的回答
  • 每个答案一个功能
  • 提供功能的示例和简短描述,而不仅仅是文档链接
  • 使用粗体标题作为第一行标记特征
4

25 回答 25

40

用户定义的控制结构

Haskell 没有速记三元运算符。内置的if- then-else始终是三元的,并且是一个表达式(命令式语言往往有?:=expression,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 :("
于 2008-10-17T21:49:02.253 回答
28

胡格尔

胡格尔是你的朋友。我承认,它不是“核心”的一部分,所以cabal install hoogle

现在您知道“如果您正在寻找高阶函数,它已经存在”(ehemient 的评论)。但是您如何找到该功能?与胡歌!

$ 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!

于 2009-06-23T22:45:42.333 回答
25

我的大脑刚刚爆炸

如果您尝试编译此代码:

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

您将收到此错误消息:

$ ghc Foo.hs

Foo.hs:3:22:
    我的脑袋简直炸裂了。
    我无法处理存在量化构造函数的模式绑定。
    相反,使用 case-expression 或 do-notation 来解压缩构造函数。
    在绑定组中
        富一
    在模式绑定中:Foo a = f
    在 `ignorefoo' 的定义中:
        忽略foo f = 1
                    在哪里
                        富a = f
于 2008-10-17T19:02:06.490 回答
22

自由定理

Phil Wadler 向我们介绍了自由定理的概念,从那时起我们就一直在 Haskell 中滥用它们。

Hindley-Milner 风格的类型系统的这些奇妙工件通过使用参数告诉您函数不会做什么来帮助进行等式推理。

例如,Functor 的每个实例都应满足两条定律:

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

但是,自由定理告诉我们,我们不必费心证明第一个,但考虑到第二个,它只是来自类型签名的“自由”!

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

你需要对惰性有点小心,但这在原始论文和 Janis Voigtlender最近关于存在seq.

于 2009-07-06T17:46:13.053 回答
20

常用列表操作的简写

以下是等价的:

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

AMonad有一个绑定操作,在 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]
于 2008-10-17T18:14:31.157 回答
19

可嵌套的多行注释

{- inside a comment,
     {- inside another comment, -}
still commented! -}
于 2008-10-17T19:00:43.560 回答
19

广义代数数据类型。这是一个示例解释器,其中类型系统可让您涵盖所有情况:

{-# 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
于 2009-04-15T01:54:02.467 回答
18

顶级绑定中的模式

five :: Int
Just five = Just 5

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

多么酷啊!fromJust不时为您节省电话head

于 2009-06-05T08:54:07.330 回答
17

可选布局

您可以使用显式大括号和分号而不是空格(也称为布局)来分隔块。

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 程序可以直接由其他程序生成。

于 2008-10-17T13:00:04.660 回答
16

操作员固定性

您可以使用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 关联左,infixr 关联右。

这允许您定义复杂的运算符来执行编写为简单表达式的高级操作。

请注意,如果将二进制函数放在反引号之间,也可以将它们用作运算符:

main = print (a `foo` b)

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

因此,您还可以为它们定义优先级:

infixr 4 `foo`
于 2008-10-17T13:27:09.333 回答
16

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"
于 2008-10-17T13:34:56.540 回答
15

避免括号

中的(.)and($)函数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 -}
于 2008-10-17T19:00:19.880 回答
15

漂亮的警卫

Prelude定义otherwise = True,使完整的保护条件读起来很自然。

fac n
  | n < 1     = 1
  | otherwise = n * fac (n-1)
于 2008-10-17T19:01:06.087 回答
15

C 样式枚举

结合顶级模式匹配和算术序列为我们提供了一种定义连续值的便捷方法:

foo : bar : baz : _ = [100 ..]    -- foo = 100, bar = 101, baz = 102
于 2009-12-14T14:12:35.123 回答
13

可读函数组合

Prelude定义(.)为数学函数组合;也就是说,g . f首先应用f,然后应用g到结果。

如果你import Control.Arrow,以下是等价的:

g . f
f >>> g

Control.Arrow提供了一个instance Arrow (->), 这对于不喜欢向后阅读函数应用程序的人来说很好。

于 2008-10-17T18:59:43.157 回答
11

let 5 = 6 in ...是有效的 Haskell。

于 2009-10-08T08:51:26.090 回答
10

无限列表

既然你提到了斐波那契,有一种非常优雅的方法可以从这样的无限列表中生成斐波那契数:

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 对参数和列表的惰性求值。

于 2008-10-17T16:02:12.780 回答
9

灵活的模块导入导出规范

导入和导出很好。

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'
于 2008-10-17T19:01:51.123 回答
8

如果您正在寻找列表或高阶函数,它已经存在

标准库中有太多便利和高阶函数。

-- 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
于 2008-10-17T19:01:30.920 回答
8

等式推理

Haskell,纯粹是功能性的,允许您将等号读取为真正的等号(在没有非重叠模式的情况下)。

这允许您将定义直接替换到代码中,并且在优化方面为编译器提供了很多关于何时发生的事情的余地。

这种推理形式的一个很好的例子可以在这里找到:

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

这也很好地体现在实例的有效成员所期望的法律或 RULES pragma 的形式中,例如 Monad 法律:

  1. 返回一个 >>= f == fa
  2. m >>= 返回 == m
  3. (m >>= f) >>= g == m >>= (\x -> fx >>= g)

通常可以用来简化一元代码。

于 2009-07-06T17:38:53.503 回答
6

增强的模式匹配

  • 懒惰的模式
  • 无可辩驳的模式

    let ~(Just x) = someExpression
    

查看模式匹配

于 2009-07-06T18:26:23.750 回答
6

并行列表理解

(特殊 GHC 功能)

  fibs = 0 : 1 : [ a + b | a <- fibs | b <- tail fibs ]
于 2009-07-06T18:31:18.843 回答
6

懒惰

无处不在的懒惰意味着你可以做一些事情,比如定义

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

但它也在语法和推理方面为我们提供了很多更微妙的好处。

例如,由于严格 ML 必须处理值限制,并且非常小心地跟踪循环 let 绑定,但是在 Haskell 中,我们可以让每个 let 都是递归的,并且不需要区分valand fun。这从语言中消除了一个主要的语法缺陷。

这间接产生了我们可爱的where子句,因为我们可以安全地将可能会或可能不会使用的计算移出主控制流,并让惰性处理共享结果。

我们可以(几乎)替换所有需要获取 () 并返回值的 ML 样式函数,只需对值进行惰性计算。有理由不时避免这样做,以避免CAF泄漏空间,但这种情况很少见。

最后,它允许不受限制的 eta 减少(\x -> f x可以用 f 代替)。这使得像解析器组合器这样的面向组合器的编程比在严格的语言中使用类似的结构更令人愉快。

这有助于您以无点风格推理程序,或将它们重写为无点风格并减少争论噪音。

于 2009-07-07T13:00:51.723 回答
5

枚举

任何作为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]"
于 2009-12-14T14:06:08.367 回答
3

单子

它们并没有那么隐蔽,但它们无处不在,即使你没有想到它们(列表,Maybe-Types)......

于 2009-07-06T18:28:24.807 回答