문장과 표현식

문장과 표현식

Nim은 흔히 쓰는 문장/표현식 패러다임을 따릅니다. 표현식과 달리 문장(statement)은 값을 만들어 내지 않아요. 다만 어떤 표현식은 문장으로 쓰이기도 합니다.

문장은 단순 문장(simple statement)과 복합 문장(complex statement)으로 나뉩니다. 단순 문장은 대입, 호출, return 문처럼 다른 문장을 안에 담을 수 없는 문장이고, 복합 문장은 다른 문장을 담을 수 있는 문장이에요. 유명한 난점인 dangling else 문제(바깥의 else가 어느 if에 붙는지 애매해지는 문제)를 피하기 위해, 복합 문장은 반드시 들여쓰기를 해야 합니다. 자세한 규칙은 문법 문서에서 확인할 수 있어요.

출처: Nim Manual

본문

Statement list expression (문장 목록 표현식)

문장을 표현식 자리에서 쓸 수도 있습니다. 모양은 (stmt1; stmt2; ...; ex)처럼 생겼어요. 이걸 문장 목록 표현식(statement list expression), 줄여서 (;)라고 합니다. (stmt1; stmt2; ...; ex)의 타입은 마지막 ex의 타입이 돼요. 그리고 그 앞에 나오는 나머지 문장들은 모두 void 타입이어야 합니다. (discard를 쓰면 void 타입을 만들 수 있어요.) (;)는 새로운 스코프를 만들지 않습니다.

Discard statement (discard 문)

예시부터 볼게요.

proc p(x, y: int): int =
  result = x + y

discard p(3, 4) # discard the return value of `p`

discard 문은 표현식의 값을 계산해서 부작용(side-effect)만 남기고, 그 결과값은 버립니다. 그래서 이 값을 무시해도 문제가 없다는 걸 확실히 알고 있을 때만 써야 해요.

주의할 점이 있어요. discard 없이 프로시저의 반환값을 그냥 무시하는 건 정적 오류(static error)입니다.

그런데 호출하는 프로시저나 이터레이터가 discardable 프라그마로 선언되어 있다면, 반환값을 묵시적으로 무시할 수 있어요.

proc p(x, y: int): int {.discardable.} =
  result = x + y

p(3, 4) # now valid

다만 discardable 프라그마는 템플릿에는 적용되지 않아요. 템플릿은 AST를 그 자리에 치환하기 때문이죠. 예를 들어 볼게요.

{.push discardable .}
template example(): string = "https://nim-lang.org"
{.pop.}

example()

이 템플릿은 "https://nim-lang.org"라는 문자열 리터럴로 풀리는데, {.discardable.}는 리터럴에는 적용되지 않으므로 컴파일러가 오류를 냅니다.

discard 문은 종종 널 문(null statement)처럼 쓰이기도 해요.

proc classify(s: string) =
  case s[0]
  of SymChars, '_': echo "an identifier"
  of '0'..'9': echo "a number"
  else: discard

Void context (void 컨텍스트)

문장들의 나열에서, 마지막을 제외한 모든 표현식은 void 타입이어야 합니다. 이 규칙에 더해, 내장 심볼인 result에 대입하는 순간 그 뒤에 오는 표현식들은 무조건 void 컨텍스트에 놓이게 돼요.

proc invalid*(): string =
  result = "foo"
  "invalid"  # Error: value of type 'string' has to be discarded

proc valid*(): string =
  let x = 317
  "valid"

invalid의 두 번째 줄은 마지막 문장도 아닌데 값이 남아 있으니 오류가 나는 거예요. 반면 valid처럼 마지막 문장으로 쓰이면 그 값이 반환값이 되죠.

Var statement (var 문)

var 문은 지역·전역 변수를 새로 선언하고 초기화합니다. 같은 타입의 변수가 여럿이면 쉼표로 구분해 한 번에 선언할 수 있어요.

var
  a: int = 0
  x, y, z: int

초기화 식이 주어지면 타입을 생략할 수 있어요. 그러면 변수는 그 초기화 식과 같은 타입이 됩니다. 초기화 식이 없으면 변수는 항상 기본값으로 초기화되는데, 그 기본값은 타입에 따라 달라지고 언제나 이진 값 0을 기준으로 합니다.

