2018년 8월 17일 금요일

[haskell] NULL에게서 도망가기 위한 여정 - 02

출처 : 하스켈로 배우는 함수형 프로그래밍

NULL에게서 도망가기 위한 여정 - 02

- 행간에 처리를 발생시킬 수 있는 힘


이전에는 java.util.Optional을 만나서 NULL에게서 좀 멀어진 느낌을 받았다.
하지만 (책에서는) 개별 인스턴스(객체)에 대한 실패를 동일 문맥상에서 취급할 수 없는 문제를 해결할 수 없었다.
* 결국 객체가 생성될 때마다 context(문맥)이 계속 생성되고 안으로 파고 들어간다.

하스켈을 봐보자.

-- Optional.hs
-- $ stack ghc Optional.hs
-- Optiona.exe
-- 1 + 1
-- Optiona.exe
-- 4 / 0

-- 문자열을 정수로 변환. 변환할 수 없다면 무효
toNum :: String -> Maybe Int
toNum s = case reads s of
        [(n,"")] -> Just n  -- 저절로 변환됨.
        _        -> Nothing

-- 사칙연산. 연산할 수 없다면 무효
addOp :: Int -> Int -> Maybe Int
addOp a b = Just (a + b)
subOp :: Int -> Int -> Maybe Int
subOp a b = Just (a - b)
mulOp :: Int -> Int -> Maybe Int
mulOp a b = Just (a * b)
divOp :: Int -> Int -> Maybe Int
divOp _ 0 = Nothing
divOp a b = Just (a `div` b)

-- +-*/ 연산 변환, 나머지 무효
toBinOp :: String -> Maybe (Int -> Int -> Maybe Int)
toBinOp "+" = Just addOp
toBinOp "-" = Just subOp
toBinOp "*" = Just mulOp
toBinOp "/" = Just divOp

eval :: String -> Maybe Int
eval expr = do
  -- 스페이스로 분할
  let [sa, sop, sb] = words expr
  
  a <- toNum sa     -- 문자열을 숫자로
  op <- toBinOp sop -- 문자열을 연산자로 
  b <- toNum sb     -- 문자열을 숫자로
  a `op` b          -- 연산
  
main :: IO ()
main = getLine >>= putStrLn . maybe "invalid" show . eval

일단 >>= 는 바인드라고 불리는데 getLine으로 받은 값을 바로 다음 타자에게 넘겨준다고 생각하면 된다. (여기선 그냥 이렇게 넘어가자)
putStrLn . maybe "invalid" show . eval
위 녀석이 뭔지 궁금할 것이다.
한번 타입을 보자.
Main> :t eval
eval :: String -> Maybe Int

Main> :t show
show :: Show a => a -> String

Main> :t show . eval
show . eval :: String -> String

Main> :t maybe
maybe :: b -> (a -> b) -> Maybe a -> b

Main> :t maybe "invalid" show . eval
maybe "invalid" show . eval :: String -> [Char]
자 보자.
eval은 String을 받아서Maybe Int를 리턴한다.
show는 아무거나 받아서 String을 내뱉는다.
show . eval 은 String을 받아서 String을 뱉는다. 이게 합성의 힘이다. 중간에 뭐가 있던 단순해지는 것이다.

maybe 함수는 디폴트값, 함수, Maybe 값을 받는다. Maybe값을 받아서 그 값의 내용물을 빼내서 함수를 적용시키고 리턴한다.
만약 Maybe값이 Nothing이라면 디폴트값을 내뱉는다. (Maybe 타입생성자를 없애버리는 역할인듯)

Main> eval "1 + 1"
Just 2

Main> (show . eval) "1 + 1"
"Just 2"

Main> (maybe "invalid" show . eval) "1 + 1"
"2"

Main> putStrLn $ (maybe "invalid" show . eval) "1 + 1"
2

Main> let a = putStrLn . maybe "invalid" show . eval
Main> a "1 + 1"
2

좋다 이렇게 합성이 되는 것이다.

좀 더 설명해보자면,
Haskell에서 Maybe라는 것은 java.util.Optional같은 것과 동일한 문제를 해결하기 위해 있다.
Nothing이 무효, Just가 유효값이다.

만약 앞에 처리에서 Nothing이 발생하면 이후 처리는 실행되지 않고 최정 결과도 Nothing이 된다.
자바의 Optional과 아주 비슷하지만 자바처럼 각각의 인스턴스에 하나하나 flatMap으로 덮어야 하는 수고가 필요없이
아주 그런 걸 신경쓰지 않고 코딩을 하듯 만들어졌다.

무효값이 되었을 경우 건너뛰는 코드 같은건 보이지 않는다.
(책의 내용을 빌리자면, 마치 원래 무효가 될지도 모른다는 생각조차 무시하고 작성된 것같이 보인다)

이것은 Haskell이 갖는 "문맥을 프로그래밍할 수 있는 힘 (monad의 do)"을 이용하고 있기 때문이라고 책은 설명한다.

문맥이란 문장과 문장 사이에서 이루어지는 무언가를 말한다. 자바에서 문장 끝에 붙는 ";"에 무언가 처리를 실시하고 있는 것과 같다.

Maybe는 그러한 "문맥"을 프로그래밍의 한 예 중 하나다.
Maybe가 갖는 문맥이란,

"이전 처리가 무효가 되었다면 그 이후도 모두 무효, 그렇지 않으면 정상 처리를 계속한다" 라는 것이다.
java.util.Optional처럼 인스턴스에 포함시킨 기능이 아니라 문맥에 포함시킨 기능이므로 각 값에 의존한 코딩을 하지 않아도 된다.
(대체 문맥에 포함시킨 기능이라는 것... 이런건 어떻게 프로그래밍하는 것 일까? 이런걸 가지고 놀 때가 언제일지)

댓글 없음:

댓글 쓰기