Real World Haskell - Chapter 7. I/O

 

Chapter 7. I/O

 

使用<- IO 获取输入,使用let pure code 获取输入。

 

pure code 就是相同的输入返回相同的输出,并且没有side effects 的代码。在Haskell 中只有I/O actions 不遵循这一规则。

 

 

严格分隔pure code 和非pure cod 有利于编译器自动优化和并行化。

 

Classic I/O in Haskell

 

-- runghc.bat

{-

@echo off

ghci main

-}

 

{-

 

main = do

    putStrLn "Greetings! What is your name?"

    inpStr <- getLine

    putStrLn $ "Welcome to Haskell, " ++ inpStr ++ "!"

-}

 

{-

$ runghc basicio.hs

Greetings! What is your name?

John

Welcome to Haskell, John!

-}

 

putStrLn 会在输出一个String 后再输出一个换行。

 

{-

ghci> let writefoo = putStrLn "foo"

ghci> writefoo

foo

-}

 

foo 不是witefoo 的返回值,而是putStrLn 的一个side effect(这就是不纯的后果!)

 

 

 

main 函数本身就是一个IO (),关键字do 表示后面的代码有side effect

 

name2reply :: String -> String

name2reply name =

    "Pleased to meet you, " ++ name ++ "./n" ++

    "Your name contains " ++ charcount ++ " characters."

        where charcount = show (length name)

       

main :: IO ()

main = do

    putStrLn "Greetings once again. What is your name?"

    inpStr <- getLine

    let outStr = name2reply inpStr

    putStrLn outStr

 

使用<- IO 获取输入,使用let pure code 获取输入。

 

Pure Versus I/O

 

pure code 就是相同的输入返回相同的输出,并且没有side effects 的代码。在Haskell 中只有I/O actions 不遵循这一规则。


 

Why Purity Matters

 


 

Working with Files and Handles

 

openFile 函数(需要引入System.IO)会返回一个文件句柄。配套的hPutStrLn 用来往文件输出。用完后要用hClose 关闭句柄。

 

作为开始,让我们以命令式的方式来读和写文件。这看起来像你在其它语言看到过的while 循环。这不是的Haskell 的最佳方式,后面我们会改进它。

 

读写文件(循环的方式)

 

import System.IO

import Data.Char(toUpper)

 

main :: IO ()

main = do

        inh <- openFile "input.txt" ReadMode

        outh <- openFile "output.txt" WriteMode

        mainloop inh outh

        hClose inh

        hClose outh

 

mainloop :: Handle -> Handle -> IO ()

mainloop inh outh =

    do ineof <- hIsEOF inh

       if ineof

            then return ()

            else do inpStr <- hGetLine inh

                    hPutStrLn outh (map toUpper inpStr)

                    mainloop inh outh

 

return 的含义与C 等语言的是不同的,return 与“<- 是反义。return 接受一个pure value wraps IO 里。

 

所有I/O action 都必须返回IO type,如果你的结果是从pure computation 来的就必须return to wrap it in IO


 

More on openFile

 

使用openBinaryFile 处理二进制文件。一些操作系统,如Windows 在处理二进制和文本文件上的行为是相当不同的。

 

Closing Handles

 

Haskell 会为文件在内部维护一个缓存,直到用hClose 关闭文件才会进行数据写入。

 

Seek and Tell

 

hTell 函数报告current position 前面有多少个字节。刚开始是0,读了5 字节以后就是5,等等。

 

hSeek 函数设置current position

 

hIsSeekable 函数用来检查一个Handle 是否可以seek

 

Standard Input, Output, and Error

 

getLine, print 的实现

 

{-

getLine = hGetLine stdin

putStrLn = hPutStrLn stdout

print = hPrint stdout

-}

 

使和echo 命令给程序输入参数

 

echo John|runghc callingpure.hs

 

Temporary Files

 

openTempFile, openBinaryTempFile,System.Directory.getTemporaryDirectory


 

Extended Example: Functional I/O and Temporary Files

 

如果 1 的输出正好可以作为2 的输入,就可以将两函数组合成复合函数。

 

Leksah IDE中添加依赖包