타입 기본값
모든 정수 타입 0
모든 부동소수점 0.0
char '\0'
bool false
ref 또는 pointer 타입 nil
프로시저 타입 nil
sequence @[]
string ""
tuple[x: A, y: B, ...] (zeroDefault(A), zeroDefault(B), ...) (객체도 마찬가지)
array[0..., T] [zeroDefault(T), ...]
range[T] default(T). 유효 범위를 벗어날 수도 있어요
T = enum cast[T](0). 유효하지 않은 값일 수도 있어요

성능을 위해 이 묵시적 초기화를 생략하고 싶다면 noinit 프라그마를 쓸 수 있어요.

var
  a {.noinit.}: array[0..1023, char]

프로시저에 noinit 프라그마를 붙이면, 그건 프로시저의 묵시적 result 변수를 가리킵니다.

proc returnUndefinedValue: int {.noinit.} = discard

묵시적 초기화는 requiresInit 타입 프라그마로도 막을 수 있어요. 이 프라그마를 붙이면 컴파일러가 해당 객체와 모든 필드에 대해 명시적 초기화를 요구합니다. 다만 컴파일러는 문법적인 모양을 보는 게 아니라 제어 흐름 분석을 통해 변수가 초기화됐는지 증명해요.

type
  MyObject {.requiresInit.} = object

proc p() =
  # the following is valid:
  var x: MyObject
  if someCondition():
    x = a()
  else:
    x = a()
  # use x

requiresInit 프라그마는 distinct 타입에도 적용할 수 있어요. 아래처럼 distinct 타입을 정의해 두고 보면요.

type
  Foo = object
    x: string

  DistinctFoo {.requiresInit, borrow: `.`.} = distinct Foo
  DistinctString {.requiresInit.} = distinct string

다음 코드는 컴파일에 실패합니다.

var foo: DistinctFoo
foo.x = "test"
doAssert foo.x == "test"

var s: DistinctString
s = "test"
doAssert string(s) == "test"

하지만 이 코드들은 성공적으로 컴파일돼요.

let foo = DistinctFoo(Foo(x: "test"))
doAssert foo.x == "test"

let s = DistinctString("test")
doAssert string(s) == "test"

Let statement (let 문)

let 문은 지역·전역 단일 대입(single assignment) 변수를 선언하고 값을 묶어줍니다. 문법은 var 문과 같고, 키워드만 var 대신 let으로 바뀌죠. let 변수는 l-value가 아니라서 var 매개변수에 넘길 수도 없고, 주소를 취할 수도 없어요. 그리고 새 값을 대입할 수도 없습니다.

let 변수에도 일반 변수와 같은 프라그마를 쓸 수 있어요.

let 문은 생성 후 불변이기 때문에, 선언할 때 반드시 값을 정의해야 합니다. 유일한 예외는 {.importc.}(또는 다른 importX 프라그마)가 적용된 경우예요. 이때는 값이 네이티브 코드, 대개 C/C++의 const에서 온다고 가정합니다.

Special identifier _ (밑줄 특수 식별자)

선언에서 _ 식별자는 특별한 의미를 가져요. _라는 이름의 정의는 스코프에 추가되지 않습니다. 즉 정의는 평가되지만 그 이름을 쓸 수는 없어요. 그 결과 _는 얼마든지 다시 정의할 수 있습니다.

let _ = 123
echo _ # error
let _ = 456 # compiles

Tuple unpacking (튜플 언패킹)

var, let, const 문에서는 튜플 언패킹을 수행할 수 있어요. 특수 식별자 _를 쓰면 튜플의 일부를 무시할 수 있습니다.

proc returnsTuple(): (int, int, int) = (4, 2, 3)

let (x, _, z) = returnsTuple()

이 코드는 대략 다음과 같은 문법 설탕(syntactic sugar)으로 취급돼요.

let
  tmpTuple = returnsTuple()
  x = tmpTuple[0]
  z = tmpTuple[2]

varlet 문에서 값 표현식이 튜플 리터럴이라면, 임시 변수 없이 각 표현식이 바로 대입으로 펼쳐집니다.

