在Go中,函数是工具;在Haskell中,函数是艺术。让我们看看这种转变是如何发生的。
咱们Go程序员写函数是这样的:
func add(a int, b int) int {
return a + b
}
但是Haskell里写函数是这样的:
add :: Int -> Int -> Int
add a b = a + b
等等,这里没有func关键字!没有return关键字!没有花括号!这是什么鬼?
先看看咱们熟悉的Go函数是怎么写的:
package main
import "fmt"
// 基本函数
func add(a int, b int) int {
return a + b
}
// 多返回值
func divide(a int, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
// 可变参数
func sum(numbers ...int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}
func main() {
result := add(5, 3)
fmt.Printf("5 + 3 = %d
", result)
quotient, err := divide(10, 2)
if err != nil {
fmt.Printf("Error: %v
", err)
} else {
fmt.Printf("10 / 2 = %d
", quotient)
}
total := sum(1, 2, 3, 4, 5)
fmt.Printf("Sum of 1,2,3,4,5 = %d
", total)
}
再看看Haskell是怎么搞的:
-- 基本函数
add :: Int -> Int -> Int
add a b = a + b
-- 处理除零错误(使用Maybe类型)
divide :: Int -> Int -> Maybe Int
divide a 0 = Nothing
divide a b = Just (a `div` b)
-- 列表求和
sumList :: [Int] -> Int
sumList [] = 0
sumList (x:xs) = x + sumList xs
main :: IO ()
main = do
let result = add 5 3
putStrLn $ "5 + 3 = " ++ show result
let quotient = divide 10 2
case quotient of
Nothing -> putStrLn "Error: division by zero"
Just q -> putStrLn $ "10 / 2 = " ++ show q
let total = sumList [1, 2, 3, 4, 5]
putStrLn $ "Sum of [1,2,3,4,5] = " ++ show total
咱们来写两个文件,看看这两种函数写法到底有什么不同:
package main
import "fmt"
// 计算阶乘
func factorial(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n-1)
}
// 判断是否为偶数
func isEven(n int) bool {
return n%2 == 0
}
// 过滤偶数
func filterEven(numbers []int) []int {
var result []int
for _, num := range numbers {
if isEven(num) {
result = append(result, num)
}
}
return result
}
func main() {
// 测试阶乘
fmt.Printf("Factorial of 5: %d
", factorial(5))
// 测试偶数判断
fmt.Printf("Is 4 even? %t
", isEven(4))
fmt.Printf("Is 5 even? %t
", isEven(5))
// 测试过滤
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
evens := filterEven(numbers)
fmt.Printf("Even numbers: %v
", evens)
}
-- 计算阶乘
factorial :: Int -> Int
factorial 0 = 1
factorial 1 = 1
factorial n = n * factorial (n - 1)
-- 判断是否为偶数
isEven :: Int -> Bool
isEven n = n `mod` 2 == 0
-- 过滤偶数
filterEven :: [Int] -> [Int]
filterEven [] = []
filterEven (x:xs)
| isEven x = x : filterEven xs
| otherwise = filterEven xs
main :: IO ()
main = do
-- 测试阶乘
putStrLn $ "Factorial of 5: " ++ show (factorial 5)
-- 测试偶数判断
putStrLn $ "Is 4 even? " ++ show (isEven 4)
putStrLn $ "Is 5 even? " ++ show (isEven 5)
-- 测试过滤
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let evens = filterEven numbers
putStrLn $ "Even numbers: " ++ show evens
Go中的函数签名:
func add(a int, b int) int
Haskell中的函数签名:
add :: Int -> Int -> Int
从程序员的角度对比理解:
Haskell的类型签名Int -> Int -> Int表明:接受一个Int,返回一个接受Int并返回Int的函数。这叫做柯里化(Currying)。
Haskell中的函数是纯函数,这意味着:
对比Go:
// Go中可能有副作用
var globalCounter int = 0
func increment() int {
globalCounter++ // 副作用:修改全局变量
return globalCounter
}
-- Haskell中纯函数
increment :: Int -> Int
increment x = x + 1 -- 没有副作用,总是返回x+1
Haskell中的函数定义可以使用模式匹配:
-- 阶乘函数使用模式匹配
factorial :: Int -> Int
factorial 0 = 1 -- 模式1:输入是0
factorial 1 = 1 -- 模式2:输入是1
factorial n = n * factorial (n - 1) -- 模式3:其他情况
这比Go中的if-else更清晰:
func factorial(n int) int {
if n == 0 {
return 1
} else if n == 1 {
return 1
} else {
return n * factorial(n-1)
}
}
Haskell中的函数都是柯里化的:
-- 这个函数
add :: Int -> Int -> Int
add a b = a + b
-- 等价于
add :: Int -> (Int -> Int)
add a = -> a + b
-- 可以部分应用
add5 :: Int -> Int
add5 = add 5
-- 使用
result = add5 3 -- 结果是8
Go中需要显式创建部分应用的函数:
func add(a, b int) int {
return a + b
}
func add5(b int) int {
return add(5, b)
}
让我们创建一个更复杂的数学计算器来展示函数式编程的威力:
package main
import (
"fmt"
"math"
)
// 计算平方
func square(x float64) float64 {
return x * x
}
// 计算距离
func distance(x1, y1, x2, y2 float64) float64 {
dx := x2 - x1
dy := y2 - y1
return math.Sqrt(dx*dx + dy*dy)
}
// 判断点是否在圆内
func isInCircle(centerX, centerY, radius, x, y float64) bool {
dist := distance(centerX, centerY, x, y)
return dist <= radius
}
func main() {
// 测试平方函数
fmt.Printf("Square of 5: %.2f
", square(5))
// 测试距离函数
dist := distance(0, 0, 3, 4)
fmt.Printf("Distance from (0,0) to (3,4): %.2f
", dist)
// 测试圆内判断
inCircle := isInCircle(0, 0, 5, 3, 4)
fmt.Printf("Point (3,4) in circle (0,0,5): %t
", inCircle)
}
-- 计算平方
square :: Double -> Double
square x = x * x
-- 计算距离
distance :: Double -> Double -> Double -> Double -> Double
distance x1 y1 x2 y2 = sqrt (dx * dx + dy * dy)
where
dx = x2 - x1
dy = y2 - y1
-- 判断点是否在圆内
isInCircle :: Double -> Double -> Double -> Double -> Double -> Bool
isInCircle centerX centerY radius x y = dist <= radius
where
dist = distance centerX centerY x y
main :: IO ()
main = do
-- 测试平方函数
putStrLn $ "Square of 5: " ++ show (square 5)
-- 测试距离函数
let dist = distance 0 0 3 4
putStrLn $ "Distance from (0,0) to (3,4): " ++ show dist
-- 测试圆内判断
let inCircle = isInCircle 0 0 5 3 4
putStrLn $ "Point (3,4) in circle (0,0,5): " ++ show inCircle
-- 正确:函数应用不需要括号
add 5 3
-- 错误:不要用括号
add(5, 3) -- 这会报错
-- 推荐:显式声明类型
factorial :: Int -> Int
factorial n = if n <= 1 then 1 else n * factorial (n - 1)
-- 不推荐:省略类型声明
factorial n = if n <= 1 then 1 else n * factorial (n - 1)
-- 错误:更具体的模式应该放在前面
factorial :: Int -> Int
factorial n = n * factorial (n - 1) -- 这会无限递归
factorial 0 = 1
-- 正确:具体模式在前
factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n - 1)
从Go的func到Haskell的纯函数,我们看到了编程思维的转变。虽然语法看起来有点不同,但纯函数和模式匹配的概念已经开始展现函数式编程的魅力。
接下来,我们将深入Haskell的类型系统,那里有更多令人惊叹的设计等待着我们。