美文网首页
《Real World Haskell》笔记(4):函数式编程

《Real World Haskell》笔记(4):函数式编程

作者: Mexplochin | 来源:发表于2018-12-13 21:48 被阅读0次
一个简单的命令行程序
-- file InteractWith.hs
import System.Environment (getArgs)

interactWith function inputFile outputFile = do
  inputStr <- readFile inputFile
  writeFile outputFile (function inputStr)

main = mainWith myFunction
  where mainWith function = do
          args <- getArgs
          case args of
            [inputFile,outputFile] -> interactWith function inputFile outputFile
            _ -> putStrLn "error: exactly two arguments needed"

        --replace id with other function below when you need
        --id 函数接受一个值,并原封不动地返回这个值
        myFunction = id
  • 该接口程序读入一个文件,将函数应用到文件,并且将结果写到另一个文件
  • do 关键字引入一个块,标识那些带有副作用的代码,比如对文件进行读和写操作,被 do 包围的 <- 操作符效果等同于赋值
分离多行文本
-- file SplitLines.hs
splitLines [] = []
splitLines cs =
    let (pre, suf) = break isLineTerminator cs
    in  pre : case suf of
                ('\r':'\n':rest) -> splitLines rest
                ('\r':rest)      -> splitLines rest
                ('\n':rest)      -> splitLines rest
                _                -> []
isLineTerminator c = c == '\r' || c == '\n'
  • break函数把一个列表分成两部分,它的第一个参数是一个函数,这个函数必须去检查列表中的元素,并且返回一个Bool值来表示列表是否在那个元素处被分开
  • break函数返回一个二元组,凭借一个谓词(即一个返回Bool值的函数)判断是否返回第一个True之前的元素构成的列表(前缀)和剩下的元素构成的列表(后缀)
行终止符转换程序

Windows系统与类Unix系统的读、写文件操作是不同的。Windows系统用文本模式读取一个文件的时候,文件I/O库把行终止序列“\r\n”(回车后跟着换行)转换成“\n”(单个换行),写一个文件的时候,文件I/O库会做相反的事情;而类Unix系统上,文本模式读写不做任何的转换工作。

以lines函数为例,由于它仅仅在换行符处分离文本,会留下回车跟在行的结尾处,如果在Linux或Unix上读取一个Windows生成的文本文件,会在每一行的结尾处得到尾随的回车;如果在Windows系统上读取一个在类Unix系统上写入的文件,行终止符会变得很乱。这使得行终止符转换程序具有现实意义。

--file fixLines.hs
import System.Environment (getArgs)

isLineTerminator c = c == '\r' || c == '\n'

splitLines [] = []
splitLines cs =
    let (pre, suf) = break isLineTerminator cs
    in  pre : case suf of
                ('\r':'\n':rest) -> splitLines rest
                ('\r':rest)      -> splitLines rest
                ('\n':rest)      -> splitLines rest
                _                -> []

fixLines inputStr = unlines (splitLines inputStr)

interactWith function inputFile outputFile = do
  inputStr <- readFile inputFile
  writeFile outputFile (function inputStr)

main = mainWith myFunction
  where mainWith function = do
          args <- getArgs
          case args of
            [inputFile,outputFile] -> interactWith function inputFile outputFile
            _ -> putStrLn "error: exactly two arguments needed"
        myFunction = fixLines
  • lines函数 在行边界上分离一段文本字符串,返回一个忽略了行终止字符的字符串列表
  • unlines函数 将由字符串组成的列表串联起来,并且在每个字符串元素的末尾加上换行符
  • words函数 利用任何空格符分割字符串
  • unwords函数 利用一个空格符把由字符串构成的列表连接起来
中缀函数

如果一个函数或构造器带两个或更多的参数,可以选择使用中缀形式,用中缀表示法定义或应用一个函数或值构造器,需用反引号包围它的名称

--中缀函数
a `plus` b = a + b
--中缀值构造器
data a `Pair` b = a `Pair` b
                  deriving (Show)
