bottom, Void, undefined, unit

Posted on June 17, 2021

리턴하지 않음을 리턴한다. - bottom
값이 없는 타입이다 - Void
결과값이 원하는 타입은 뭐든 될 수 있는 함수 - undefined
될 수 있는 값이 하나뿐인 타입 - unit

꼭 이런 말장난 같은 타입들인데, 각 각 중요한 역할들을 합니다.

간단하게 용도를 요약하면,
Void는 타입 체커만 통과하고 값이 들어오면 안되는 때 써먹기 위한 안전 장치
bottom은 무한 루프를 돌거나 예외를 발생시키는, 결과가 없는 함수를 위한 타입
undefined는 bottom 개념을 명시적으로 코드에서 쓸 필요가 있을 때 (디버깅을 위한 임시 코드에 많이 쓰입니다.)
unit은 값 생성자가 () 하나인 그냥 보통의 타입으로 특별한 정보가 필요없이 자리만 차지할 값이 필요할 때

Int가 가질 수 있는 값은 {0,1,2,...}
Unit이 가질 수 있는 값은 {()}
Void가 가질 수 있는 값은 {}
bottom은 아예 값 개념이 없습니다.

bottom

타입 이론type theory에서 나온 개념으로 값 개념이 없는 타입을 말합니다.(Zero 타입 또는 empty 타입이라고 부르기도 합니다.) 값 개념이 없다는 말이 뭘 의미할까요?

“무한 루프 함수는 값을 리턴하지 않는다.”
모든 함수는 리턴값이 있어야 합니다. 그러면 예외가 발생하거나 무한 루프를 도는 함수의 리턴값은 뭘까요? “리턴값이 없는” bottom이란 타입을 정하고
“무한 루프 함수는 bottom 타입을 리턴한다.”
이렇게 말합니다.

bottom은 값이 없는게 아니라, 값을 리턴하지 않는 함수의 결과 타입을 말합니다. 값을 가질 수 없는 Void와는 다릅니다. bottom은 값이 있고 없고가 아니라, 결과를 리턴하지 않는 함수의 타입입니다. 타입을 값의 추상화로만 바라 봐서는 쉽게 수긍이 가지 않습니다. 타입을 함수 동작의 추상화로 보면 어떨까 합니다.

아래 같은 자료는 아직 못봤습니다. 틀렸을 수도 있는 추측입니다.

함수 타입을 분류하는 추상화 계층을 그리면

  1. 값을 리턴하는 함수 타입
    - Int를 리턴하는 함수 타입
    - Maybe Int를 리턴하는 함수 타입
    - (Int -> Int)를 리턴하는 함수 타입

  2. 값을 리턴하지 않는 함수 타입

  3. 값을 리턴할 때도 있고, 안 할 때도 있는 함수 타입
    - 1번 타입 | 2번 타입

Int 타입 함수보다 bottom 타입 함수는 한 단계 위 추상 계층으로 볼 수 있습니다. 실제 프로그래밍에서 타입은 버그를 줄여주기 위해 컴파일 직전, 타입 체커가 읽어 들여, 코드 조각(함수)들의 접점들이 다 무리없이 연결되는지 검사할 때 씁니다. 사람이 확실히 지킬수만 있다면 이론적으론 타입을 빼고 가도 됩니다. 물론 사람이 타입 체커만큼 본다는 건 불가능하겠지만 말입니다. 타입 이론에선 결과값을 돌려주지 않는 함수들을 표현할 때 필요한 개념이라 하는데, 그럼 하스켈에서는 다음처럼 Bottom 타입을 명시적으로 써주지 않는 이유는 뭘까요?

f1 :: a -> Bottom -- Bottom이란 타입은 없습니다.

무한 루프를 돌거나 에러로 끝나는 함수라면, 실용으로 생각하면 무한루프만이라도 Bottom이라 써줘도 될 것 같은데, 명확히 Bottom이란 타입은 없습니다. 추측 - 3번의 경우는 타입을 어떻게 적어줘야 할까요? Int | Bottom 이렇게 적으려면 새로운 문법을 도입해야 합니다. 모든 타입에는 항상 bottom이 들어있기 때문에 굳이 Bottom 표시를 안하는 게 아닐까요? 사실, 함수는 시그널을 받으면 리턴하지 않고 종료할 수 있기 때문에, 모든 함수는 3번 타입으로 볼 수 있습니다. (예외를 발생시키지 않도록 시그널을 받지 않게 mask를 씌우는 경우가 있긴 있습니다. 이런 경우가 훨씬 드물기 때문에 굳이 문법 요소를 추가한다면, 디폴트가 bottom이 있고, 어떤 표시를 하면 bottom이 아니라고 표시하는게 효율적일 것 같기도 합니다.)

명시적으로 드러나는 때는 Bottom 타입 대신 undefined 함수를 씁니다. (아래 undefined 섹션 참고.)

“모든 타입에는 bottom이 존재합니다.”

