타입 읽어보기(Reading Types)

언어 살펴보기 단원에서 REPL에서 코드를 실행시켜 보았죠. 이번에도 다시 한번 코드를 돌려볼 건데요. 강조하는 부분을 좀 다르게 해서 진행해 볼 예정이에요. 자 elm repl 을 터미널에서 다시 한번 입력해주시면, 아래와 갈은 화면이 나올 거에요.

---- elm repl 0.17.0 -----------------------------------------------------------
 :help for help, :exit to exit, more at <https://github.com/elm-lang/elm-repl>
--------------------------------------------------------------------------------
>

원시 타입과 리스트(Primitives and Lists)

자 간단한 표현문을 입력하고, 어떻게 동작하는 지 확인해 보세요.

> "hello"
"hello" : String

> not True
False : Bool

> round 3.1415
3 : Int

이 예제에서는 REPL이 값의 타입이 무엇인지 함께 알려줘요. "hello"String이고, 3Int라고 알려주죠. 복잡하지 않아요.

이번엔 값들이 다른 타입을 가진 리스트를 만들어 볼게요.

> [ "Alice", "Bob" ]
[ "Alice", "Bob" ] : List String

> [ 1.0, 8.6, 42.1 ]
[ 1.0, 8.6, 42.1 ] : List Float

> []
[] : List a

첫번째 경우엔 ListString 값을 가지죠. 두번째 ListFloat를 가지고요. 세번째 경우엔 빈 리스트 인데, 이 경우엔 어떤 종류의 값들을 가지는 지 사실 알 수가 없죠. List a의 의미는 "난 내가 리스트인 걸 알아 하지만 그 어떤 것으로든 채울수 있어."에요. 소문자 a는 타입 변수(type variable)라고 부르는데, 이 의미는 특정 유형으로 고정시키지 않겠다는 거에요. 즉, 어떻게 사용하느냐에 따라 타입이 바뀔 수 있다는 거죠.

함수(Functions)

자 이제 몇몇 함수의 타입을 봐볼게요.

> import String
> String.length
<function> : String -> Int

String.length 함수는 String -> Int 타입을 가져요. 이 의미는 String 매개변수를 받아서 정수형으로 반환한다는 의미에요. 자 매개변수를 넘겨보죠.

> String.length "Supercalifragilisticexpialidocious"
34 : Int

초기의 표현식이 어떻게 Int 타입의 결과가 나왔는지 이해하는 게 중요해요. String -> Int 함수는 String 을 매개변수로 받고, 결과는 Int 인거죠.

String 이 아니면 어떻게 될까요?

> String.length [1,2,3]
-- error!

> String.length True
-- error!

String -> Int 함수는 String 매개변수여야 합니다!

익명 함수(Anonymous Functions)

Elm은 익명 함수로 불리는 특징이 있어요. 다음과 같이 이름이 없는 함수를 만들 수 있죠.

> \n -> n / 2
<function> : Float -> Float

백슬래쉬(backslash)와 화살표(arrow)사이는 함수에 매개변수들을 열거해요. 그리고 화살표 오른쪽 부분은 매개변수를 이용해 어떤 동작을 할 지 적는거죠. 이 예제에서는, "난 몇개의 인자를 취할 거고, 이 인자를 n 이라 부를거야. 그리고 난 이걸 2로 나눌래."라고 말하는 거에요.

익명 함수를 직접 사용할 수 있어요. 아래는 128을 매개변수로 두고, 익명 함수를 사용한 예제에요.

> (\n -> n / 2) 128
64 : Float

Float -> Float 함수여서, Float를 매개변수로 주고, 결과는 또다른 Float이 돼요.

알고 넘어가기: 백슬래시로 시작하는 이유는 lambda λ 처럼 보이기 때문이에요. (억지스러워 보이는 건 착각) 제대로 고려하지 않고 만든 것 같지만, Elm과 같은 뿌리의 언어들이 갖는 역사속에서 만들어 진거에요.

또한 (\n -> n / 2) 128을 작성할 때, 익명 함수가 괄호로 둘러 쌓여 있다는 게 중요해요. Elm은 화살표 뒷부분을 다 읽어들이고, 괄호가 함수의 끝을 알려주는 기준점이 되는거에요.

이름을 갖는 함수(Named Function)

같은 방법으로 값에 이름을 줄 수도 있어요. 익명함수에 이름을 줄수도 있죠. 유연하죠!

> oneHundredAndTwentyEight = 128.0
128 : Float

> half = \n -> n / 2
<function> : Float -> Float

> half oneHundredAndTwentyEight
64 : Float

결국엔, 명명되지 않은 것과 같이 동작해요. 여러분이 Float -> Float 함수를 썼다면, Float 를 넘겨주고 다른 Float을 최종적으로 받죠.

모든 함수들이 이렇게 정의 되어지는 건, 사실 비밀이 숨어있기 때문인데요! 다음을 보시면 아시겠지만 사실 익명 함수에 이름만 주는 것 뿐이죠.