let (x, y, z) = (1, 2, 3)
# becomes
let
  x = 1
  y = 2
  z = 3

튜플 언패킹은 중첩될 수도 있어요.

proc returnsNestedTuple(): (int, (int, int), int, int) = (4, (5, 7), 2, 3)

let (x, (_, y), _, z) = returnsNestedTuple()

Const section (const 섹션)

const 섹션은 값이 상수 표현식(constant expression)인 상수들을 선언합니다.

import std/[strutils]
const
  roundPi = 3.1415
  constEval = contains("abc", 'b') # computed at compile time!

한번 선언되면 상수의 심볼을 상수 표현식으로 쓸 수 있어요.

상수 선언의 값 부분은 상수마다 새 스코프를 엽니다. 그래서 그 상수 값 안에서 선언된 심볼은 바깥에서 접근할 수 없어요.

const foo = (var a = 1; a)
const bar = a # error
let baz = a # error

자세한 내용은 Constants and Constant Expressions를 참고하세요.

Static statement/expression (static 문/표현식)

static 문/표현식은 컴파일 타임에 실행되도록 명시적으로 요구합니다. static 블록 안에서는 부작용이 있는 코드조차 허용되는 경우도 있어요.

static:
  echo "echo at compile time"

static은 루틴처럼도 쓸 수 있습니다.

proc getNum(a: int): int = a

# Below calls "echo getNum(123)" at compile time.
static:
  echo getNum(123)

# Below call evaluates the "getNum(123)" at compile time, but its
# result gets used at run time.
echo static(getNum(123))

컴파일 타임에 어떤 Nim 코드가 실행될 수 있는지는 제약이 있어요. 자세한 내용은 Restrictions on Compile-Time Execution을 참고하세요. 그리고 컴파일러가 그 블록을 컴파일 타임에 실행할 수 없다면 정적 오류가 됩니다.

If statement (if 문)

예시를 볼게요.

var name = readLine(stdin)

if name == "Andreas":
  echo "What a nice name!"
elif name == "":
  echo "Don't you have a name?"
else:
  echo "Boring name..."

if 문은 제어 흐름에 분기를 만드는 간단한 방법이에요. if 키워드 뒤의 표현식을 평가해서 참이면 콜론 뒤의 문장들을 실행하고, 거짓이면 elif 분기가 있다면 그 elif의 표현식을 평가합니다. 다시 참이면 해당 문장들을 실행하고, 마지막 elif까지 이 과정을 반복해요. 모든 조건이 거짓이면 else 부분을 실행합니다. else가 없으면 그냥 다음 문장으로 넘어가요.

if 문에서 새 스코프는 if/elif/else 키워드 바로 뒤에서 시작해서 해당 then 블록이 끝나는 지점에서 끝납니다. 시각화를 위해 아래 예시에서 그 스코프를 {| |}로 감쌌어요.

if {| (let m = input =~ re"(\w+)=\w+"; m.isMatch):
  echo "key ", m[0], " value ", m[1]  |}
elif {| (let m = input =~ re""; m.isMatch):
  echo "new m in this scope"  |}
else: {|
  echo "m not declared here"  |}

Case statement (case 문)

예시를 볼게요.

let line = readline(stdin)
case line
of "delete-everything", "restart-computer":
  echo "permission denied"
of "go-for-a-walk":     echo "please yourself"
elif line.len == 0:     echo "empty" # optional, must come after `of` branches
else:                   echo "unknown command" # ditto

# indentation of the branches is also allowed; and so is an optional colon
# after the selecting expression:
case readline(stdin):
  of "delete-everything", "restart-computer":
    echo "permission denied"
  of "go-for-a-walk":     echo "please yourself"
  else:                   echo "unknown command"

case 문은 if 문과 비슷하지만 다중 분기 선택을 나타냅니다. case 키워드 뒤의 표현식을 평가해서, 그 값이 어떤 *slicelist(슬라이스 목록)에 들어 있으면 해당 of 키워드 뒤의 문장을 실행해요. 값이 어느 *slicelist에도 없으면, 뒤따르는 elifelse 부분이 if 문과 같은 의미로 실행됩니다. elifelse: if처럼 처리돼요. elseelif가 없는데 expr이 담을 수 있는 모든 가능한 값이 *slicelist에 다 들어 있지 않다면 정적 오류가 나요. 이 규칙은 **순서 타입(ordinal)**의 표현식에만 적용됩니다. expr의 "모든 가능한 값"은 expr의 타입으로 결정돼요. 이 정적 오류를 막으려면 else: discard를 쓰면 됩니다.

