Expression
모든 프로그래밍 언어는 수식을 갖고 있을 수 밖에 없다. (프롤로그조차도 포함했으니)
수식을 통해 계산한 결과 값을 assignment statement를 통해 변수에 저장한다.
Arithmetic Expression
수식 중에 제일 간단한 것은 산술 수식이다.
산수식은 operator, operand, 괄호, 함수 호출로 구성된다.
대부분의 언어에서 이항연산자는 infix 지만, 일부 언어에서는 prefix 를 사용하기도 한다. (LISP)
당연히 postfix를 사용하는 언어도 있다..
연산자는 크게 단항(unary), 이항(binary), 삼항(ternary) 연산자로 구분된다.
피연산자 개수가 12개가 될 수도 있고, 행렬이 될 수도 있고.. 언어마다 다양하다.
Arithmetic Expression 을 설계할 때는 어떤 점을 고민해야할까
- Operator Precedence rule
여러 연산자 중에 어떤 연산자를 먼저 계산해야 하는가 (우선순위)
인접한 연산자들에 대해 우선순위를 결정하는 규칙
보통 괄호 - 단항 - 제곱 - 곱셈/나눗셈 - 덧뺄셈 순으로 연산한다.
관계 연산자와 비트 연산자도 있는데, 이들 사이에는 사실 우선순위가 문제될 게 없다.
그래도 이 연산자들이 하나의 expression에 나왔을 때 누굴 먼저 연산할지는 정의를 해두어야 한다.
- Operator Associativity rule
같은 우선순위를 가진 연산자에 대해 누구부터 할 것인가
보통 왼쪽에서 오른쪽 방향으로 연산한다. (left associativity)
하지만 제곱을 나타내는 연산자는 right associativity를 갖는다.
APL은 모든 연산자가 동등한 우선순위를 가지며 오른쪽부터 연산한다고 한다.
이 연산 우선순위는 괄호를 사용해 오바리이딩될 수 있다.
- Operand Evaluation Order
피연산자의 값은 어떤 순서로 확인하는가
1. 변수 (메모리에서 값을 가져온다.)
2. 상수 (메모리/명령어에서 상수값을 가져온다.)
3. 괄효 표현식 계산
4. 함수 호출
순으로 실제 값을 얻어온다.
- Operand evaluation side effect
함수를 호출하다보면 side effect가 발생할 수 있다. (Fuctional Side Effects)
특히, non-local variable을 건드는 경우 발생하기 쉽다.
함수를 실행하다가 난 이 변수값을 바꾼적이 없는데 값이 바뀌어있는 경우가 side effect가 발생한 경우다.
이렇게 함수의 인자로 변수 주소를 넘기면 함수 내부에서 변수의 값을 직접 조작할 수 있으니, a + func(&a) 에서 a도 바뀔 수 있고, 그 값이 b로 들어갈 수도 있다.
똑똑한 컴파일러는 그러면 func(&a)를 먼저 하고 a를 하면 되겠지! 라고 판단할 수도 있겠지만 (교환법칙), 이 경우, fun() 실행시 오류가 발생할 수도 있다..
이 문제를 해결하는 방법은 2가지가 있다.
1. functional side effect 가 발생하지 않도록 언어 규칙에서 제한을 건다.
non-local variable을 사용하지 못하게 하고, two-way parameter도 사용하지 못하게 막는다.
장점은 사이드 이펙트가 발생하지 않도록 할 수 있다는 것
단점은 유연성이 떨어지는 것이다. (함수가 열심히 작업한 값을 내가 가져다 사용하지 못한다..)
2. 피연산자의 값을 결정하는 순서를 고정시킨다.
(교환법칙 같은 것 인정 안한다. func(&a)부터 먼저 해라)
자바가 채택한 방법이다.
이와 관련된 것으로 Referential Transparency 가 있다.
함수가 어떤 값을 건들이면, 이를 남들에게 투명하게 다 공개하는 것을 말한다.
공개할 수 없으면 건들이지도 말아야 한다.
만약 side effect가 없다면 result1 == result2 일 것이다.
아니라면 referential transparency 가 위반된 것이다.
이 특성을 사용하면 프로그램의 시멘틱(의미)이 명확해지는 장점이 있으니 프로그래밍이 쉬워진다.
특히 순수 함수형 언어는 이 특성을 갖는다. 함수가 state를 갖지 않기 때문에 주어진 값에 대해 항상 같은 값을 내뱉기 때문이다. (외부의 값을 가져다 쓸 때는 항상 상수만을 사용해야하며, 변할 수 있는 값은 항상 파라미터로 받는다.)
- Operator Overloading
연산자를 기존의 목적 외에도 다른 목적으로 사용하는 것을 operator overloading 이라고 한다.
보통은 + 연산이 덧셈이라는 한가지 의미만 갖지만, C에서는 * 연산자가 곱셈과 포인터 연산으로 사용되니 잠재적 문제가 있을 수 있다. (하나는 이항, 하나는 단항이니 구분이 되지만, 컴파일러가 이를 못잡는 순간 신텍스 에러가 발생한다.)
C++, C#, F#은 연산자를 사용자가 마음대로 재정의할 수 있도록 기능을 제공한다.
프로그램이 점점 깔끔해지고 좋아진다.
하지만 경우에 따라 이상하게 재정의하면 readability 가 나빠질 수도 있다.
- Type mixing in expression
다른 타입이 섞여있을 때 연산하는 경우 생각할 수 있는 방법이 type conversion이다.
언제든 일어날 수 있는데, 막 일어나도 되냐, 절대로 일어나면 안되냐, 일어나도 되는 경우가 있느냐 와 같은 것을 고민한다.
type conversion은 크게 2가지로 나뉜다.
1. narrowing conversion
만약 실수를 정수형으로 바꾸면 소수점 아래 유효숫자가 날아갈 것이다.
따라서 정밀도가 떨어진다.
이렇게 좁아지는 방향으로 바꾸는 것을 말한다.
2. widening conversion
넓어지는 방향으로 바꾸는 것을 말한다.
단, 정수형을 실수형으로 바꾸면 (제일 큰 정수형을 사용했을 때) 일부 bit가 실수의 유효숫자쪽으로 넘어가버리니 정확한 값으로 바뀌지 않을 수 있다. 따라서 이런 경우는 항상 widening이 아닐 수 있다.
이제 이런 저런 타입이 하나의 exression에 섞여서 나오는 경우를 보자.
이때는 overloading 과 밀접한 관련이 있다.
타입 오버로딩이 되니 타입을 섞어서 쓸 수 있는 것이다.
type coercion 은 컴파일러가 오버로딩을 할 수 있도록 자동으로 강제 형변환 하는 것을 말한다.
이 경우, 프로그래머가 예상하지 못한 잠재적 위험이 발생할 수 있다.
그래서 대부분의 언어에서는 widening conversion 만 하도록 제한을 걸어둔다.
ML, F#에서는 coercion이 없고, 항상 프로그래머가 직접 형변환을 하도록 제한한다.
이런 경우를 명시적 타입 변환 (Explicit Type Conversion) 이라고 하는데, 다르게말해서 casting 이라고도 한다. (C언어 계열)
expression 에서 발생하는 에러는 몇가지가 더 있다.
부동소수점에서 표현할 수 있는 아주 작은 양수를 서로 곱했을 때 범위를 넘어가서 0이 된다거나, (underflow)
이 결과를 가지고 나눈다거나 (zero division), 제일 정수에 2를 곱한다거나 (overflow) 하는 런타임 시스템이 잡기 힘든 문제가 발생할 수 있다.
Conditional Expression
C언어 기반의 언어는 3항 연산자 (? :) 를 사용한 조건식을 제공한다.
Relational Expression (Boolean Expression)
같다/다르다, 크다 작다에 대한 참/거짓 결과를 반환한다.
다르다는 표현은 언어마다 다양하다.
!=, /=, ~=, .NE. , <>, # 등등
자바스크립트와 PHP는 ===, !== 와 같은 기호도 있다.
값만 비교할 것인지, 타입까지 비교할 것인지에 대한 차이다.
(type coercion 여부)
Boolean Expression에 대해서는, C언어의 경우 Boolean type 이 없어서 int 타입으로 그 역할을 대신했다.
(0이면 false 아니면 참)
그런데 이렇게 했더니 문제가 생겼다.
a < b < c 에서 a = 2, b = 0, c= 1 을 넣었다고 해보자.
a < b 는 거짓이므로 0이 되어서 0과 c를 비교하니 원래는 거짓이 나와야 하는데 참이 나온 것이다.
다른 문제로는 Short Circuit Evaluation 문제도 있다.
이 식에서 a = 0 이면 13 * a = 0 이므로 곱셈에 대해 그 뒤의 연산은 굳이 계산할 필요 없이 항상 0이다.
이렇게 뒤의 연산이 할 필요 없이 값이 결정되면, 뒤 연산을 하지 않는 것을 short curcuit evaluation 이라고 한다.
그런데 non-short-circuit evaluation 문제가 있다.
이 코드를 보면 index == length 가 되면 out of range가 나기 때문에, 반드시 sort-circuit evaluation을 해야 에러를 피할 수 있다.
반대로 short circuit을 해서 문제가 발생하는 경우가 있다.
short circuit을 하면 b++이 실행되지 않아서 의도와 다르게 동작할 수도 있다.
C/C++, Java는 boolean operator 에 short circuit을 지원하는데, bitwise 에서는 지원하지 않는다.
ruby, perl, ML, F#, python은 short circuit을 지원한다.
Assignment Statements
왼쪽에 target, 가운데에 assign operator, 오른쪽에 expression이 나오는 것이 일반적인 형태다.
이떄 assign_operator 는 = 을 많이 사용하는데,
어떤 언어는 = 을 동등 비교 연산자로 써서 := 와 같은 기호로 표현하기도 한다.
C계열은 = 를 대입 연산자로 쓰므로, 동등비교를 할 때는 == 를 사용한다.
Perl 에서는 신기하게 Conditional Target을 사용하기도 한다.
값은 하나로 고정인데, 어디에 저장할지를 조건에 따라 결정하는 것이다.
ALGOL과 C계열은 compound Assignment Operator (복합 대입 연산자) 를 지원한다.
a += b 와 같은 식으로 쓸 수 있으며, 어셈블리에서 뽑아온 언어다.
대입 연산자 중에서는 단항 대입 연산자도 있다. (unary assignment operators)
예를 들어, C계열 언어는 a++ 과 같은 연산자를 지원하는데, 이 연산의 의미는 대입한 뒤에 1 증가 시키라는 의미이므로 대입 연산자와 감소/증가 연산이 합쳐진 것으로도 볼 수 있다.
맨 아래의 경우, count 를 ++ 해서 증가시키고 그 값을 할당까지 한 뒤에 - 연산을 취한다.
C계열, Perl, Javascript 와 같은 언어에서는 assignment statement 가 생성한 결과를 operand로 사용할 수도 있다.
이렇게 하는 경우, 다른 side effect가 발생할 수 있다는 단점이 있기는 하다.
Perl, Ruby 와 같은 언어는 이렇게 여러 값을 여러 타겟에 한번에 할당하는 연산을 지원한다.
이를 사용해 swap 도 간단하게 작성할 수 있다.
함수형 언어에서의 Assignment 는 변수가 아니라 바인딩의 개념이다.
identifier가 값을 대표하는 이름이고 변수가 아니다! (symbol 이다.)
따라서 만약 값을 주고 싶으면 var 이라는 키워드 (F#은 let 이라는 키워드 사용, var과 비슷하지만 새로운 scope를 만드는 차이가 있다.) 를 사용할 수 있다.
Assignment 에서도 서로 다른 타입의 값-타겟을 할당하려는 경우가 있을 수 있다.
이런 경우를 mixed-mode 라고 한다.
포트란, C/C++, Perl 과 같은 언어는 모든 숫자형 타입은 다른 숫자형 타입 변수에 저장될 수 있다.
자바와 C#은 widening assignment coercion 만 발생한다.
Ada는 직접 타입 캐스팅을 명시적으로 해야 오류가 발생하지 않는다.
'CS > 프로그래밍언어론' 카테고리의 다른 글
[프로그래밍언어론] 13. Subprograms (0) | 2024.06.13 |
---|---|
[프로그래밍언어론] 11. Logic Programming Language (Prolog) (0) | 2024.06.13 |
[프로그래밍언어론] 10. Data Type (0) | 2024.06.13 |
[프로그래밍언어론] 9. 함수형 언어 (LISP) (0) | 2024.06.12 |
[프로그래밍언어론] 8. Name & Binding (0) | 2024.06.12 |