Leksah ->Edit Package ->Dependencies ->Enter ->输入“directory ->Add ->Save

 

读写临时文件

 

import System.IO

import System.Directory(getTemporaryDirectory, removeFile)

import System.IO.Error(catch)

import Control.Exception(finally)

 

main :: IO ()

main = withTempFile "mytemp.txt" myAction

 

myAction :: FilePath -> Handle -> IO ()

myAction tempname temph =

    do

       putStrLn "Welcome to tempfile.hs"

       putStrLn $ "I have a temporary file at " ++ tempname

 

       pos <- hTell temph

       putStrLn $ "My initial position is " ++ show pos

 

       let tempdata = show [1..10]

       putStrLn $ "Writing one line containing " ++

                  show (length tempdata) ++ " bytes: " ++

                  tempdata

       hPutStrLn temph tempdata

 

       pos <- hTell temph

       putStrLn $ "After writing, my new position is " ++ show pos

 

       putStrLn $ "The file content is: "

       hSeek temph AbsoluteSeek 0

 

       c <- hGetContents temph

 

       putStrLn c

 

       putStrLn $ "Which could be expressed as this Haskell literal:"

       print c

 

withTempFile :: String -> (FilePath -> Handle -> IO a) -> IO a

withTempFile pattern func =

    do

       tempdir <- catch (getTemporaryDirectory) (/_ -> return ".")

       (tempfile, temph) <- openTempFile tempdir pattern  -- pattern 就是"mytemp.txt"mytemp976.txt 里的数字是系统加的,而且每次都不同。

 

       finally (func tempfile temph)

               (do hClose temph

                   removeFile tempfile)

 

hPutStrLn hPutStr 的区别是hPutStrLn 带换行。


 

Lazy I/O

 

Haskell lazy 语言,I/O 数据也仅在其值必须被known 时才evaluated 出来。

 

hGetContents

 

由于lazy 特性,一次整个读500 GB 大小的文件是可能的。

 

hGetContents 在你调用的时侯,实际上没有任何数据被读取。

 

lazy I/O 来处理500G 的数据文件(后面有改进版)

{-

import System.IO

import Data.Char(toUpper)

 

main :: IO ()

main = do

    inh <- openFile "input.txt" ReadMode

    outh <- openFile "output.txt" WriteMode

    inpStr <- hGetContents inh

    let result = processData inpStr

    hPutStr outh result

    hClose inh

    hClose outh

 

processData :: String -> String

processData = map toUpper

-}

 

lazy I/O 来处理500G 的数据文件

 

import System.IO

import Data.Char(toUpper)

main = do

    inh <- openFile "input.txt" ReadMode

    outh <- openFile "output.txt" WriteMode

    inpStr <- hGetContents inh

    hPutStr outh (map toUpper inpStr)

    hClose inh

    hClose outh


 

readFile and writeFile

 

Haskell 程序员常用hGetContents 作为过滤器。他们读一个文件,过滤某些内容,然后再将结果写到别的什么地方。实际上使用readFile writeFile是实现过滤器的更简洁的方法。

 

readFile 在内部使用hGetContents

 

lazy I/O 来处理500G 的数据文件

-- ab.bat

runhaskell main

cmd

------------------------------

-- input.txt

hello,world!

-------------------------------

-- main.hs

import Data.Char(toUpper)

main = do

    inpStr <- readFile "input.txt"

    writeFile "output.txt" (map toUpper inpStr)

 

运行ab.bat,生成output.txt,内容是:

HELLO,WORLD!

 

A Word on Lazy Output

 


 

interact

 

使用interact 与用户交互

 

-- main.hs

import Data.Char(toUpper)

 

main = interact (map toUpper)

 

-- 先运行批处理

-- runghc.bat

{-

@echo off

ghci main

-}

{-

*Main> main   -- 然后,运行main 函数

hello,world!  -- 接着,输入字串

HELLO,WORLD!  -- 最后,程序输出字串

-}

 

--加输入提示版

 

module Main where

import Data.Char(toUpper)

main = interact (map toUpper . (++) "Your data, in uppercase, is:/n/n")

 

