본문으로 건너뛰기

지연 IO 에서 시작하는_Haskell 노트 6

무료2018-05-26#Functional_Programming#Haskell随机数#Haskell异常处理#Haskell获取命令行参数#Haskell ByteString#Haskell Buffer

buffer, chunk 와 thunk 의 이야기

一.지연 I/O 와 buffer

Haskell 에서 I/O 도 지연됩니다. 예를 들어:

readThisFile = withFile "./data/lines.txt" ReadMode (\handle -> do
    contents <- hGetContents handle
    putStr contents
  )

하드 디스크에서 파일을 읽을 때 한 번에 모두 메모리로 읽는 것이 아니라 조금씩 스트림으로 읽습니다. 텍스트 파일의 경우 기본 buffer 는 line-buffering 으로, 한 번에 한 줄씩 읽습니다. 바이너리 파일의 경우 기본 buffer 는 block-buffering 으로, 한 번에 한 chunk 씩 읽습니다. 구체적인 크기는 운영체제에 따라 다릅니다

line-buffering 과 block-buffering 은BufferMode값으로 나타냅니다:

data BufferMode
  = NoBuffering | LineBuffering | BlockBuffering (Maybe Int)
    -- Defined in 'GHC.IO.Handle.Types'

BufferMode타입에는 세 개의 값이 있습니다.NoBuffering, LineBuffering, BlockBuffering (Maybe Int)은 각각 buffer 를 사용하지 않음, line-buffering 사용, block-buffering 사용을 나타냅니다.其中Maybe Int는 각 chunk 가 몇 바이트 (byte) 인지 나타냅니다.Nothing인 경우 시스템 기본 chunk 크기를 사용합니다.NoBuffering한 번에 한 문자씩 읽음을 의미하며 (character), 하드 디스크를 빈번히 (고빈도로) 액세스하게 되므로 일반적으로 사용하지 않습니다

BufferMode를 수동으로 설정할 수 있습니다. 예를 들어:

readThisFileInBlockMode = withFile "./data/lines.txt" ReadMode (\handle -> do
    hSetBuffering handle $ BlockBuffering (Just 1024)
    contents <- hGetContents handle
    putStr contents
  )

매번1024B(1KB) 씩 읽습니다.其中hSetBuffering의 타입은:

hSetBuffering :: Handle -> BufferMode -> IO ()

파일 포인터와BufferMode값을 받아 빈 I/O Action 을 반환합니다

buffer 가 있으면 buffer 를 flush 해야 합니다. 그래서hFlush도 있습니다:

hFlush :: Handle -> IO ()

buffer 를 비우는 데 사용합니다. buffer 가 가득 차거나 다른 자동 flush 메커니즘 (예:line-buffering은 줄바꿈 문자를 만나면 flush 함) 을 기다릴 필요가 없습니다

P.S.이는이미지는 잘 떠오르지만 우아하지는 않은 비유입니다:

당신의 변기는 수조에 1 갤런의 물이 차면 자동으로 물을 내립니다. 그래서 당신은 계속 물을 부어 1 갤런이 되면 변기가 자동으로 물을 내리고, 물 안의 데이터도 보이게 됩니다. 하지만 수동으로 레버를 눌러 물을 내릴 수도 있습니다. 기존 물을 모두 내려보냅니다. 물을 내리는 동작이hFlush라는 이름의 의미입니다.

二.Data.ByteString

시스템에서 파일을 읽을 때 성능을 고려해 Buffer 를 채택해야 한다면, 메모리로 읽은 후에는 어떻게 할까요? 어떻게 저장하고 어떻게 조작할까요?

ByteString은 새로운 데이터 타입처럼 보이지만, 이미String이 있지 않은가요?

지연적인 List

String은 Char List 의 별명이며, List 는 지연됩니다. 따라서:

str = "abc"
charList = ['a', 'b', 'c']
charList' = 'a' : 'b' : 'c' : []

> str == charList && charList == charList'
True