-- we can use the constructor either prefix or infix
foo = Pair 1 2
bar = True `Pair` "quux"
练习
--file myfunc.hs
--写一些安全的列表函数
safeHead::[a]->Maybe a
safeHead []=Nothing
safeHead (x:xs)=Just x

safeTail::[a]->Maybe [a]
safeTail []=Nothing
safeTail (x:xs)=Just xs

--写一个和words功能近似的函数splitWith函数
--输入一个谓词和一个任意类型元素组成的列表
--在使谓词返回False的元素处分割这个列表
splitWith::(a->Bool)->[a]->[[a]]
splitWith func xs=
  let (pre,suf)=span func xs
  in pre : case suf of
             (x:rest)->splitWith func rest
             _->[]
--file firstWord.hs
--利用命令行框架,编写一个打印输入数据的每一行的第一个单词的程序
import System.Environment (getArgs)

getfst::String->String
getfst oneline=head (words oneline)

getAllfst::[String]->[String]
getAllfst (oneline:restlines)=(getfst oneline):(getAllfst restlines)
getAllfst _=[]

getOutputStr::String->String
getOutputStr inputStr=unlines (getAllfst (lines inputStr))

interactWith function inputFile outputFile = do
  inputStr <- readFile inputFile
  writeFile outputFile (function inputStr)

main = mainWith myFunction
  where mainWith function = do
          args <- getArgs
          case args of
            [inputFile,outputFile] -> interactWith function inputFile outputFile
            _ -> putStrLn "error: exactly two arguments needed"
        myFunction = getOutputStr
--file transpose.hs
--转置文本程序 把“hello\nworld\n”转换成“hw\neo\nlr\nll\nod\n”
import System.Environment (getArgs)

transform::[Char]->[[Char]]
transform (c:cs)=[c]:transform cs
transform _=[]

transpose func strs=zipWith (:) (head strs) (func (last strs))

getOutputStr inputStr=unlines (transpose transform (lines inputStr))

interactWith function inputFile outputFile = do
  inputStr <- readFile inputFile
  writeFile outputFile (function inputStr)

main = mainWith myFunction
  where mainWith function = do
          args <- getArgs
          case args of
            [inputFile,outputFile] -> interactWith function inputFile outputFile
            _ -> putStrLn "error: exactly two arguments needed"
        myFunction = getOutputStr

循环的表示 显式递归

-- file IntParse.hs
import Data.Char (digitToInt)-- 只载入 Data.Char 中的 digitToInt 函数

--尾递归函数:如果输入非空,函数在尾模式中递归地调用自身
--基本情形(base case)处理空列表
--递归情形(recursive case)处理非空列表,对头元素进行某种操作,对余下的其他元素递归地处理
loop :: Int -> String -> Int
loop acc [] = acc
loop acc (x:xs) = let acc' = acc * 10 + digitToInt x
                  in loop acc' xs
asInt :: String -> Int
asInt xs = loop 0 xs

{-
import Data.Char(digitToInt)

asInt::String->Int
asInt xs=loop 0 xs
  where loop acc xs=
          case xs of
            []->acc
            (x:xs_)->loop acc_ xs_
              where acc_=acc*10 + digitToInt x
-}
//对应C函数
int asint(char *str){
    int acc; // accumulate the partial result
    for (acc = 0; isdigit(*str); str++)
        acc = acc * 10 + (*str -'0');
    return acc;
}

上述代码展示了一个惯用的结构递归(structural recursion)法:通过研究列表的结构,分别处理空列表和非空列表两种状况;非递归情形(空列表)被称为基本情形(终止情形、归纳情形),当对函数进行递归调用时,计算最终会回到基本情形上。

对列表元素进行转换

以显式递归的方法为例

import Data.Char (toUpper)
upperCase :: String -> String
upperCase (x: xs) = toUpper x : upperCase xs
upperCase []      = []

