expr — 표현식 평가하기

expr — 표현식 평가하기

스크립트에서 계산을 하거나 조건을 비교해야 할 때가 많아요. 단순히 명령을 나열하는 것과 달리, 수식을 평가해 값으로 만들고 싶을 때 Tcl의 expr 명령을 써요.

출처: Tcl 공식 문서 — expr

본문

시그니처

expr arg ?arg arg ...?

설명

args를 이어 붙이고(그 사이에 구분 공백 추가) 결과를 Tcl 표현식으로 평가한 뒤 그 값을 반환해요. Tcl 표현식에 허용되는 연산자는 C 표현식에 허용되는 연산자의 부분집합을 포함해요. Tcl과 C에 공통인 연산자에 대해 Tcl은 해당 C 연산자와 같은 의미와 우선순위를 적용해요. 표현식은 거의 항상 수치 결과(정수나 부동소수점 값)를 내요. 예를 들어 표현식

expr 8.2 + 6

는 14.2로 평가돼요. Tcl 표현식은 피연산자를 지정하는 방식에서 C 표현식과 달라요. 또한 Tcl 표현식은 비수치 피연산자와 문자열 비교, 그리고 C에 없는 추가 연산자를 지원해요.

피연산자 (Operands)

Tcl 표현식은 피연산자, 연산자, 괄호, 쉼표의 조합으로 되어 있어요. 공백은 피연산자·연산자·괄호(또는 쉼표) 사이에 쓸 수 있고 표현식 명령에 무시돼요. 가능하면 피연산자는 정수 값으로 해석돼요. 정수 값은 10진수(보통), 2진수(피연산자의 처음 두 문자가 0b), 8진수(0o), 16진수(0x)로 지정할 수 있어요. 옛 Tcl 릴리스와의 호환을 위해 피연산자의 첫 문자가 0이면 둘째 문자가 o이든 아니든 단순히 8진수 정수 값을 나타내기도 해요.

피연산자가 위 정수 형식 중 하나가 아니면, 가능하면 부동소수점 숫자로 취급돼요. 부동소수점 숫자는 십진 숫자, 소수점 ., 과학 표기법의 e·E, 부호 +·-를 쓰는 여러 일반 형식으로 지정할 수 있어요. 예를 들어 다음은 모두 유효한 부동소수점 숫자예요: 2.1, 3., 6e4, 7.91e+16. 또한 Inf·NaN 문자열도 각 문자가 어떤 대소문자든 부동소수점 값으로 인식돼요. 수치 해석이 불가능하면(수치·불리언이 아닌 모든 리터럴 피연산자는 중괄호나 큰따옴표로 인용해야 함) 피연산자는 문자열로 남아요(그리고 제한된 연산자 집합만 적용할 수 있어요).

피연산자는 다음 중 어떤 방식으로든 지정할 수 있어요:

  • 정수든 부동소수점이든 수치 값으로
  • string is boolean이 이해하는 어떤 형태든 불리언 값으로
  • 표준 $ 표기법을 쓰는 Tcl 변수로(변수 값이 피연산자로 쓰임)
  • 큰따옴표로 감싼 문자열로(표현식 파서가 따옴표 사이의 정보에 백슬래시·변수·명령 치환을 수행하고 결과 값을 피연산자로 사용)
  • 중괄호로 감싼 문자열로(여는 중괄호와 닫는 중괄호 사이의 문자는 치환 없이 피연산자로 사용)
  • 대괄호로 감싼 Tcl 명령으로(명령이 실행되고 그 결과가 피연산자로 사용)
  • sin($x)처럼 인자가 위 피연산자 형식 중 하나인 수학 함수로

위 치환(예: 인용 문자열 안)이 일어나는 곳에서는 표현식 명령이 그것을 수행해요. 다만 명령 파서가 표현식 프로세서를 호출하기 전에 이미 한 라운드의 치환을 수행했을 수 있어요. 아래에서 논의하듯, 명령 파서가 내용에 치환을 수행하지 못하도록 표현식을 중괄호로 감싸는 게 보통 가장 좋아요.

간단한 표현식 예를 들면, 변수 a가 값 3이고 b가 6이라고 할 때, 아래 각 줄 왼쪽 명령은 오른쪽 값을 만들어요:

expr {3.1 + $a} 6.1
expr {2 + "$a.$b"} 5.6
expr {4*[llength "6 2"]} 8
expr {{word one} < "word $a"} 0

연산자 (Operators)

유효한 연산자(대부분 tcl::mathop 네임스페이스에서 명령으로도 사용 가능; mathop(n) 매뉴얼 참고)는 우선순위가 낮아지는 순서로 아래에 나열돼요.

- + ~ !

단항 마이너스, 단항 플러스, 비트 단위 NOT, 논리 NOT. 이 연산자들은 문자열 피연산자에 적용할 수 없고, 비트 단위 NOT은 정수에만 적용할 수 있어요.

**

거듭제곱. 임의 수치 피연산자에 유효해요. 첫 숫자가 1보다 큰 정수일 때 Tcl이 처리할 수 있는 최대 지수 값은 268435455예요.

* / %

곱셈, 나눗셈, 나머지. 이 연산자들은 문자열 피연산자에 적용할 수 없고, 나머지는 정수에만 적용할 수 있어요. 나머지는 항상 제수(divisor)와 같은 부호를 가지며 제수의 절댓값보다 작은 절댓값을 가져요.

정수에 적용할 때 나눗셈·나머지 연산자는 수직선을 각 조각이 제수 크기인 인접한 겹치지 않는 같은 크기 조각들의 시퀀스로 나누는 것으로 볼 수 있어요. 나눗셈 결과는 제수가 어느 조각 안에 있었는지를, 나머지 결과는 그 조각의 어디에 제수가 있었는지를 나타내요. 그 결과로 "-57 / 10"의 결과는 항상 -6이고, "-57 % 10"의 결과는 항상 3이에요.

+ -

덧셈과 뺄셈. 임의 수치 피연산자에 유효해요.

<< >>

왼쪽·오른쪽 시프트. 정수 피연산자에만 유효해요. 오른쪽 시프트는 항상 부호 비트를 전파해요.

< > <= >=

불리언 미만, 초과, 이하, 이상. 각 연산자는 조건이 참이면 1, 아니면 0을 만들어요. 이 연산자들은 문자열과 수치 피연산자 모두에 적용할 수 있고, 그 경우 문자열 비교가 사용돼요.

== !=

불리언 같음·다름. 각 연산자는 0/1 결과를 만들어요. 모든 피연산자 타입에 유효해요.

eq ne

불리언 문자열 같음·문자열 다름. 각 연산자는 0/1 결과를 만들어요. 피연산자 타입은 문자열로만 해석돼요.

in ni

목록 포함·부정 목록 포함. 각 연산자는 0/1 결과를 만들고 첫 인자를 문자열로, 둘째 인자를 Tcl 목록으로 취급해요. in 연산자는 첫 인자가 둘째 인자 목록의 멤버인지 나타내고, ni 연산자는 결과의 의미를 뒤집어요.

&

비트 단위 AND. 정수 피연산자에만 유효해요.

^

비트 단위 배타적 OR. 정수 피연산자에만 유효해요.

|

비트 단위 OR. 정수 피연산자에만 유효해요.

&&

논리 AND. 두 피연산자가 모두 0이 아니면 1, 아니면 0 결과를 만들어요. 불리언·수치(정수·부동소수점) 피연산자에만 유효해요. 이 연산자는 게으르게(lazily) 평가해서 결과를 결정하는 데 필요할 때만 둘째 피연산자를 평가해요.

||

논리 OR. 두 피연산자가 모두 0이면 0, 아니면 1 결과를 만들어요. 불리언·수치 연산자에만 유효해요. 이 연산자는 게으르게 평가해요.

x ? y : z

C에서처럼 if-then-else. x가 0이 아니면 결과는 y의 값, 아니면 z의 값이에요. x 피연산자는 불리언이나 수치 값을 가져야 해요. 이 연산자는 게으르게 평가해 y·z 중 하나만 평가해요.

각 연산자가 만드는 결과에 대한 자세한 내용은 C 매뉴얼을 참고하세요. 거듭제곱 연산자는 곱셈·나눗셈 연산자처럼 타입을 승격시키고, (타입 변환 후) pow 함수의 출력과 같은 결과를 만들어요.

