[ Haskell  ]

Haskell 学习笔记

来自 Wikibook Yet Another Haskell Tutorial

语法

算术运算

Haskell 支持 +-*/^(幂)、sqrt(开方)这些算术运算,支持括号。

元组

元组是一种容器,里面的元素可以是不同类型的。

Prelude> (5,3)
(5,3)

二元组(pairs)操作的函数有

Prelude> fst (5, "hello")
5
Prelude> snd (5, "hello")
"hello"

列表

列表也是一种容器,里面的元素只能是同一种类型的。

Prelude> [1,2,3]
[1,2,3]

:cons 操作符,用于“连接”一个元素和一个列表。实际上[1,2,3]只是1:2:3:[]的语法糖。

Prelude> 0:[1,2]
[0,1,2]
Prelude> 5:[1,2,3,4]
[5,1,2,3,4]

对列表操作的函数有:

Prelude> length [1,2,3,4,10]
5
Prelude> head [1,2,3,4,10]
1
Prelude> length (tail [1,2,3,4,10])
4

字符串

在 Haskell 中,一个String实际上是Char的列表。我们可以这样创建字符串:

Prelude> 'H':'e':'l':'l':'o':[]
"Hello"

列表(当然包括字符串)可以使用++操作符进行列表/字符串连接。

Prelude> "Hello " ++ "World"
"Hello World"

非字符串的值和字符串之间通过这两个函数转换:

Prelude> "Five squared is " ++ show (5*5)
"Five squared is 25"
Prelude> read "5" + 3
8

重要的列表函数

绝大多数 Haskell 程序都是通过操作列表完成的。操作列表有三个主要的函数:

Prelude> map Data.Char.toUpper "Hello World"
"HELLO WORLD"

Prelude> filter Data.Char.isLower "Hello World"
"elloorld"

Prelude> foldr (+) 0 [3,8,12,5]
28

源代码文件

Haskell 源码以.hs结尾。在交互式解释器中输入:load xxx.hs(或直接:l xxx.hs)来加载源码。或者直接通过命令行加载:

$> ghci xxx.hs

一个程序(而非模块的)的源码中必须有一个叫做main的函数,它的模块名也必须是Main。使用下面的命令编译:

$> ghc --make Main.hs -o main.exe

函数

函数的定义形式为:

square x = x * x

此时square x就等于x * x,因此square是一个函数。

当你更改了源代码后,可以在解释器中输入:reload xxx.hs:r xxx.hs来重新加载源程序。

函数中使用if...then...else...then...语句:

signum x =
    if x < 0
      then -1
      else if x > 0
        then 1
        else 0

注意:在 Haskell 中的条件判断必须有 then else 从句。

Haskell 也支持 case 结构:

f x =
    case x of
      0 -> 1
      1 -> 5
      2 -> 2
      _ -> -1

其中_为通配符。

Haskell 可以使用 layout 来控制程序结构,和 Python 比较接近,即使用缩进来控制结构。如果不想使用 layout,则必须显式地使用花括号和分号。

f x = case x of
        { 0 -> 1 ; 1 -> 5 ; 2 -> 2 ; _ -> -1 }

此时缩进可以比较随意。

函数也可以分段定义:

f 0 = 1
f 1 = 5
f 2 = 2
f _ = -1

更复杂的函数可以通过函数合成来构造:

Test> square (f 1)
25
Test> f (square 1)
5

注意括号是必须的。也可以通过.函数来完成函数的合成,比如f . g就是先 g 后 f:

Test> (square . f) 1
25
Test> (f . square) 1
5

注意括号也是必须的。

在 Prelude 中预置了一些函数,比如:

Name Usage
sqrt 平方根
id 返回参数本身,id x = x
null 告诉你一个列表是否是空的
== 检查两个对象是否相等
/= 检查两个对象是否不等

还有fst, snd, head, tail, ++在之前已经提到过了。

let 绑定

通过 let/in 语句可以创建在函数中的局部“变量/值”。例如下面是计算二次方程的根的函数:

roots a b c =
    let discr = sqrt (b*b - 4*a*c)
    in  ((-b + discr) / (2*a),
         (-b - discr) / (2*a))

事实上你可以在一个let中提供多个声明,只要它们缩进相同即可,否则会引发错误:

roots a b c =
    let discr = sqrt (b*b - 4*a*c)
        twice_a = 2*a
    in  ((-b + discr) / twice_a,
         (-b - discr) / twice_a)

中缀函数(Infix)

中缀函数是由符号组成的,而不是用字母组成的,例如(+), (*), (++)都是中缀函数。想要在非中缀模式下使用它们,你只要用括号把它们包裹起来即可,如下面两行代码等价:

Prelude> 5 + 10
15
Prelude> (+) 5 10
15

同样地,非中缀函数(如map)可以用反引号扩起来,就能在中缀模式下使用,如下面两行代码等价:

Prelude> map Data.Char.toUpper "Hello World"
"HELLO WORLD"
Prelude> Data.Char.toUpper `map` "Hello World"
"HELLO WORLD"

注释

在 Haskell 中,--是行注释,而{--}是块注释。

递归

Haskell 里没有循环,因为循环必须用到 destructive update 来更新 index。Haskell 用递归。

例如在 Haskell 里的计算阶乘的函数为:

factorial 1 = 1
factorial n = n * factorial (n-1)

计算一个列表的长度的函数:

my_length [] = 0
my_length (x:xs) = 1 + my_length xs

交互性

在 Haskell 中,I/O 操作是通过 Monad 实现的。

使用 do 关键字来实现执行一系列命令,例如:

module Main
    where

import System.IO

main = do
  hSetBuffering stdin LineBuffering  -- 当 GHC 读取输入时,使用行缓冲
  putStrLn "Please enter your name: "
  name <- getLine  -- 也可写作 name = getLine,但 getLine 不是一个真正的函数,它的返回值不确定
  putStrLn ("Hello, " ++ name ++ ", how are you?")

除了 getLine 以外,返回随机数的函数也不是真正的函数,例如randomRIO。下面这个程序是“猜数字”程序:

module Main
    where

import System.IO
import System.Random

main = do
  hSetBuffering stdin LineBuffering
  num <- randomRIO (1::Int, 100)
  putStrLn "I'm thinking of a number between 1 and 100"
  doGuessing num

doGuessing num = do
  putStrLn "Enter your guess:"
  guess <- getLine
  let guessNum = read guess
  if guessNum < num
    then do putStrLn "Too low!"
            doGuessing num
    else if guessNum > num
           then do putStrLn "Too high!"
                   doGuessing num
           else do putStrLn "You Win!"