square :: [Double] -> [Double]
square (x:xs) = x*x : square xs
square []     = []
列表映射 map

使用 map 重写的 square 和 upperCase 函数如下,

import Data.Char (toUpper)
upperCase2 xs = map toUpper xs

square2 xs = map squareOne xs
    where squareOne x = x * x

因为map 的抽象出现在 square 和 upperCase 函数,所以可以通过观察这两个函数,找出它们之间的共同点,然后实现自己的 map 函数。

-- file myMap.hs
myMap :: (a -> b) -> [a] -> [b]
myMap f (x:xs) = f x : myMap f xs
myMap _ [] = []
筛选列表元素 filter

筛选函数的递归情形比之前的 map 函数要复杂一些,它使用守卫对元素进行条件判断,只有符合条件的元素,才会被加入进结果列表里。

-- file oddList.hs
oddList :: [Int] -> [Int]
oddList (x:xs) | odd x     = x : oddList xs
               | otherwise = oddList xs
oddList []                 = []

filter的显式递归定义如下

--file myfilter.hs
myfilter :: (a -> Bool) -> [a] -> [a]
myfilter p [] = []
myfilter p (x:xs)
    | p x       = x : myfilter p xs
    | otherwise = myfilter p xs
处理集合并得出结果 reduce

reduce将集合缩减为一个值,这种递归计算是纯函数语言里表示 loop 最自然的方式,

-- file mySum.hs
mySum xs = helper 0 xs
    where helper acc (x:xs) = helper (acc + x) xs
          helper acc []     = acc

helper 函数就是一个reduce,它通过尾递归进行计算,acc 是累积器(accumulator)参数,记录了当前列表元素的总和。

折叠 foldl foldr

折叠函数对一个列表中的所有元素做某种处理,并且一边处理元素,一边更新累积器,最后在处理完整个列表之后,返回累积器的值

-- file fold.hs
foldl :: (a -> b -> a) -> a -> [b] -> a
foldl step acc (x:xs) = foldl step (step acc x) xs
foldl _ acc []        = acc
--niceSum xs = foldl (+) 0 xs
--niceSum [1, 2, 3]
--    == foldl (+) 0                   (1:2:3:[])
--    == foldl (+) (0 + 1)             (2:3:[])
--    == foldl (+) ((0 + 1) + 2)       (3:[])
--    == foldl (+) (((0 + 1) + 2) + 3) []
--    == (((0 + 1) + 2) + 3)

foldr :: (b -> a -> a) -> a -> [b] -> a
foldr step acc (x:xs) = step x (foldr step acc xs)
foldr _ acc []        = acc
--niceSumFoldr xs = foldr (+) 0 xs
--niceSumFoldr [1, 2, 3]
--    == foldr (+) 0 (1:2:3:[])
--    == 1 +           foldr (+) 0 (2:3:[])
--    == 1 + (2 +      foldr (+) 0 (3:[]))
--    == 1 + (2 + (3 + foldr (+) 0 []))
--    == 1 + (2 + (3 + 0))

关于foldr如何工作的直觉性解释是它用zero初始值替代了空列表,并且调用step函数替代列表的每个值构造器。实际上则应将它看成是对输入列表的一种转换,它的第一个参数决定了该怎么处理列表的 head 和 tail 部分;而它的第二个参数则决定了,当遇到空列表时,该用什么值来代替这个空列表

foldr的作用

所有可以用 foldr 定义的函数,统称为主递归(primitive recursive)

--file mfr.hs
--通过foldr定义filter
foldrFilter func xs=foldr step [] xs
  where step x acc | func x = x:acc
                  | otherwise =acc

--通过foldr定义map
foldrMap func xs=foldr step [] xs
  where step x acc=(func x):acc

--通过foldr定义foldl
foldrFoldl::(a->b->a)->a->[b]->a
foldrFoldl fstep acc xs=foldr step id xs acc
  where step x g a=g (fstep a x)