실제 데이터 타입을 선언할 때 Bottom을 같이 선언하지 않아도, 자동으로 모든 타입에는 Bottom 타입이 함께 있다 볼 수 있습니다. (볼 수 있습니다라 적은 이유는, 개념 설명을 이렇게 하는 자료는 있는데, 정말 명시적으로 Int | Bottom 이런식의 코딩, 구현으로 바뀐다는 자료는 못 찾았습니다. 아마도 구현 자체를 바꾸는게 아니라, 이 개념을 염두하고 내부 처리를 하겠지요)
참고 - Toan Nguyen - Typeable에 lifted 타입 설명에 보면 Bool 타입은 3가지 값 True, False, Bottom을 갖는다고 합니다.

※ type theory에서 bottom은 모든 타입의 서브 타입인데, 상대적인 개념으로 모든 타입의 슈퍼 타입을 의미하는 top 타입이란 용어도 있습니다.

f :: Bool -> Bool
f x = undefined

Bool에 있는 bottom을 쓴다는 뜻이고,

f :: Bool -> Bool
f = undefined

이렇게 쓰면 포인트 프리가 아니라 (Bool -> Bool) 타입에 있는 bottom을 쓴다는 얘기입니다.

total/partial 함수를 bottom으로 설명하면, bottom을 리턴하는 함수는 partial 함수, 이에 반해 모든 가능한 입력을 받아서 항상 bottom이 아닌 유효한 값을 리턴하는 함수를 total 함수라 부릅니다.

참고 - Newtype

Q. 모든 하스켈 타입은 bottom과 sum 타입을 이루니, 모든 하스켈 타입은 대수 타입으로 봐도 되지 않을까요?

Void

별도 글에 정리해 두었습니다. Void

undefined

bottom 개념을 명시적으로 표현하기 위하 함수입니다.

undefined :: a

모든 타입이 될 수 있기 때문에, 어떤 경우에도 타입 체커를 통과합니다. 하스켈에서 타입 제약 없이 a라고만 쓰면 무슨 의미일까요? 모든 타입이란 의미로 타입 체킹할 때 필요한 타입과 모두 매치된다는 뜻입니다.

error :: String -> a

어떤 타입의 함수 안에서도 쓸 수 있다는 뜻입니다. 이론상, 모든 타입에 속할 수 있는 값은 bottom 뿐이 없습니다. 하스켈에서는 bottom을 결과 타입에 제약 없이 a만 써서 표현한다고 볼 수 있습니다.

타입 체커가 undefined 함수를 만나면, 모든 타입을 처리할 수 있구나 하고 통과하고, 나중에 undefined를 평가할 때가 되면, 평가해서 얻을 값이 없다는 걸 알고 에러를 내뱉습니다.

unit

Bool 타입 값을 위한 비트는 1비트가 필요합니다. 0이면 False, 1이면 True로 처리하면 되니 1비트만 있으면 됩니다. 그럼 될 수 있는 값이 하나인 타입은 몇 비트가 필요할까요? 0비트입니다. 데이터가 0비트라면 데이터가 없다는 말과 같습니다. 타입만 알려줘서 컴파일 타임에 타입 체커만 통과 하는 거면 Void로도 되지 않았을까요? 왜 unit이 필요했을까요?

data () = ()

Void 타입은, 타입 체커는 통과하지만 언제든 절대 값이 들어오면 안되는 곳에 써주는 안전 장치입니다. 막다른 골목 표시 같은 겁니다. 
unit은 평가하게 되도 에러가 나지 않습니다. 또한 폴리모픽이 아니기 때문에 명시적으로 ()를 지정해 줬을 때만 타입 체커를 통과합니다. 값이 고정이니 의미있는 정보를 담을 순 없습니다. 더미값이 필요할 때 써주는데, 필요한 크기가 0이므로 메모리를 효율적으로 씁니다. Void는 GHC가 값이 들어오면 안된다는 걸 체크하는 안전 장치라면, ()는 프로그래머가 명시적으로 패턴 매칭 통과를 위해 쓰는 표시입니다.

참고 - 각 타입들을 인자로 받을 때의 동작

import Data.Void

-- 쓰지 않을 값을 요구하는 곳에 써 줍니다.
-- IO는 타입 변수 자리가 하나 있는데, 여기를 메꿔주는 역할만 합니다.
-- 구별되는 값이 없으므로, 쓸모 있는 정보를 담을 수 없습니다.
func :: () -> IO ()
func _ = putStrLn "ok"

> func ()
ok

-- Void 타입의 값을 만들 수 없으므로 func2는 실행할 수 없습니다.
-- Void는 안전 장치로 쓰입니다.
func2 :: Void -> IO ()
func2 _ = putStrLn "ok"

> func2 1
error : 
No instance for (Num Void) arising from the literal ‘1
In the first argument of ‘func2’, namely ‘1
      In the expression: func2 1
      In an equation for ‘it’: it = func2 1

-- undefined는 평가되지 않고 다른 타입에는 폴리모픽하게 맞춰 줄 수 있는데
-- 평가 되는 순간 에러가 납니다. 보통 디버깅에 씁니다.
func3 :: a -> IO ()
func3 _ = putStrLn "ok"

> func3 undefined
ok
-- bottom 개념이 눈에 보이도록 한 게 undefined입니다.

정리

특별한 값을 가지지 않은 타입들 (undefined / bottom), Void, unit 들은, 하나 하나의 개념은 그렇지 않지만, 결국 모두 타입 체커와 인터랙션을 위한 장치들입니다.

Github 계정이 없는 분은 메일로 보내주세요. lionhairdino at gmail.com