case 문에서 값으로 허용되는 것은 순서 타입, float, string, cstring뿐이에요.

순서가 아닌 타입은 가능한 값을 전부 나열할 수 없으므로 반드시 else 부분이 필요합니다. 예외는 string 타입인데, 지금은 끝에 elseelif 분기가 없어도 되지만, 이게 앞으로도 계속 유지될지는 보장되지 않아요.

case 문은 의미 분석 과정에서 **완전성(exhaustiveness)**을 검사하기 때문에, 각 of 분기의 값은 반드시 상수 표현식이어야 합니다. 이 제약 덕분에 컴파일러가 더 성능 좋은 코드를 만들어낼 수도 있어요.

특별한 의미 확장으로, case 문의 of 분기에 나오는 표현식이 set이나 array 생성자로 평가될 수도 있어요. 그러면 그 set이나 array는 요소들의 목록으로 펼쳐집니다.

const
  SymChars: set[char] = {'a'..'z', 'A'..'Z', '\x80'..'\xFF'}

proc classify(s: string) =
  case s[0]
  of SymChars, '_': echo "an identifier"
  of '0'..'9': echo "a number"
  else: echo "other"

# is equivalent to:
proc classify(s: string) =
  case s[0]
  of 'a'..'z', 'A'..'Z', '\x80'..'\xFF', '_': echo "an identifier"
  of '0'..'9': echo "a number"
  else: echo "other"

case 문은 l-value를 만들어내지 않아요. 그래서 아래 예시는 동작하지 않습니다.

type
  Foo = ref object
    x: seq[string]

proc get_x(x: Foo): var seq[string] =
  # doesn't work
  case true
  of true:
    x.x
  else:
    x.x

var foo = Foo(x: @[])
foo.get_x().add("asd")

이건 resultreturn을 명시적으로 쓰면 고칠 수 있어요.

proc get_x(x: Foo): var seq[string] =
  case true
  of true:
    result = x.x
  else:
    result = x.x

When statement (when 문)

예시를 볼게요.

when sizeof(int) == 2:
  echo "running on a 16 bit system!"
elif sizeof(int) == 4:
  echo "running on a 32 bit system!"
elif sizeof(int) == 8:
  echo "running on a 64 bit system!"
else:
  echo "cannot happen!"

when 문은 if 문과 거의 같지만 몇 가지 차이가 있어요.

  • 각 조건(expr)은 반드시 bool 타입의 상수 표현식이어야 합니다.
  • 문장들이 새 스코프를 열지 않습니다.
  • 참으로 평가된 표현식에 속한 문장만 컴파일러가 변환하고, 나머지 문장은 의미 검사(semantics)를 아예 하지 않아요! 다만 각 조건 자체는 의미 검사를 거칩니다.

when 문은 조건부 컴파일 기법을 가능하게 해줍니다. 특별한 문법 확장으로, when 구문은 객체 정의 안에서도 쓸 수 있어요.

When nimvm statement (when nimvm 문)

nimvmwhen nimvm 문의 표현식으로 쓰이는 특수 심볼이에요. 컴파일 타임 경로와 실행 파일 경로를 구별해 실행을 분기할 때 사용합니다.

예시를 볼게요.

proc someProcThatMayRunInCompileTime(): bool =
  when nimvm:
    # This branch is taken at compile time.
    result = true
  else:
    # This branch is taken in the executable.
    result = false
const ctValue = someProcThatMayRunInCompileTime()
let rtValue = someProcThatMayRunInCompileTime()
assert(ctValue == true)
assert(rtValue == false)

when nimvm 문은 다음 요구 사항을 지켜야 해요.

  • 그 표현식은 반드시 항상 nimvm이어야 합니다. 더 복잡한 표현식은 허용되지 않아요.
  • elif 분기를 포함하면 안 됩니다.
  • 반드시 else 분기를 포함해야 합니다.
  • 분기 안의 코드는 when nimvm 문 다음에 오는 코드의 의미에 영향을 주면 안 됩니다. 예를 들어 다음 코드에서 쓰일 심볼을 정의하면 안 돼요.