거듭제곱을 제외한 모든 이항 연산자는 같은 우선순위 수준 안에서 왼쪽에서 오른쪽으로 결합하고, 거듭제곱은 오른쪽에서 왼쪽으로 결합해요. 예를 들어

expr {4*2 < 7}

는 0을 반환하고,

expr {2**3**2}

는 512를 반환해요.

&&, ||, ?: 연산자는 C에서처럼 "게으른 평가"를 하는데, 결과를 결정하는 데 필요하지 않으면 피연산자를 평가하지 않는다는 뜻이에요. 예를 들어

expr {$v?[a]:[b]}

에서 $v의 값에 따라 "[a]" 또는 "[b]" 중 하나만 실제로 평가돼요. 다만 이것은 전체 표현식이 중괄호로 감싸진 경우에만 참이에요. 그렇지 않으면 Tcl 파서가 expr 명령을 호출하기 전에 "[a]""[b]"를 둘 다 평가해요.

수학 함수 (Math Functions)

표현식 파서가 sin($x) 같은 수학 함수를 만나면 이것을 tcl::mathfunc 네임스페이스의 보통 Tcl 명령 호출로 바꿔요. 다음과 같은 표현식의 처리는

expr {sin($x+$y)}

모든 면에서 다음 처리와 같아요:

expr {[tcl::mathfunc::sin [expr {$x+$y}]]}

그리고 이것은 다시 다음 처리와 같아요:

tcl::mathfunc::sin [expr {$x+$y}]

실행기는 네임스페이스에서 함수를 해석하는 보통 규칙으로 tcl::mathfunc::sin을 찾아요. ::tcl::mathfunc::sin이나 [namespace current]::tcl::mathfunc::sin이 요청을 충족시키고, 다른 것들도(현재 네임스페이스 경로 설정에 따라) 충족할 수 있어요.

일부 수학 함수는 C에서처럼 쉼표로 구분된 여러 인자를 가져요. 그래서:

expr {hypot($x,$y)}

는 이렇게 끝나요:

tcl::mathfunc::hypot $x $y

기본으로 제공되는 수학 함수는 mathfunc(n) 매뉴얼 페이지를 참고하세요.

타입, 오버플로우, 정밀도

정수와 관련된 모든 내부 계산은 필요에 따라 LibTomMath 다중 정밀도 정수 라이브러리를 호출해 수행돼서, 모든 정수 계산이 정확히 수행돼요. Tcl 8.5 이전 릴리스에서는 정수 계산을 C 타입 long intTcl_WideInt 중 하나로 수행해, 값이 그 타입의 범위를 넘어서면 암묵적 범위 절단이 일어났어요. 이런 암묵적 절단에 의존했던 코드는 절단이 필요한 지점의 표현식에 int()·wide() 함수 호출을 명시적으로 추가해야 해요.

부동소수점과 관련된 모든 내부 계산은 C 타입 double로 수행돼요. 문자열을 부동소수점으로 변환할 때 지수 오버플로우가 감지되고, 적절히 Inf·-Inf의 double 값이 돼요. 부동소수점 오버플로우·언더플로우는 하드웨어가 지원하는 정도까지 감지되며, 일반적으로 꽤 신뢰할 수 있어요.

정수·부동소수점·문자열 피연산자의 내부 표현 간 변환은 필요에 따라 자동으로 수행돼요. 산술 계산에서는 어떤 부동소수점 숫자가 도입되기 전까지는 정수를 쓰고, 이후에는 부동소수점을 써요. 예를 들어

expr {5 / 4}

는 1을 반환하고,

expr {5 / 4.0}
expr {5 / ( [string length "abcd"] + 0.0 )}

은 둘 다 1.25를 반환해요. 부동소수점 값은 정수 값처럼 보이지 않도록 항상 ".""e"와 함께 반환돼요. 예를 들어

expr {20.0/5.0}

는 4가 아니라 4.0을 반환해요.

문자열 연산

문자열 값은 비교 연산자의 피연산자로 쓸 수 있지만, 표현식 평가기는 가능하면(즉 연산자에 대한 모든 인자가 수치 해석을 허용할 때) eq·ne 연산자의 경우를 제외하고 비교를 정수나 부동소수점으로 하려고 해요. 비교의 한 피연산자가 문자열이고 다른 하나가 수치 값을 가지면, 문자열 피연산자와 비교하기 위해 수치 피연산자 값의 정규 문자열 표현이 만들져요. 정수 값의 정규 문자열 표현은 십진 문자열 형식이에요. 부동소수점 값의 정규 문자열 표현은 Tcl format 명령의 %g 형식 지정자가 만드는 것이에요. 예를 들어