> half n = n / 2
<function> : Float -> Float

다음과 같은 형태로도 생각할 수 있어요.

> half = \n -> n / 2
<function> : Float -> Float

함수의 매개변수가 얼마든지 상관 없이 쓸 수 있어요. 복수의 매개변수를 사용해서 작성해볼게요.

> divide x y = x / y
<function> : Float -> Float -> Float

> divide 3 2
1.5 : Float

되는 것 같긴 한데, 왜 divide타입에 화살표가 두개 일까요?! 일단 "모든 매개변수는 화살표로 나눠지고, 마지막은 함수의 결과"라고 생각하시면 편해요. divide는 두개의 매개변수를 받고 Float를 리턴하는 거죠.

divide의 화살표가 두개인지 이해를 제대로 하기 위해선, 익명함수가 어떻게 변형되어 정의 되는 지 아는 게 도움이 되요.

> divide x y = x / y
<function> : Float -> Float -> Float

> divide x = \y -> x / y
<function> : Float -> Float -> Float

> divide = \x -> (\y -> x / y)
<function> : Float -> Float -> Float

위에 세개는 모두 동일해요. 그냥 매개변수를 하나씩 익명변수로 바꿔본 건데요. divide 3 2 와 같은 같은 표현식은 사실 다음과 같은 과정을 거쳐서 동작해요.

  divide 3 2
  (divide 3) 2                 -- Step 1 - 암시적으로 괄호가 추가됨
  ((\x -> (\y -> x / y)) 3) 2  -- Step 2 - `divide`를 확장
  (\y -> 3 / y) 2              -- Step 3 - x 를 3으로 변경
  3 / 2                        -- Step 4 - 3을 2로 변경
  1.5                          -- Step 5 - 그냥 수학적으로 계산

divide를 확장한 뒤, 매개변수를 하나씩 넘겨주어서, 각각 xy를 바꿔 치는 거죠.

자 타입이 동작하는 방법에 대해서 더 자세히 알아 볼게요. step 3 의 코드를 따라 쳐보면,

> (\y -> 3 / y)
<function> : Float -> Float

half처럼 Float -> Float 함수죠. 좀 더 복잡한 step 2 도 봐보죠.

> (\x -> (\y -> x / y))
<function> : Float -> Float -> Float

자, \x -> ... 같은 형태는 Float -> ...으로 동작하는 걸 알고 있죠. 또한 (\y -> x / y)Float -> Float 타입을 가지겠죠.

여러분이 만약 실제로 괄호를 쓴다면, Float -> (Float -> Float)이 될거에요. 여기서 여러분이 매개변수를 한개씩 넘기면, 즉 x가 매개변수로 변경 되었을 때 실제론 이미 다른 함수가 되어버리는 거에요.

이건 Elm에 존재하는 함수에서도 마찬가지에요.

> import String
> String.repeat
<function> : Int -> String -> String

여러분이 매개변수를 한개씩 넘기기 때문에 이건 사실 Int -> (String -> String)이죠.

Elm에선 모두 함수가 이런식으로 동작하기 때문에, 여러분은 매개변수를 한번에 모두 넘겨줄 필요가 없어요. 다음과 같이 쓸 수 있다는 거죠.

> divide 128
<function> : Float -> Float

> String.repeat 3
<function> : String -> String

이건 부분 적용(partial application)이라고 부르는데요. |> 연산자를 이용해서 함수를 연결하여 사용할 수 있어요. 이게 함수 타입이 많은 화살표를 가진 이유에요!

타입 어노테이션(Type Annotations)

지금까지는 Elm이 타입을 알아내도록 했지만, 타입을 어노테이션을 사용해서 원하는 타입을 정의할 수도 있어요. 다음과 같이 작성하면 되요.

half : Float -> Float
half n =
  n / 2

divide : Float -> Float -> Float
divide x y =
  x / y

askVegeta : Int -> String
askVegeta powerLevel =
  if powerLevel > 9000 then
    "It's over 9000!!!"

  else
    "It is " ++ toString powerLevel ++ "."

사람들이 타입 어노테이션을 사용할 때 실수할 수도 있는데요. 하지만 컴파일러는 실수하지 않기 때문에, 여전히 해당 타입이 무엇인지 알아낼 수 있어요. 컴파일러는 타입 어노테이션이 제대로 작성 되었는 지 확인하죠. 즉, 컴파일러가 여러분이 추가한 타입 어노테이션을 검사해줘요.

더 알아보기: 어떤 사람들은 타입 어노테이션이 실제 정의하는 라인 위에 있다는 걸 이상하게 생각해요. 위에 타입 어노테이션을 적는 이유는 나중에 타입 어노테이션을 적는 게 쉽고 확장성이 없기 때문이에요. 그냥 라인 한줄을 추가하는 것 뿐인 이 방법은 조잡한 프로토타입 코드를 좋은 품질의 코드로 바꿔줘요.

results matching ""

    No results matching ""