Return statement (return 문)

예시를 볼게요.

return 40 + 2

return 문은 현재 프로시저의 실행을 끝냅니다. 프로시저에서만 쓸 수 있어요. 표현식 expr이 있으면 이건 다음의 문법 설탕입니다.

result = expr
return result

표현식 없는 return은, 프로시저에 반환 타입이 있다면 return result를 짧게 쓴 표기입니다. result 변수는 항상 프로시저의 반환값이에요. 컴파일러가 자동으로 선언합니다. 다른 변수들처럼 result도 (이진) 0으로 초기화됩니다.

proc returnZero(): int =
  # implicitly returns 0

Yield statement (yield 문)

예시를 볼게요.

yield (1, 2, 3)

yield 문은 이터레이터에서 return 문 대신 쓰입니다. 이터레이터에서만 유효해요. 실행은 이터레이터를 호출한 for 루프의 몸통으로 되돌아갑니다. yield는 반복 과정을 끝내는 게 아니에요. 다음 반복이 시작되면 실행은 다시 이터레이터로 돌아옵니다. 자세한 내용은 이터레이터 섹션(Iterators and the for statement)을 참고하세요.

Block statement (block 문)

예시를 볼게요.

var found = false
block myblock:
  for i in 0..3:
    for j in 0..3:
      if a[j][i] == 7:
        found = true
        break myblock # leave the block, in this case both for-loops
echo found

block 문은 문장들을 (이름 붙은) 블록으로 묶는 수단입니다. 블록 안에서는 break 문으로 블록을 즉시 빠져나갈 수 있어요. break 문에는 둘러싼 블록의 이름을 적어서 어느 블록을 빠져나갈지 지정할 수 있습니다.

Break statement (break 문)

예시를 볼게요.

break

break 문은 블록을 즉시 빠져나갈 때 씁니다. 심볼(symbol)이 주어지면 그건 빠져나갈 둘러싼 블록의 이름이에요. 심볼이 없으면 가장 안쪽 블록을 빠져나갑니다.

While statement (while 문)

예시를 볼게요.

echo "Please tell me your password:"
var pw = readLine(stdin)
while pw != "12345":
  echo "Wrong password! Next try:"
  pw = readLine(stdin)

while 문은 expr이 거짓으로 평가될 때까지 실행됩니다. 무한 루프는 오류가 아니에요. while 문은 묵시적인 블록을 여는 데다가, 그래서 break 문으로 빠져나갈 수 있어요.

Continue statement (continue 문)

continue 문은 둘러싼 루프 구문의 즉시 다음 반복으로 넘어가게 합니다. 루프 안에서만 허용돼요. continue 문은 중첩된 블록의 문법 설탕입니다.

while expr1:
  stmt1
  continue
  stmt2

이 코드는 다음 코드와 동일해요.

while expr1:
  block myBlockName:
    stmt1
    break myBlockName
    stmt2

Assembler statement (어셈블러 문)

