연산자
연산자 (Operators)
Crystal은 하나, 둘, 셋의 피연산자를 가진 다양한 연산자를 지원해요.
연산자 표현식은 사실 메서드 호출로 파싱돼요. 예를 들어 a + b는 a에 대해 b를 인자로 + 메서드를 호출하는 a.+(b)와 의미상 동일해요.
하지만 연산자 문법에는 특별한 규칙이 몇 가지 있어요:
- 보통 수신자와 메서드 이름 사이에 두는 점(
.) — 즉 연산자 — 은 생략할 수 있어요. - 연산자 호출의 연쇄 시퀀스는 연산자 우선순위를 구현하기 위해 컴파일러가 재구성해요.
연산자 우선순위를 강제하면
1 * 2 + 3 * 4같은 표현식이 일반 수학 규칙대로(1 * 2) + (2 * 3)으로 파싱되게 보장해요. - 일반 메서드 이름은 문자나 밑줄로 시작해야 하지만, 연산자는 특수 문자로만 구성돼요. 문자나 밑줄로 시작하지 않는 메서드는 모두 연산자 메서드예요.
- 사용 가능한 연산자는 컴파일러에 화이트리스트로 등록돼 있어요(아래 연산자 목록 참고). 이 리스트는 심볼로만 된 메서드 이름을 허용하고, 우선순위 규칙을 포함해 그들을 연산자로 취급해요.
연산자는 일반 메서드처럼 구현되고, 표준 라이브러리는 예를 들어 수학 표현식에 대해 많은 구현을 제공해요.
연산자 메서드 정의하기 (Defining operator methods)
대부분의 연산자는 일반 메서드로 구현할 수 있어요.
연산자에 어떤 의미든 부여할 수 있지만, 일반적인 연산자 의미와 비슷한 의미로 유지하는 게 좋아요. 헷갈리고 예상 밖으로 동작하는 난해한 코드를 피하려면요.
몇몇 연산자는 컴파일러가 직접 정의해서 사용자 코드에서 재정의할 수 없어요. 예로는 반전 연산자 !, 대입 연산자 =, ||= 같은 복합 대입 연산자, 범위 연산자가 있어요. 메서드를 재정의할 수 있는지는 아래 연산자 표의 Overloadable 열에 표시돼요.
단항 연산자 (Unary operators)
단항 연산자는 접두 표기법으로 쓰고 피연산자가 하나뿐이에요. 그래서 메서드 구현은 인자를 받지 않고 self에만 동작해요.
아래 예시는 2차원 벡터 Vector2 타입에 벡터 반전을 위한 단항 연산자 메서드 -를 보여줘요.
struct Vector2
getter x, y
def initialize(@x : Int32, @y : Int32)
end
# Unary operator. Returns the inverted vector to `self`.
def - : self
Vector2.new(-x, -y)
end
end
v1 = Vector2.new(1, 2)
-v1 # => Vector2(@x=-1, @y=-2)
이항 연산자 (Binary operators)
이항 연산자는 피연산자가 둘이에요. 그래서 메서드 구현은 두 번째 피연산자를 나타내는 인자를 정확히 하나 받아요. 첫 번째 피연산자는 수신자 self예요.
아래 예시는 2차원 벡터 Vector2 타입에 벡터 덧셈을 위한 이항 연산자 메서드 +를 보여줘요.
struct Vector2
getter x, y
def initialize(@x : Int32, @y : Int32)
end
# Binary operator. Returns *other* added to `self`.
def +(other : self) : self
Vector2.new(x + other.x, y + other.y)
end
end
v1 = Vector2.new(1, 2)
v2 = Vector2.new(3, 4)
v1 + v2 # => Vector2(@x=4, @y=6)
관례상, 이항 연산자의 반환 타입은 첫 번째 피연산자(수신자)의 타입이어야 해요. 그래서 typeof(a <op> b) == typeof(a)가 돼요. 그렇지 않으면 대입 연산자(a <op>= b)가 의도치 않게 a의 타입을 바꿔버릴 수 있어요.
합리적인 예외도 있을 수 있어요. 예를 들어 표준 라이브러리에서 정수 타입의 실수 나눗셈 연산자 /는 항상 Float64를 반환해요. 몫이 정수의 값 범위로 제한되면 안 되기 때문이에요.
삼항 연산자 (Ternary operators)
조건 연산자 (? :)는 유일한 삼항 연산자예요. 메서드로 파싱되지 않고 그 의미를 바꿀 수 없어요.
컴파일러가 이를 if 표현식으로 변환해요.
연산자 우선순위 (Operator Precedence)
이 목록은 우선순위 순으로 정렬돼 있어요. 위쪽 항목이 아래쪽 항목보다 더 강하게 결합돼요.
| Category | Operators |
|---|---|
| Index accessors | [], []? |
| Unary | +, &+, -, &-, !, ~ |
| Exponential | **, &** |
| Multiplicative | *, &*, /, //, % |
| Additive | +, &+, -, &- |
| Shift | <<, >> |
| Binary AND | & |
| Binary OR/XOR | |,^ |
| Equality and Subsumption | ==, !=, =~, !~, === |
| Comparison | <, <=, >, >=, <=> |
| Logical AND | && |
| Logical OR | || |
| Range | .., ... |
| Conditional | ?: |
| Assignment | =, []=, +=, &+=, -=, &-=, *=, &*=, /=, //=, %=, |=, &=,^=,**=,<<=,>>=, ||=, &&= |
| Splat | *, ** |
연산자 목록 (List of operators)
산술 연산자 (Arithmetic operators)
단항 (Unary)
| Operator | Description | Example | Overloadable | Associativity |
|---|---|---|---|---|
+ |
양수 | +1 |
yes | right |
&+ |
래핑 양수 | &+1 |
yes | right |
- |
음수 | -1 |
yes | right |
&- |
래핑 음수 | &-1 |
yes | right |
곱셈 (Multiplicative)
| Operator | Description | Example | Overloadable | Associativity |
|---|---|---|---|---|
** |
거듭제곱 | 1 ** 2 |
yes | right |
&** |
래핑 거듭제곱 | 1 &** 2 |
yes | right |
* |
곱셈 | 1 * 2 |
yes | left |
&* |
래핑 곱셈 | 1 &* 2 |
yes | left |
/ |
나눗셈 | 1 / 2 |
yes | left |
// |
바닥 나눗셈 | 1 // 2 |
yes | left |
% |
나머지 | 1 % 2 |
yes | left |
덧셈 (Additive)
| Operator | Description | Example | Overloadable | Associativity |
|---|---|---|---|---|
+ |
덧셈 | 1 + 2 |
yes | left |
&+ |
래핑 덧셈 | 1 &+ 2 |
yes | left |
- |
뺄셈 | 1 - 2 |
yes | left |
&- |
래핑 뺄셈 | 1 &- 2 |
yes | left |
기타 단항 연산자 (Other unary operators)
| Operator | Description | Example | Overloadable | Associativity |
|---|---|---|---|---|
! |
반전 | !true |
no | right |
~ |
이진 보수 | ~1 |
yes | right |
시프트 (Shifts)
| Operator | Description | Example | Overloadable | Associativity |
|---|---|---|---|---|
<< |
왼쪽 시프트, 추가 | 1 << 2, STDOUT << "foo" |
yes | left |
>> |
오른쪽 시프트 | 1 >> 2 |
yes | left |
이진 (Binary)
| Operator | Description | Example | Overloadable | Associativity |
|---|---|---|---|---|
& |
이진 AND | 1 & 2 |
yes | left |
| |
이진 OR | 1 | 2 |
yes | left |
^ |
이진 XOR | 1 ^ 2 |
yes | left |
관계 연산자 (Relational operators)
관계 연산자는 두 값 사이의 관계를 검사해요. _같음(equality)_과 부등(inequalities) 그리고 _포섭(subsumption)_을 포함해요.
같음 (Equality)
같음 연산자 ==는 피연산자들의 값이 같은 것으로 간주되는지 검사해요.
다름 연산자 !=는 반전을 표현하는 약어예요. a != b는 !(a == b)와 동일해야 해요.
다름 연산자를 구현하는 타입은 이 점을 반드시 지켜야 해요. 부등은 등식보다 종종 더 빨리 증명될 수 있기 때문에, 성능을 위해 특별한 구현이 유용할 수 있어요.
두 연산자 모두 가환(commutative)이어야 해요. 즉 a == b는 b == a일 때에만 참이어야 해요. 이것은 컴파일러가 강제하지 않으며, 구현하는 타입이 스스로 신경 써야 해요.
| Operator | Description | Example | Overloadable | Associativity |
|---|---|---|---|---|
== |
같음 | 1 == 2 |
yes | left |
!= |
다름 | 1 != 2 |
yes | left |
INFO: 표준 라이브러리는 연산자가 아닌 또 다른 같음 검사로
Reference#same?를 정의해요. 이것은 참조 동일성(referential identity)을 검사하는데, 두 값이 메모리의 같은 위치를 참조하는지 판정해요.
부등 (Inequalities)
부등 연산자는 값 사이의 순서를 나타내요.
삼자 비교 연산자 <=>(spaceship 연산자라고도 불려요)는 두 요소 사이의 순서를 반환 값의 부호로 표현해요.
| Operator | Description | Example | Overloadable | Associativity |
|---|---|---|---|---|
< |
작음 | 1 < 2 |
yes | left |
<= |
작거나 같음 | 1 <= 2 |
yes | left |
> |
큼 | 1 > 2 |
yes | left |
>= |
크거나 같음 | 1 >= 2 |
yes | left |
<=> |
삼자 비교 | 1 <=> 2 |
yes | left |
INFO: 표준 라이브러리는
Comparable모듈을 정의하며, 이 모듈은 삼자 비교 연산자에서 다른 모든 부등 연산자와 같음 연산자를 파생해요.
포섭 (Subsumption)
패턴 매칭 연산자 =~는 첫 번째 피연산자의 값이 패턴 매칭으로 두 번째 피연산자의 값과 매칭되는지 검사해요.
패턴 비매칭 연산자 !~는 그 반대를 표현해요.
case 포섭 연산자 ===(부정확하게 case 같음 연산자 나 triple equals 로도 불려요)는 오른쪽 피연산자가 왼쪽 피연산자가 나타내는 집합의 멤버인지 검사해요.
정확한 해석은 관련된 데이터 타입에 따라 달라져요.
컴파일러는 case ... when 조건에 이 연산자를 삽입해요.
반대 연산자는 없어요.
| Operator | Description | Example | Overloadable | Associativity |
|---|---|---|---|---|
=~ |
패턴 매칭 | "foo" =~ /fo/ |
yes | left |
!~ |
패턴 비매칭 | "foo" !~ /fo/ |
yes | left |
=== |
case 포섭 | /foo/ === "foo" |
yes | left |
관계 연산자 연쇄 (Chaining relational operators)
관계 연산자 ==, !=, ===, <, >, <=, >=는 연쇄될 수 있고 복합 표현식으로 해석돼요.
예를 들어 a <= b <= c는 a <= b && b <= c로 취급돼요.
다른 연산자를 섞는 것도 가능해요: a >= b <= c > d는 a >= b && b <= c && c > d와 동일해요.
놀라운 결합 동작을 피하려면 같은 우선순위 클래스의 연산자만 결합하는 게 좋아요.
예를 들어 a == b <= c는 a == b && b <= c와 동일하지만, a <= b == c는 a <= (b == c)와 동일해요.
논리 (Logical)
| Operator | Description | Example | Overloadable | Associativity |
|---|---|---|---|---|
&& |
논리 AND | true && false |
no | left |
|| |
논리 OR | true || false |
no | left |
범위 (Range)
범위 연산자는 Range 리터럴에서 사용돼요.
| Operator | Description | Example | Overloadable |
|---|---|---|---|
.. |
포함 범위 | 1..10 |
no |
... |
제외 범위 | 1...10 |
no |
스플랫 (Splats)
스플랫 연산자는 메서드 인자에서 튜플을 분해할 때만 쓸 수 있어요. 자세한 내용은 Splats and Tuples를 참고하세요.
| Operator | Description | Example | Overloadable |
|---|---|---|---|
* |
스플랫 | *foo |
no |
** |
더블 스플랫 | **foo |
no |
조건 (Conditional)
조건 연산자 (? :)는 내부적으로 컴파일러가 if 표현식으로 다시 써요.
| Operator | Description | Example | Overloadable | Associativity |
|---|---|---|---|---|
? : |
조건 | a == b ? c : d |
no | right |
대입 (Assignments)
대입 연산자 =는 두 번째 피연산자의 값을 첫 번째 피연산자에 대입해요. 첫 번째 피연산자는 변수(이 경우 연산자를 재정의할 수 없어요)이거나 호출(이 경우 연산자를 재정의할 수 있어요)이에요.
자세한 내용은 assignment를 참고하세요.
| Operator | Description | Example | Overloadable | Associativity |
|---|---|---|---|---|
= |
변수 대입 | a = 1 |
no | right |
= |
호출 대입 | a.b = 1 |
yes | right |
[]= |
인덱스 대입 | a[0] = 1 |
yes | right |
복합 대입 (Combined assignments)
대입 연산자 =는 연산자와 대입을 결합하는 모든 연산자의 기반이에요. 일반적인 형태는 a <op>= b이고, 컴파일러가 이를 a = a <op> b로 변환해요.
일반 확장 공식의 예외는 논리 연산자들이에요:
a ||= b는a || (a = b)로 변환돼요a &&= b는a && (a = b)로 변환돼요
a가 인덱스 접근자([])일 때는 또 다른 특별한 경우가 있어요. 왼쪽이 nil 가능 변형([]?)으로 바뀌어요:
a[i] ||= b는a[i]? || (a[i] = b)로 변환돼요a[i] &&= b는a[i]? && (a[i] = b)로 변환돼요
모든 변환은 수신자(a)가 변수라고 가정해요. 호출이라면 대체는 의미상 동일하지만 구현이 조금 더 복잡하고(익명 임시 변수 도입), a=가 호출 가능해야 한다고 기대해요.
수신자는 변수나 호출 외에 다른 것이 될 수 없어요.
| Operator | Description | Example | Overloadable | Associativity |
|---|---|---|---|---|
+= |
덧셈 및 대입 | i += 1 |
no | right |
&+= |
래핑 덧셈 및 대입 | i &+= 1 |
no | right |
-= |
뺄셈 및 대입 | i -= 1 |
no | right |
&-= |
래핑 뺄셈 및 대입 | i &-= 1 |
no | right |
*= |
곱셈 및 대입 | i *= 1 |
no | right |
&*= |
래핑 곱셈 및 대입 | i &*= 1 |
no | right |
/= |
나눗셈 및 대입 | i /= 1 |
no | right |
//= |
바닥 나눗셈 및 대입 | i //= 1 |
no | right |
%= |
나머지 및 대입 | i %= 1 |
yes | right |
|= |
이진 OR 및 대입 | i |= 1 |
no | right |
&= |
이진 AND 및 대입 | i &= 1 |
no | right |
^= |
이진 XOR 및 대입 | i ^= 1 |
no | right |
**= |
거듭제곱 및 대입 | i **= 1 |
no | right |
<<= |
왼쪽 시프트 및 대입 | i <<= 1 |
no | right |
>>= |
오른쪽 시프트 및 대입 | i >>= 1 |
no | right |
||= |
논리 OR 및 대입 | i ||= true |
no | right |
&&= |
논리 AND 및 대입 | i &&= true |
no | right |
인덱스 접근자 (Index Accessors)
인덱스 접근자는 인덱스나 키로 값을 조회하는 데 쓰여요. 예를 들어 배열 항목이나 맵 항목이요. nil 가능 변형 []?는 인덱스를 찾지 못하면 nil을 반환해야 하고, nil 불가 변형은 그 경우 예외를 던져요.
표준 라이브러리 구현은 보통 KeyError나 IndexError를 던져요.
| Operator | Description | Example | Overloadable |
|---|---|---|---|
[] |
인덱스 접근자 | ary[i] |
yes |
[]? |
nil 가능 인덱스 접근자 | ary[i]? |
yes |
출처: Crystal 공식 문서
더 알아보기 (Learn more)
- 대입 연산자의 세부 규칙은 대입 (Assignment) 문서를 확인해 보세요.
- 논리 AND는 && 연산자, 논리 OR는 || 연산자 문서를 함께 보세요.