-- 1. (++) "Your data, in uppercase, is:/n/n" :: [Char] -> [Char]

-- 2. map toUpper :: [Char] -> [Char]

 

-- 1 的输出正好可以2 作为输入。

 

以上代码有个小问题,输入提示的部分也变成大写了。

 

module Main where

import Data.Char(toUpper)

main = interact ((++) "Your data, in uppercase, is:/n/n" .

            map toUpper)

 

这里把提示字串移出map 外了。


 

Filters with interact

 

main = interact (unlines . filter (elem 'a') . lines)

 

-- runghc filter.hs < input.txt

{-

I like Haskell

Haskell is great

-}


 

The IO Monad

 

如果要从键盘读一行,I/O 函数不可能每次都返回同样的结果对不对?可以认为I/O 就是改变世界的状态。

 

Actions

 

action 类似函数。定义action 的时侯它们什么也不做,在被invoked 的时侯就会执行一些任务。

 

IO () 是一个action

 

ghci> :type putStrLn

putStrLn :: String -> IO ()

ghci> :type getLine

getLine :: IO String

 

 

action 可以存储或传递到纯代码里。

 

runall :: [IO ()] -> IO ()

runall [] = return ()

runall (firstelem:remainingelems) =

    do firstelem

       runall remainingelems

abc = putStrLn "abc"  -- 一个action

main = do print "Start of the program"

          do abc

          print $ map show [1..10]  -- > ["1","2","3","4","5","6","7","8","9","10"]

          runall $ map (/s ->  putStrLn ("Data: " ++  s) ) ["1","2","3","4","5","6","7","8","9","10"]

          print "Done!"

 

$ 的意思是给后面的所有东西加个括号。

 

可以这样认为:do 块中除了let 外每一条语句都必须产生一个I/O action

 

str2action :: String -> IO ()

str2action input = putStrLn ("Data: " ++ input)

 

list2actions :: [String] -> [IO ()]

list2actions = map str2action

 

numbers :: [Int]

numbers = [1..10]

 

strings :: [String]

strings = map show numbers 

 

actions :: [IO ()]

actions = list2actions strings

 

printitall :: IO ()

printitall = runall actions

 

-- Take a list of actions, and execute each of them in turn.

runall :: [IO ()] -> IO ()

runall [] = return ()

runall (firstelem:remainingelems) =

    do firstelem

       runall remainingelems

 

main = do str2action "Start of the program"

          runall $ map (/s ->  putStrLn ("Data: " ++  s) ) ["1","2","3","4","5","6","7","8","9","10"]

          --printitall

          str2action "Done!"

         

加复数“s 字尾用于提示“这是一个是列表”

 

这里的代码完成的功能是:数字 ->字串 ->action

 

使用mapM_ 函数产生I/O 输入

 

str2message :: String -> String

str2message input = "Data: " ++ input

 

str2action :: String -> IO ()

str2action = putStrLn . str2message

 

numbers :: [Int]

numbers = [1..10]

 

main = do str2action "Start of the program"

          mapM_ (str2action . show) numbers

          str2action "Done!"

 

mapM_ 类似map ,接受一个I/O action 函数作第一个参数,一个列表作为第二个参数。mapM_ 会抛出那个I/O 函数的结果。

 

ghci> :type mapM

mapM :: (Monad m) => (a -> m b) -> [a] -> m [b]

ghci> :type mapM_

mapM_ :: (Monad m) => (a -> m b) -> [a] -> m ()

 

mapM mapM 这些函数实际上工作于任何Monad 上。

 

带下划线的函数通常会丢弃它们的结果。

 

map mapM 的区别是mapM 会执行action


 

Sequencing

 

do 块实际上就是将许多actions joining together 的简便做法。有两个操作符可以用来代替do 块。

 

用来代替do 语句的运算符“>>”,“>>=

 

ghci> :type (>>)

(>>) :: (Monad m) => m a -> m b -> m b

ghci> :type (>>=)

(>>=) :: (Monad m) => m a -> (a -> m b) -> m b

 