-- xs@(x:xs') and xs'@(x':xs'') and ...
-- foldr step id xs acc
-- ==step x (foldr step id xs') acc                   
--   -> ( foldr step id xs'                                 ) (fstep acc x)
-- ==step x (step x' (foldr step id xs'')) acc        
--   -> ( (foldr step id xs'')                  (fstep _ x')) (fstep acc x)
-- ==step x (step x' (step x'' (foldr step id xs''')))
--   -> ( ((foldr step id xs''')(fstep _' x'')) (fstep _ x')) (fstep acc x)
-- ...because foldr _ id []=id, let xs'''=[]
--   -> ( ( id                  (fstep _' x'')) (fstep _ x')) (fstep acc x)            

--通过foldr定义恒等identity转换
identity :: [a] -> [a]
identity xs = foldr (:) [] xs

--通过foldr定义++
append :: [a] -> [a] -> [a]
append xs ys = foldr (:) ys xs
匿名函数

Haskell的匿名函数以反斜杠符号 \ 为开始,后跟函数的参数(可以包含模式),函数体定义在 -> 符号之后。因为匿名函数从 lambda 演算而来,所以匿名函数通常也被称为 lambda 函数。lambda 函数的定义只能有一条语句,这种局限性,限制了在 lambda 定义中可使用的模式,也就是说,lambda函数只能表示一种情形,所以要注意别将函数应用到错误的模式。

unsafeHead = \(x:_) -> x
--unsafeHead [] 导致 *** Exception: unsafeHead.hs:2:14-24: Non-exhaustive patterns in lambda
部分函数应用和柯里化

在 Haskell 中,所有函数都只接受一个参数。所谓的多参函数,实际上是接受一个参数并返回另一个函数,这个被返回的函数也接受一个参数,最后返回值。柯里化即函数的部分应用(partial application of the function)指 函数正被它的其中几个参数(传入参数的数量,少于函数所能接受参数的数量)所应用。

节 Section

使用括号包围一个中序操作符,通过在括号里面提供左操作对象或者右操作对象,产生一个部分应用函数,这样的函数应用方式被称为节。

isInAlphabet = (`elem` ['A','a',...,'Z','z'])
--isInAlphabet 'f'==True 
As-模式

As-模式 形如xs@(:xs'),如果输入值能匹配 @ 符号右边的模式 (:xs') ,那么就将这个值绑定到 @ 符号左边的变量xs中

通过组合函数复用代码
--写一个行为和 Data.List.tails 类似,但是并不包含空列表后缀的函数
--tails "foo"==["foo","oo","o",""]
--利用As-模式
suffixes :: [a] -> [[a]]
suffixes xs@(_:xs') = xs : suffixes xs'
suffixes [] = []

--利用组合函数
import Data.List (tails)
suffixes2 xs = init (tails xs)

提取组合模式形成函数后再次定义

--组合模式
compose :: (b -> c) -> (a -> b) -> a -> c
compose f g x = f (g x)
--定义函数 
suffixes3 xs = compose init tails xs
--柯里化
suffixes4 = compose init tails

Prelude 里面,使用 (.) 操作符就可以起到与compose一样的组合起两个函数的作用。通过使用 (.) 来组合函数,并产生新函数,组合链的长度没有限制,只要 (.) 符号右边函数的输出值类型适用于 (.) 符号左边函数的输入值类型。

let capCount = length . filter (isUpper . head) . words
-- capCount "Hello there, Mon!"==2
严格求值

seq 函数对传入的第一个参数强迫(force)求值,然后返回它的第二个参数。
seq 的用法:

  • 表达式中被求值的第一个必须是 seq
  • 对多值严格求值时,可以连接起 seq 调用
  • seq需要在运行时检查输入值是否已经被求值

相关文章

网友评论

      本文标题:《Real World Haskell》笔记(4):函数式编程

      本文链接:https://www.haomeiwen.com/subject/ldgbhqtx.html