expr {"0x03" > "2"}
expr {"0y" > "0x12"}

는 둘 다 1을 반환해요. 첫 비교는 정수 비교로, 둘째는 문자열 비교로 수행돼요. Tcl이 가능하면 값을 숫자로 취급하려는 경향 때문에, 정말 문자열 비교를 원하면서 피연산자 값이 임의일 수 있을 때 == 같은 연산자를 쓰는 건 일반적으로 좋지 않아요. 그런 경우에는 eq·ne 연산자나 string 명령을 쓰는 게 더 좋아요.

성능 고려사항

최고 속도와 최소 저장 공간을 위해 표현식을 중괄호로 감싸요. 이렇게 하면 Tcl 바이트코드 컴파일러가 최상의 코드를 만들 수 있어요.

앞서 언급했듯이 표현식은 두 번 치환돼요. 한 번은 Tcl 파서가, 한 번은 expr 명령이요. 예를 들어

set a 3
set b {$a + 2}
expr $b*4

는 4의 배수가 아니라 11을 반환해요. Tcl 파서가 먼저 변수 b에 대해 "$a + 2"를 치환하고, 그다음 expr 명령이 표현식 "$a + 2*4"를 평가하기 때문이에요.

대부분의 표현식은 둘째 라운드의 치환이 필요하지 않아요. 중괄호로 감싸져 있거나, 그렇지 않으면 변수·명령 치환이 자체 치환이 필요 없는 숫자·문자열을 내기 때문이에요. 다만 일부 중괄호 없는 표현식이 두 라운드의 치환이 필요하기 때문에, 바이트코드 컴파일러는 이 상황을 처리할 추가 명령을 내보내야 해요. 가장 비싼 코드는 명령 치환을 포함한 중괄호 없는 표현식에 필요해요. 그런 표현식은 표현식이 실행될 때마다 새 코드를 생성해 구현해야 해요.

그렇지 않으면 상수인 표현식의 더 넓은 맥락 안에 비상수 표현식 문자열을 넣어야 한다면, 가장 효율적인 기법은 변하는 부분을 재귀 expr 안에 두는 거예요. 그러면 적어도 바깥 부분은 컴파일될 수 있어요(변하는 부분은 자체가 별도 표현식으로 평가돼야 하긴 해요). 그래서 이 예제에서 결과는 20이고 바깥 표현식은 완전히 캐시된 바이트코드 컴파일의 이점을 받아요.

set a 3
set b {$a + 2}
expr {[expr $b] * 4}

함수나 연산자의 치환을 허용하려고 표현식이 중괄호 없이 쓰이면, mathfunc(n)·mathop(n) 매뉴얼 페이지에 문서화된 명령을 직접 쓰는 걸 고려해 보세요.

예제

"흥미로운" 수학 함수를 계산하는 프로시저를 정의해요:

proc tcl::mathfunc::calc {x y} {
 expr { ($x**2 - $y**2) / exp($x**2 + $y**2) }
}

극좌표를 직교좌표로 변환해요:

# convert from ($radius,$angle)
set x [expr { $radius * cos($angle) }]
set y [expr { $radius * sin($angle) }]

직교좌표를 극좌표로 변환해요:

# convert from ($x,$y)
set radius [expr { hypot($y, $x) }]
set angle [expr { atan2($y, $x) }]

두 문자열 값의 관계를 설명하는 메시지를 출력해요:

puts "a and b are [expr {$a eq $b ? {equal} : {different}}]"

환경 변수가 정의되어 있으면서도 참 불리언 값으로 설정됐는지를 변수에 설정해요:

set isTrue [expr {
 [info exists ::env(SOME_ENV_VAR)] &&
 [string is true -strict $::env(SOME_ENV_VAR)]
}]

0..99 범위의 무작위 정수를 만들어요:

set randNum [expr { int(100 * rand()) }]

더 알아보기

  • array, for, if, mathfunc, mathop, namespace, proc, string, Tcl, while
  • 키워드: arithmetic, boolean, compare, expression, fuzzy comparison