一.지연 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 ()
실제로ByteString과String타입은 대부분의 시나리오에서 상호 변환이 쉽기 때문에, 먼저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
이 경우getExecutablePath는ghc(실행 파일) 의 절대 경로를 반환합니다
四.난수
I/O 외에도 또 다른 확실히 순수하지 않은 시나리오는 난수입니다. 그렇다면 순수 함수로 난수를 만들 수 있을까요?
의사 난수를 만드는 것은 가능합니다.C 언어와 유사한 방식으로, "씨앗"을 주어야 합니다:
random :: (Random a, RandomGen g) => g -> (a, g)
其中Random과RandomGen씨의 타입은 각각:
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
其中isDoesNotExistError와ioError는 다음과 같습니다:
isDoesNotExistError :: IOError -> Bool
ioError :: IOError -> IO a
전자는predicate로, 전달된IOError가 대상 (파일) 이 존재하지 않아 발생한 것인지 판별합니다. 후자는 JS 의throw와 같아, 이 예외를 다시 던집니다
IOError 의 기타predicate에는 다음과 같습니다:
isAlreadyExistsError
isAlreadyInUseError
isFullError
isEOFError
isIllegalOperation
isPermissionError
isUserError
其中isUserError는userError :: 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
)
其中ioeGetFileName은IOError에서 파일 경로를 꺼내는 데 사용됩니다 (이러한 도구 함수는 모두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 를 참조하세요
아직 댓글이 없습니다