>> 运算符将两个action 连起来。第一个action 先执行,然后第二个action 再执行。整个表达式的结果就是第二个action 的结果,并且会丢弃第一个结果。

 

putStrLn "line 1" >> putStrLn "line 2"

 

>>= 运算符会返回一个action ,并将这个action 传给右边的表达式。

 

getLine >>= putStrLn

 

getLine I/O 读一行,然后传给putStrLn 打印出来。

 

main =

    putStrLn "Greetings! What is your name?" >>

    getLine >>=

        (/inpStr -> putStrLn $ "Welcome to Haskell, " ++ inpStr ++ "!")


 

The True Nature of Return

 

return 用于将数据封装进Monad 。对I/O return 会先获取纯数据,然后传给I/O Monad

 

main =

    putStrLn "Greetings! What is your name?" >>

    return "guys" >>=

        (/inpStr -> putStrLn $ "Welcome to Haskell, " ++ inpStr ++ "!")

 

询问用户“yes or "no"

{-

 

isGreen :: IO Bool

isGreen =

    do putStrLn "Is green your favorite color?"

       inpStr <- return "yes"  -- getLine

       return ((toUpper . head $ inpStr) == 'Y')

-}

 

将纯与不纯代码分开

 

isYes :: String -> Bool

isYes inpStr = (toUpper . head $ inpStr) == 'Y'

 

isGreen :: IO Bool

isGreen =

    do putStrLn "Is green your favorite color?"

       inpStr <- getLine

       return (isYes inpStr)

 

<- 操作符用于从Monad 中取出纯数据

 

returnTest :: IO ()

returnTest =

    do one <- return 1

       let two = 2

       putStrLn $ show (one + two)

 

main = returnTest  -- >3

 

let 操作符用于在do 块中定义纯代码(不带action)

 

Is Haskell Really Imperative?

 

 

Side Effects with Lazy I/O

 

当我们说Haskell 没有side effects ,究竟意味着什么?恶劣的循环就算是纯代码也可能导致系统内存耗尽然后crash

 

纯函数不会修改全局变量,不会请求I/O

 

hGetContents 不适合这些场合:与用户交互以获取数据,或从管道获取其它程序的数据。


 

Buffering

 

I/O 子系统是现代计算机最慢的部分。

 

操作系统会将最常用的数据放到内存中。

 

程序语言通常进行buffering ,会从OS 申请一大块内存,这意味着就算代码中只操作一个字节的数据也会占用那个大内存。

 

Buffering Modes

 

三种BufferMode 类型: NoBuffering, LineBuffering, BlockBuffering

 

NoBuffering OS 一次读一个字符,一次写入一个字符(立即写入的)。性能很低,不适用于general-purpose use

 

LineBuffering 遇到换行符,或是数据量太大时就执行写操作。读入换行符之前的所有数据。当从终端读取时一遇到回车就立即返回数据。通常作为默放设置。

 

BlockBuffering 引起Haskell 在可能的时侯读或写固定大小的数据。它有最佳性能,在处理成组的大数据时。接受一个Mabe 参数,用Just 4096 设置buffer 4096,用Nothing 设置默认buffer 大小。

 

默认buffering 模式依赖于操作系统和Haskell 实现。

 

hGetBuffering 获取当前模式,用hSetBuffering 设置buffer 模式。便如hSetBuffering stdin (BlockBuffering Nothing)

 

Flushing The Buffer

 

hFlush hClose 都会将buffer 中的数据立即写入。


 

Reading Command-Line Arguments

 

System.Environment.getArgs 返回IO [String],列表里的每个元素对应命令行传过来的一个参数。

 

System.Environment.getProgName 用于获取程序名。

 

System.Console.GetOpt 提供了一些解析命令行选项的工具,对使用复杂选项的程序很有用。

 

Environment Variables

 

System.Environment: getEnv getEnvironment. getEnv 这两个函数用于查找指定的变量,如果不存在就引发错误。

 

getEnvironment 返回整个环境[(String, String)]lookup 函数用于查找特定的环境变量。

 

linux 中可以用System.Posix.Env 模块中的putEnv setEnv 来设置环境变量。对Windows Haskell 中不存在这种函数。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值