【Haskell】分支表达式

分支结构是编程中非常重要的语法结构,在Go语言中有if-elseswitch-case,Haskell中也有对应的if-then-elsecase-of。不同的是Haskell中的分支结构都是表达式,而不是语句。事实上,在Haskell中任何东西都是表达式。即便是向putStrLn这样的打印函数也是表达式,也就是说在Haskell中任何东西都必须是有值的。

if-then-else

我们以一个判断奇偶数的例子开始。

oddEeven a = if even a 
    then "even"
    else "old"

习惯了其他语言的if-else,在面对Haskell的if-else-then时,会感觉很不习惯,因为它必须有一个值。在Go语言中,if语句表达的是如果xxx做什么,否则做什么。而在Haskell中的if表达式表达的是如果xxx是什么,否则是什么

我们在来实现一个简单的计算函数,感受if-then-else的嵌套。

caculate a b c = if b == '+' then Just(a + c)
            else if b == '-' then Just(a - c)
            else if b == '*' then Just(a * c)
            else if b == '/' then 
                 if c == 0 then Nothing
                           else Just(a / c)
            else Nothing 

guard

guard称为哨兵模式,它是对if语法的扩展,用来简化有许多if判断的情况下的代码编写。我们还是以上面的计算函数为例,首先让我们来看看Go语言对多if分支的优化。

func calculate(a, b float64, c byte) (float64, error) {
	switch {
	case c == '+':
		return a + b, nil
	case c == '-':
		return a - b, nil
	case c == '*':
		return a * b, nil
	case c == '/':
		if b == 0 {
			return 0, fmt.Errorf("divide zero")
		} else {
			return a / b, nil
		}
	default:
		return 0, fmt.Errorf("operation not support")
	}
}

Haskell的哨兵模式和Go语言的switch有些相似,写法上要简洁许多。

caculate' a b c | b == '+' = Just (a + c)
                | b == '-' = Just (a - c)
                | b == '*' = Just (a * c)
                | b == '/' && c == 0 = Nothing 
                | b == '/' = Just (a / c)
                | otherwise = Nothing 

注意看哨兵模式和普通函数在写法上的区别,Bool表达式出现在参数和=之间,以|开始,语法模式变成了函数名 参数 | 条件 = 值。除了哨兵模式,在函数参数和=之间是不会有其他内容的。这也是一个经常让初学者感到困惑不已的地方。

哨兵模式最后的otherwise相当于Go里面的defaultotherwise虽然也不是必须的,但是如果没有你会收到一条警告,并且当出现无法匹配的条件时,函数会直接崩溃。显然,遗漏otherwise并不是一个程序员该有的素养。

在哨兵模式中,条件会从上到下依次匹配。所以我们将b == '/' && c == 0放到了b=='/'的前面,当然也可以不去判断除数是否为零,因为在Haskell中除0会得到Infinity而不是崩溃,只是这里我们希望除0的结果是Nothing

哨兵模式不能自我嵌套,从它的语法结构就决定了这一点。至于=后面,只要是表达式就行,比如我们可以用if表达式重写上面的代码。

caculate'' a b c | b == '+' = Just (a + c)
                 | b == '-' = Just (a - c)
                 | b == '*' = Just (a * c)
                 | b == '/' = if c == 0 then Nothing else Just (a / c)
                 | otherwise = Nothing 

缩进对于Haskell来说非常重要,因为它会通过缩进来识别段落。但是无论编译器是否依赖于缩进,你都应该把你的代码对齐,因为这也是程序员的基本素养。

多路if

多路if英文名叫 MultiWayIf,这是Haskell的一个语言扩展,必须打开这个扩展才能支持多路if的语法。注意是语法级别的支持,开启的方式是在文件开头加上一行特殊的注释{-# LANGUAGE MultiWayIf #-}

{-# LANGUAGE MultiWayIf #-}

caculate''' a b c = if | b == '+' -> Just (a + c)
                       | b == '-' -> Just (a - c)
                       | b == '*' -> Just (a * c)
                       | b == '/' -> if c == 0 then Nothing else Just (a / c)
                       | otherwise -> Nothing 

MultiWayIf 要比else if简洁许多,看完简直想把else-if丢进垃圾桶。

case-of

case-of对于Go语言中的switch-case,不同的是,case-of也是表达式。

caculate'''' a b c = case b of '+' -> Just (a + c)
                               '-' -> Just (a - c)
                               '*' -> Just (a * c)
                               '/' -> case c of 0 -> Nothing
                                                _ -> Just (a / c)
                               _   -> Nothing 

case-of可以无限嵌套,但是作为一个素质的程序员,还是要禁止套娃。case-of使用_来匹配任何模式,或者任意值。语法上,_匹配可以不放在最后,但是它后面的模式将永远无法被匹配求值。

现在来思考一个问题:case-of做的是模式匹配,还是等值判断,还是两者皆有?

这个问题我现在也不能确定,但我更倾向于只有模式匹配,虽然这听起来很诡异。


小贴士:等号的误区

在Go语言中,=用来赋值,可以随意使用。然而到了Haskell中,=变成了一个很神奇的东西。特别是对于初学者,会对=感到很困惑,经常会在使用=时出现语法错误。这里我们就来好好理解理解Haskell中的=

在Haskell中,=的含义是绑定,而绑定的右边必须是表达式。无论是定义函数还是定义变量,都是绑定,它们并无多大区别,你甚至可以认为绑定变量就是定义了一个无参函数。

问题的原因就在于你无法在表达式中绑定变量。=只是产生一个绑定,而绑定本身并不是表达式,因此你无法将一个绑定作为表达式在绑定到另一个变量。这在任何语言中都是一样的,比如在Go语言中也无法写出a := b := 1这样的代码。

产生这一认知误区的另一个原因是等价转换错误。比如我们在Go语言中定义如下函数:

func add (a int) int {
	return a + 1
}

翻译到Haskell中如下:

add a = a + 1

请注意,上面两段代码并不是等价表达。和上面Haskell代码等价的Go语言表达代码是下面这个:

var addFn = add

你同样无法在add后面添加变量绑定。

但是我们知道,在Haskell中定义函数时是可以绑定变量的,这就是let-inwhere

let-in本身就是一个表达式,注意let x = 1let x = 1 in x的区别。let-in会产生一个很小的命名空间,在let中绑定的变量只在in中可见,整个表达式的值等于in中表达式的值。

where用于在函数中绑定顶层变量,在整个函数中可见,除了变量,也能绑定函数,非常方便。注意where本身并不是表达式,它只是函数语法的一部分。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值