Nim 코드에 어셈블러 코드를 직접 박아 넣는 건 unsafe asm 문으로 지원됩니다. 어셈블러 코드 안에서 Nim 식별자를 가리키는 식별자는 특수 문자로 감싸야 하는데, 그 특수 문자는 문의 프라그마에서 지정할 수 있어요. 기본 특수 문자는 `입니다.

{.push stackTrace:off.}
proc addInt(a, b: int): int =
  # a in eax, and b in edx
  asm """
      mov eax, `a`
      add eax, `b`
      jno theEnd
      call `raiseOverflow`
    theEnd:
  """
{.pop.}

GNU 어셈블러를 쓰면 따옴표와 줄바꿈이 자동으로 삽입됩니다.

proc addInt(a, b: int): int =
  asm """
    addl %%ecx, %%eax
    jno 1
    call `raiseOverflow`
    1:
    :"=a"(`result`)
    :"a"(`a`), "c"(`b`)
  """

이 코드는 다음처럼 따옴표와 줄바꿈을 일일이 적은 코드와 같아요.

proc addInt(a, b: int): int =
  asm """
    "addl %%ecx, %%eax\n"
    "jno 1\n"
    "call `raiseOverflow`\n"
    "1: \n"
    :"=a"(`result`)
    :"a"(`a`), "c"(`b`)
  """

Using statement (using 문)

using 문은 같은 매개변수 이름과 타입이 반복해서 쓰이는 모듈에서 문법상 편의를 제공합니다. 매번 타입을 적는 대신에요.

proc foo(c: Context; n: Node) = ...
proc bar(c: Context; n: Node, counter: int) = ...
proc baz(c: Context; n: Node) = ...

대신 컴파일러에 "이름이 c인 매개변수는 기본적으로 Context 타입이고, nNode 타입이다" 같은 관례를 알려줄 수 있어요.

using
  c: Context
  n: Node
  counter: int

proc foo(c, n) = ...
proc bar(c, n, counter) = ...
proc baz(c, n) = ...

proc mixedMode(c, n; x, y: int) =
  # 'c' is inferred to be of the type 'Context'
  # 'n' is inferred to be of the type 'Node'
  # But 'x' and 'y' are of type 'int'.

using 섹션은 varlet 섹션과 같은 들여쓰기 기반 그룹핑 문법을 사용합니다.

한 가지 주의할 점은, using템플릿에는 적용되지 않아요. untyped 템플릿 매개변수는 기본적으로 system.untyped 타입을 쓰기 때문입니다.

using 선언을 적용받을 매개변수와 명시적으로 타입을 적은 매개변수를 섞는 것도 가능한데, 그 사이에 세미콜론이 필요해요.

If expression (if 표현식)

if 표현식은 if 문과 거의 같지만 표현식이라는 점이 다릅니다. 다른 언어의 *삼항 연산자와 비슷한 기능이에요. 예시를 볼게요.

var y = if x > 8: 9 else: 10

if 표현식은 항상 값을 결과로 내므로 else 부분이 필수입니다. elif 부분도 쓸 수 있어요.

When expression (when 표현식)

if 표현식과 같지만, when 문에 대응하는 버전이에요.

Case expression (case 표현식)

case 표현식도 case 문과 아주 비슷해요.

var favoriteFood = case animal
  of "dog": "bones"
  of "cat": "mice"
  elif animal.endsWith"whale": "plankton"
  else:
    echo "I'm not sure what to serve, but everybody loves ice cream"
    "ice cream"

위 예시에서 보듯 case 표현식도 부작용을 만들 수 있어요. 한 분기에 문장이 여러 개 주어지면, Nim은 마지막 표현식을 결과값으로 사용합니다.

Block expression (block 표현식)

block 표현식은 block 문과 거의 같지만, 블록 안의 마지막 표현식을 값으로 사용하는 표현식이에요. 문장 목록 표현식과 비슷하지만, 문장 목록 표현식은 새 블록 스코프를 열지 않는다는 점이 달라요.

let a = block:
  var fib = @[0, 1]
  for i in 0..10:
    fib.add fib[^1] + fib[^2]
  fib

Table constructor (테이블 생성자)

테이블 생성자는 배열 생성자의 문법 설탕입니다.

{"key1": "value1", "key2", "key3": "value2"}

# is the same as:
[("key1", "value1"), ("key2", "value2"), ("key3", "value2")]

빈 테이블은 {:}로 쓸 수 있어요. (빈 set이 {}인 것과 대조적이죠.) 이것은 곧 빈 배열 생성자 []를 쓰는 또 다른 방법입니다. 이렇게 테이블을 다소 특이하게 지원하는 방식에는 장점이 많아요.

  • (key, value) 쌍의 순서가 보존됩니다. 그래서 예를 들어 {key: val}.newOrderedTable로 순서 있는 딕셔너리를 쉽게 지원할 수 있어요.
  • 테이블 리터럴을 const 섹션에 넣을 수 있고, 컴파일러는 배열과 마찬가지로 실행 파일의 데이터 섹션에 쉽게 넣습니다. 그리고 생성된 데이터 섹션은 최소한의 메모리만 필요해요.
  • 모든 테이블 구현이 문법적으로 동등하게 취급됩니다.
  • 최소한의 문법 설탕을 빼면, 언어 코어는 테이블에 대해 알 필요가 없어요.

Type conversions (타입 변환)

문법적으로 *타입 변환은 프로시저 호출처럼 생겼는데, 프로시저 이름 자리에 타입 이름이 들어옵니다. 타입 변환은 항상 안전해요. 타입을 다른 타입으로 변환하는 데 실패하면 (정적으로 판단할 수 없다면) 예외가 발생하기 때문이에요.

Nim에서는 타입 변환보다 일반 프로시저를 더 선호하는 경우가 흔해요. 예를 들어 $는 관례상 문자열 변환 연산자이고, toFloattoInt는 부동소수점 ↔ 정수 변환에 쓰입니다.

타입 변환은 오버로드된 루틴을 구분하는 데에도 쓸 수 있어요.

proc p(x: int) = echo "int"
proc p(x: string) = echo "string"

let procVar = (proc(x: string))(p)
procVar("a")

부호 없는 숫자에 대한 연산은 래핑(wrap)되고 검사되지 않으므로, 부호 없는 정수로의 타입 변환이나 부호 없는 정수 사이의 타입 변환도 마찬가지로 검사되지 않아요. 이렇게 한 이유는 대개 C에서 Nim으로 알고리즘을 포팅할 때 C 프로그래밍 언어와의 상호 운용성을 더 좋게 하기 위한 것입니다.

*참고: 역사적으로 연산은 검사되지 않았고 변환은 때때로 검사됐어요. 하지만 이 문서와 언어 구현의 리비전 1.0.4부터는 변환도 이제 항상 검사되지 않습니다.

Type casts (타입 캐스트)

*타입 캐스트는 표현식의 비트 패턴을 다른 타입인 것처럼 해석하는 조악한 메커니즘입니다. 타입 캐스트는 저수준 프로그래밍에서만 필요하고, 본질적으로 안전하지 않아요.

cast[int](x)

캐스트의 대상 타입은 **구체 타입(concrete type)**이어야 합니다. 예를 들어 타입 클래스(비구체 타입)를 대상으로 삼으면 유효하지 않아요.

type Foo = int or float
var x = cast[Foo](1) # Error: cannot cast to a non concrete type: 'Foo'

타입 캐스트는 앞 섹션에서 언급한 *타입 변환과 혼동하면 안 돼요. 타입 변환과 달리, 타입 캐스트는 캐스트되는 데이터의 기본 비트 패턴을 바꿀 수 없습니다. (대상 타입의 크기가 소스 타입과 다를 수 있다는 점은 제외하고요.) 캐스팅은 다른 언어의 *type punning이나 C++의 reinterpret_cast, bit_cast와 비슷해요.

대상 타입의 크기가 소스 타입보다 크면, 남는 메모리는 0으로 채워집니다.

The addr operator (addr 연산자)

addr 연산자는 l-value의 주소를 반환합니다. 위치의 타입이 T라면, addr 연산자의 결과 타입은 ptr T입니다. 주소는 항상 추적되지 않는 참조(unraced reference)예요. 스택에 있는 객체의 주소를 취하는 것은 안전하지 않아요. 포인터가 스택 위의 객체보다 오래 살아남을 수 있고, 그렇게 되면 존재하지 않는 객체를 가리키게 되거든요. 변수의 주소는 얻을 수 있어요. C 같은 다른 컴파일 언어와의 상호 운용을 쉽게 하기 위해, let 변수, 매개변수, for 루프 변수의 주소도 얻을 수 있습니다.

let t1 = "Hello"
var
  t2 = t1
  t3 : pointer = addr(t2)
echo repr(addr(t2))
# --> ref 0x7fff6b71b670 --> 0x10bb81050"Hello"
echo cast[ptr string](t3)[]
# --> Hello
# The following line also works
echo repr(addr(t1))

The unsafeAddr operator (unsafeAddr 연산자)

unsafeAddr 연산자는 addr 연산자의 deprecated(더 이상 권장하지 않는) 별칭입니다.

let myArray = [1, 2, 3]
foreignProcThatTakesAnAddr(unsafeAddr myArray)

더 알아보기 (Learn more)