문자열"abc"을 선언하는 것은 약속일 뿐입니다. 우리는 Char List 를 갖게 될 것이라고요. 그렇다면 언제 실제로 이 List 를 갖게 (또는 만들게) 될까요?

계산을 강요당할 때 (값을 구할 때), 예를 들어 위 예에서==판단할 때:

instance (Eq a) => Eq [a] where
  {-# SPECIALISE instance Eq [Char] #-}
  []     == []     = True
  (x:xs) == (y:ys) = x == y && xs == ys
  _xs    == _ys    = False

(GHC.Classes 에서 발췌)

패턴 매칭을 통해 왼쪽에서 오른쪽으로 요소가 같은지 비교하며 탐색합니다. 매번 List 의 첫 요소를 가져옵니다.이 시점에서 비로소List 가 필요해지며, "만들어집니다"

지연적이지 않은JS 로 설명하면 다음과 같습니다:

function unshift(x, xs) {
  return [x].concat(xs);
}
const str = 'abc';
charList = unshift('a', unshift('b', unshift('c', [])));
function eq(s, a) {
  if (!s.length && !a.length) return true;
  return s[0] == a[0] && eq(s.slice(1), a.slice(1));
}

// test
eq(str, charList);

하지만 즉시 평가하는 JS 와 달리 Haskell 은 지연적입니다. 따라서 실제 상황은 다음과 유사합니다:

const EMPTY_LIST = {
  value: Symbol.for('_EMPTY_LIST_'),
  tail: () => EMPTY_LIST
};
function unshift(x, xs) {
  return { value: x, tail: () => xs };
}
function sugar(str) {
  return str.split('')
    .reduceRight((a, v) => a.concat([v]), [])
    .reduce((a, v) => unshift(v, a), EMPTY_LIST);
}
const str = sugar('abc');
const charList = unshift('a', unshift('b', unshift('c', EMPTY_LIST)));
function eq(s, a) {
  if (s === EMPTY_LIST && a === EMPTY_LIST) return true;
  return s.value == a.value && eq(s.tail(), a.tail());
}

// test
eq(str, charList);

"지연" 링크드 리스트를 사용하여 실제로 필요할 때만 만들어지는 List 를 시뮬레이션합니다.'a' : 'b' : 'c' : []'a'로 시작하는 List 를 갖겠다는 "약속"입니다. 이 List 가 얼마나 길고, 얼마나 많은 공간을 차지하는지는실제로 평가가 필요하기 전까지는 알 수 없습니다 (알 필요도 없으므로 무한히 긴 List 가 존재할 수 있으며, 어떻게 저장할지 걱정할 필요가 없습니다)

하지만 이러한 지연성은十全十美하지 않으며, 직접적인 문제로효율이 높지 않다는 점이 있습니다. 특히 매우 긴 List 의 시나리오 (예: 파일 읽기) 에서 "약속" (시뮬레이션 시나리오의tail()) 을 처리하는 비용은 높지 않을 수 있지만, 만약 일련의 "약속"이 쌓이면 이러한 "약속"을 처리하는 비용이 두드러지며, 실제 효율은 자연스럽게 떨어집니다. 따라서 이 문제를 해결하기 위해foldl의 엄격한 버전 (비지연 버전) 인foldl'을 도입한 것처럼ByteString을 도입했습니다

P.S.위에서 언급한 "약속"은 사실 Haskell 에 해당하는 용어가 있으며, thunk 라고 합니다

ByteString

Bytestring 의 각 요소는 1 바이트 (8 비트) 입니다. 지연과 엄격 (비지연) 두 가지가 있습니다:

  • 지연:Data.ByteString.Lazy. 역시 지연성을 가지지만 List 보다 조금 더 성실하여, 요소별 thunk 가 아니라 chunk 별 (64K 마다 1 chunk) 로 처리합니다. 어느 정도 thunk 의 수를 줄였습니다

  • 엄격:Data.ByteString모듈에 위치하며, thunk 를 전혀 생성하지 않습니다. 일련의 바이트를 나타내므로 무한히 긴 strict bytestring 은 존재하지 않으며, 지연 List 의 메모리 장점도 없습니다

lazy bytestring 은 chunk List(List 의 각 요소가 64K 크기의 strict bytestring) 와 같아, 지연성으로 인한 효율 영향을 줄이면서도 지연성의 메모리 장점을 가지고 있습니다. 따라서대부분의 경우 lazy 버전을 사용합니다

P.S.64K 라는 크기는 이유가 있습니다:

64K 는 당신의 CPU 의 L2 캐시에 들어갈 가능성이 매우 높습니다

자주 사용하는 함수

ByteString 은 또 다른 List 와 같으므로 List 의 대부분의 메서드가 ByteString 에도同名으로 구현되어 있습니다. 예를 들어:

head, tail, init, null, length, map, reverse, foldl, foldr, concat, takeWhile, filter

따라서 먼저 이름 충돌을 피해야 합니다:

-- 지연 ByteString
import Data.ByteString.Lazy as B
-- 엄격 ByteString
import Data.ByteString as S

ByteString 을 생성합니다:

-- Word8 List 를 ByteString 으로 변환
B.pack :: [GHC.Word.Word8] -> ByteString
-- 엄격 ByteString 을 지연 ByteString 으로 변환
B.fromChunks :: [Data.ByteString.Internal.ByteString] -> ByteString

其中Word8은 범위가 더 작은Int와 같습니다 (0 ~ 255사이, Int와 마찬가지로Num클래스에 속합니다). 예를 들어:

> B.pack [65, 66, 67]
"ABC"
> B.fromChunks [S.pack [65, 66, 67], S.pack [97, 98, 99]]
"ABCabc"

주의:fromChunks는 주어진 strict bytestring 그룹을 연결하여 chunk List 로 만듭니다. 먼저 연결한 다음一个个의 64K 공간에 채우는 것이 아닙니다. 만약 많은 조각난strict bytestring이 있고 연결하여 메모리를 차지하고 싶지 않다면, 이 방식을 사용하여它们을 연결할 수 있습니다

요소를 삽입합니다:

B.cons :: GHC.Word.Word8 -> B.ByteString -> B.ByteString
B.cons' :: GHC.Word.Word8 -> B.ByteString -> B.ByteString

cons는 List 의:와 같으며, 왼쪽에 요소를 삽입하는 데 사용합니다. 역시 지연적입니다 (첫 번째 chunk 가 새 요소를 수용할 수 있더라도 chunk 를 삽입합니다). cons'는 그 엄격한 버전으로, 첫 번째 chunk 의 나머지 공간을 우선적으로 채웁니다. 차이는 다음과 같습니다:

> Prelude.foldr B.cons B.empty [50..60]
Chunk "2" (Chunk "3" (Chunk "4" (Chunk "5" (Chunk "6" (Chunk "7" (Chunk "8" (Chunk "9" (Chunk ":" (Chunk ";" (Chunk "<"
Empty))))))))))
> Prelude.foldr B.cons' B.empty [50..60]
Chunk "23456789:;<" Empty

P.S.오래된 버전의GHC는 위와 같은 차이를show하지만, 0.10.0.1 이후의Show구현은 문자열 리터럴과 유사한 형태로 변경되어 차이를 알 수 없게 되었습니다. 자세한 내용은Haskell: Does ghci show "Chunk .. Empty"? 를 참조하세요

파일 읽기/쓰기:

-- chunk 별로 읽기
S.readFile :: FilePath -> IO S.ByteString
-- 모두 읽기
B.readFile :: FilePath -> IO B.ByteString
-- chunk 별로 쓰기
S.writeFile :: FilePath -> S.ByteString -> IO ()
-- 한 번에 쓰기
B.writeFile :: FilePath -> B.ByteString -> IO ()

실제로ByteStringString타입은 대부분의 시나리오에서 상호 변환이 쉽기 때문에, 먼저String으로 구현한 후 성능이 좋지 않은 시나리오에서ByteString으로 변경할 수 있습니다

P.S.더 많은ByteString관련 함수는Data.ByteString 를 참조하세요

三.명령줄 인수

대화형 입력과 파일 읽기 외에도, 명령줄 인수는 사용자 입력을 얻는 또 다른 중요한 방법입니다:

-- readWhat.hs
import System.Environment
import System.IO

main = do
  args <- getArgs
  contents <- readFile (args !! 0)
  putStr contents

한번 시도해 봅시다:

$ ghc --make ./readWhat.hs
[1 of 1] Compiling Main             ( readWhat.hs, readWhat.o )
Linking readWhat ...
$  ./readWhat ./data/lines.txt
hoho, this is xx.
who's that ?
$ ./readWhat ./data/that.txt
contents in that file
another line
last line

이제cat의 기본 기능이 갖춰졌습니다.其中getArgs의 타입은:

getArgs :: IO [String]

System.Environment모듈에 위치하며, I/O Action 형태로 명령줄 인수로 구성된String배열을 반환합니다. 유사한 것으로는 다음과 같습니다:

-- 프로그램 이름 가져오기 (실행 파일 이름)
getProgName :: IO String
-- 현재 절대 경로 가져오기
getExecutablePath :: IO FilePath
-- 환경 변수 설정하기
setEnv :: String -> String -> IO ()
-- 환경 변수 가져오기
getEnv :: String -> IO String

P.S.더 많은 환경 관련 함수는System.Environment 를 참조하세요

예를 들어:

import System.IO
import System.Environment

main = do
  progName <- getProgName
  args <- getArgs
  pwd <- getExecutablePath
  setEnv "NODE_ENV" "production"
  nodeEnv <- getEnv "NODE_ENV"
  putStrLn pwd
  putStrLn ("NODE_ENV " ++ nodeEnv)
  putStrLn (progName ++ (foldl (++) "" $ map (" " ++) args))

한번 시도해 봅시다:

$ ghc --make ./testArgs
[1 of 1] Compiling Main             ( testArgs.hs, testArgs.o )
Linking testArgs ...
$ ./testArgs -a --p path
/absolute/path/to/testArgs
NODE_ENV production
testArgs -a --p path

P.S.ghc --make sourceFile컴파일 실행 외에도 소스 코드를 직접 실행하는 방식이 있습니다:

$ runhaskell testArgs.hs -b -c
/absolute/path/to/ghc-8.0.1/bin/ghc
NODE_ENV production
testArgs.hs -b -c

이 경우getExecutablePathghc(실행 파일) 의 절대 경로를 반환합니다

四.난수

I/O 외에도 또 다른 확실히 순수하지 않은 시나리오는 난수입니다. 그렇다면 순수 함수로 난수를 만들 수 있을까요?

의사 난수를 만드는 것은 가능합니다.C 언어와 유사한 방식으로, "씨앗"을 주어야 합니다:

random :: (Random a, RandomGen g) => g -> (a, g)

其中RandomRandomGen씨의 타입은 각각:

instance Random Word -- Defined in 'System.Random'
instance Random Integer -- Defined in 'System.Random'
instance Random Int -- Defined in 'System.Random'
instance Random Float -- Defined in 'System.Random'
instance Random Double -- Defined in 'System.Random'
instance Random Char -- Defined in 'System.Random'
instance Random Bool -- Defined in 'System.Random'

instance RandomGen StdGen -- Defined in 'System.Random'
data StdGen
  = System.Random.StdGen {-# UNPACK #-}GHC.Int.Int32
                        {-# UNPACK #-}GHC.Int.Int32
    -- Defined in 'System.Random'

P.S.其中Word는 너비를 지정할 수 있는 부호 없는 정수형을 가리킵니다. 자세한 내용은Int vs Word in common use? 를 참조하세요

숫자, 문자, 불리언 타입 등은 모두 난수값을 가질 수 있으며, 씨는 특별한mkStdGen :: Int -> StdGen함수를 통해 생성해야 합니다. 예를 들어:

> random (mkStdGen 7) :: (Int, StdGen)
(5401197224043011423,33684305 2103410263)
> random (mkStdGen 7) :: (Int, StdGen)
(5401197224043011423,33684305 2103410263)

확실히 순수 함수이므로 두 번 호출한 결과가 완전히 같습니다 (연속으로 호출했기 때문이 아니라, 10 일 보름 후에 호출해도 이 결과입니다). 타입 선언을 통해random함수에 기대하는 반환값 타입을 알립니다. 다른 것으로 바꿔봅시다:

> random (mkStdGen 7) :: (Bool, StdGen)
(True,320112 40692)
> random (mkStdGen 7) :: (Float, StdGen)
(0.34564054,2071543753 1655838864)
> random (mkStdGen 7) :: (Char, StdGen)
('\279419',320112 40692)

random함수는 매번 다음 씨앗을 생성하므로, 이렇게 할 수 있습니다:

import System.Random

random3 i = collectNext $ collectNext $ [random $ mkStdGen i]
  where collectNext xs @((i, g):_) = xs ++ [random g]

한번 시도해 봅시다:

> random3 100
[(-3633736515773289454,693699796 2103410263),(-1610541887407225575,136012003 1780294415),(-1610541887407225575,136012003 1780294415)]
> (random3 100) :: [(Bool, StdGen)]
[(True,4041414 40692),(False,651872571 1655838864),(False,651872571 1655838864)]
> [b | (b, g) <- (random3 100) :: [(Bool, StdGen)]]
[True,False,False]

P.S.(random3 100) :: [(Bool, StdGen)]random3의 반환 타입만 한정하며, 컴파일러는random $ mkStdGen i에 필요한 타입이(Bool, StdGen)임을 추론할 수 있음에 주의하세요

이제 (의사) 랜덤らしく 되었습니다.random은 순수 함수이므로,다른 반환값을 얻으려면 씨앗 매개변수를 변경해야 하기때문입니다

실제로 더 간단한 방법이 있습니다:

random3' i = take 3 $ randoms $ mkStdGen i
> random3' 100 :: [Bool]
[True,False,False]

其中randoms :: (Random a, RandomGen g) => g -> [a]함수는RandomGen매개변수를 받아Random무한 시퀀스를 반환합니다

그 외에도 자주 사용하는 것으로는 다음과 같습니다:

-- [min, max] 범위의 난수 반환
randomR :: (Random a, RandomGen g) => (a, a) -> g -> (a, g)
-- randomR 와 유사하며, 무한 시퀀스 반환
randomRs :: (Random a, RandomGen g) => (a, a) -> g -> [a]

예를 들어:

> randomR ('a', 'z') (mkStdGen 1)
('x',80028 40692)
> take 24 $ randomRs (1, 6) (mkStdGen 1)
[6,5,2,6,5,2,3,2,5,5,4,2,1,2,5,6,3,3,5,5,1,4,3,3]

P.S.더 많은 난수 관련 함수는System.Random 를 참조하세요

동적 씨앗

고정된 씨앗은 매번 같은 난수 시퀀스만 반환하므로 의미가 없습니다. 따라서 동적인 씨앗 (예: 시스템 시간 등) 이 필요합니다:

getStdGen :: IO StdGen

getStdGen은 프로그램 실행 시 시스템에 난수 생성기 (random generator) 를 요청하여 전역 생성기 (global generator) 로 저장합니다

예를 들어:

main = do
  g <- getStdGen
  print $ take 10 (randoms g :: [Bool])

한번 시도해 봅시다:

$ ghc --make rand.hs
[1 of 1] Compiling Main             ( rand.hs, rand.o )
Linking rand ...
$ ./rand
[False,False,True,False,False,True,False,True,False,False]
$ ./rand
[True,False,False,False,True,False,False,False,True,True]
$ ./rand
[True,True,True,False,False,True,True,False,False,True]

주의: GHCI 환경에서getStdGen을 호출하면 항상 같은 씨앗을 얻습니다. 프로그램이 연속으로getStdGen을 호출하는 효과와 유사하므로, 항상 같은 난수 시퀀스를 반환합니다:

> getStdGen
1661435168 1
> getStdGen
1661435168 1
> main
[False,False,False,False,True,False,False,False,True,True]
> main
[False,False,False,False,True,False,False,False,True,True]

무한 시퀀스의 뒷부분을 수동으로 가져가거나newStdGen :: IO StdGen함수를 사용할 수 있습니다:

> newStdGen
1018152561 2147483398
> newStdGen
1018192575 40691

newStdGen은 기존 global generator 를 두 개의 random generator 로 나누어, 그중 하나를 global generator 로 설정하고 다른 하나를 반환합니다. 따라서:

> getStdGen
1661435170 1655838864
> getStdGen
1661435170 1655838864
> newStdGen
1018232589 1655838863
> getStdGen
1661435171 2103410263

위 예시와 같이, newStdGen은 새로운 random generator 를 반환할 뿐만 아니라,global generator 를 리셋합니다

五.예외 처리

지금까지 많은 예외를 보았습니다 (패턴 매칭 누락, 타입 선언 누락, 빈 배열 첫 요소 가져오기, 0 으로 나누기 예외 등). 예외가 발생하면 프로그램은 즉시 오류를 보고 종료하지만, 예외를 캡처하는 것은 시도해 보지 않았습니다

실제로 다른 주류 언어와 마찬가지로 Haskell 에도 완전한 예외 처리 메커니즘이 있습니다

I/O 예외

I/O 관련 시나리오에서는 더 엄격한 예외 처리가 필요합니다. 내부 로직과 비교하여 외부 환경은 훨씬 더 제어 불가능하고 신뢰할 수 없기 때문입니다:

파일을 열 때 파일이 잠겨 있을 수도 있고, 파일이 삭제되었을 수도 있으며, 혹은 하드 디스크 전체가 뽑혔을 수도 있습니다

이 시점에서 예외를 던져, 프로그램에 어떤 문제가 발생했음을 알리고, 예상대로 정상적으로 실행되지 않았음을 알립니다

I/O 예외는catchIOError로 캡처할 수 있습니다. 예를 들어:

import System.IO.Error
catchIOError :: IO a -> (IOError -> IO a) -> IO a

I/O Action 과 해당 예외 처리 함수를 입력하여 같은 타입의 I/O Action 을 반환합니다. 메커니즘은try-catch와 유사하며, I/O Action 이 예외를 던진 경우에만 예외 처리 함수를 실행하고 그 반환값을 반환합니다. 예를 들어:

import System.IO
import System.IO.Error
import Control.Monad
import System.Environment

main = do
  args <- getArgs
  when (not . null $ args) (do
    contents <- catchIOError (readFile (head args)) (\err -> do return "Failed to read this file!")
    putStr contents
    )

파일을 찾을 수 없거나, 기타 이유로readFile이 예외를 발생시킬 경우 힌트 메시지를 출력합니다:

$ runhaskell ioException.hs ./xx
Failed to read this file!

여기서는 단순히 모든 예외를 먹어버렸지만,最好는 구분하여 처리하는 것입니다:

main = do
  args <- getArgs
  when (not . null $ args) (do
    contents <- catchIOError (readFile (head args)) errorHandler
    putStr contents
    )
  where errorHandler err
          | isDoesNotExistError err = do return "File not found!"
          | otherwise = ioError err

其中isDoesNotExistErrorioError는 다음과 같습니다:

isDoesNotExistError :: IOError -> Bool
ioError :: IOError -> IO a

전자는predicate로, 전달된IOError가 대상 (파일) 이 존재하지 않아 발생한 것인지 판별합니다. 후자는 JS 의throw와 같아, 이 예외를 다시 던집니다

IOError 의 기타predicate에는 다음과 같습니다:

isAlreadyExistsError
isAlreadyInUseError
isFullError
isEOFError
isIllegalOperation
isPermissionError
isUserError

其中isUserErroruserError :: String -> IOError함수를 통해 수동으로 만든 예외를 판별하는 데 사용됩니다

오류 정보 가져오기

예외를 발생시킨 사용자 입력을 출력하고 싶다면, 이렇게 할 수 있습니다:

exists = do
  file <- getLine
  when (not . null $ file) (do
    contents <- catchIOError (readFile file) (\err -> do
      return ("File " ++ file ++ " not found!\n")
      )
    putStr contents
    )

한번 시도해 봅시다:

> exists
./xx
File ./xx not found!
> exists
./io.hs
main = print "hoho"

예상대로입니다. 여기서는lambda함수를 사용하여 외부의file변수에 액세스할 수 있습니다. 예외 처리 함수가 상당히 크다면 쉽지 않습니다:

exists' = do
  file <- getLine
  when (not . null $ file) (do
    contents <- catchIOError (readFile file) (errorHandler file)
    putStr contents
    )
  where errorHandler file = \err -> do (return ("File " ++ file ++ " not found!\n"))

file변수를errorHandler에 전달하기 위해 한 층 더 감쌌지만,보기에 어리석게 보이고, 보존할 수 있는 현장 정보도 매우 제한적입니다

따라서 다른 언어와 마찬가지로, 예외 객체에서 일부 오류 정보를 꺼낼 수 있습니다. 예를 들어:

exists'' = do
  file <- getLine
  when (not . null $ file) (do
    contents <- catchIOError (readFile file) (\err ->
      case ioeGetFileName err of Just path -> return ("File at " ++ path ++ " not found!\n")
                                 Nothing -> return ("File at somewhere not found!\n")
      )
    putStr contents
    )

其中ioeGetFileNameIOError에서 파일 경로를 꺼내는 데 사용됩니다 (이러한 도구 함수는 모두ioe로 시작합니다):

ioeGetFileName :: IOError -> Maybe FilePath

P.S.유사한 함수에 대한 더 많은 정보는Attributes of I/O errors 를 참조하세요

순수 함수 예외

예외는 I/O 시나리오에만 국한된 것이 아닙니다. 예를 들어:

> 1 `div` 0
*** Exception: divide by zero
> head []
*** Exception: Prelude.head: empty list

순수 함수도 예외를 발생시킬 수 있습니다. 위의 0 으로 나누기 예외와 빈 배열 첫 요소 가져오기 예외에는 두 가지 처리 방법이 있습니다:

  • Maybe또는Either사용

  • try :: Exception e => IO a -> IO (Either e a)사용 (Control.Exception모듈에 위치)

예를 들어:

import Data.Maybe
> case listToMaybe [] of Nothing -> ""; Just first -> first
""
> case listToMaybe ["a", "b"] of Nothing -> ""; Just first -> first
"a"

其中listToMaybe :: [a] -> Maybe a는 List 첫 요소를 가져와Maybe타입으로 포장하는 데 사용됩니다 (빈 List 는Nothing)

0 으로 나누기 예외는 제수가0이 아님을 수동으로 검사하거나, evaluate로 I/O 시나리오에 넣어try로 캡처합니다:

> import Control.Exception
> first <- try $ evaluate $ 1 `div` 0 :: IO (Either ArithException Integer)
> first
Left divide by zero

실제로 0 으로 나누기 예외의 구체적인 타입은DivideByZero이며, Control.Exception모듈에 위치합니다:

data ArithException
  = Overflow
  | Underflow
  | LossOfPrecision
  | DivideByZero
  | Denormal
  | RatioZeroDenominator
    -- Defined in 'GHC.Exception'

특정 예외 카테고리가 불분명한 경우 (이는 실제로 예외 타입을 알 수 없고, 소스 코드를調べて도 추측할 수 없는 경우), 또는 모든 타입의 예외를 캡처하기를 희망하는 경우, SomeException을 사용할 수 있습니다:

> first <- try $ evaluate $ head [] :: IO (Either SomeException ())
> first
Left Prelude.head: empty list

P.S.4 가지 예외 처리方案에 대한 더 많은 정보는Handling errors in Haskell 를 참조하세요

참고 자료

댓글

아직 댓글이 없습니다

댓글 작성