연산자

연산자 (Operators)

Crystal은 하나, 둘, 셋의 피연산자를 가진 다양한 연산자를 지원해요.

연산자 표현식은 사실 메서드 호출로 파싱돼요. 예를 들어 a + ba에 대해 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 == bb == 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 <= ca <= b && b <= c로 취급돼요. 다른 연산자를 섞는 것도 가능해요: a >= b <= c > da >= b && b <= c && c > d와 동일해요.

놀라운 결합 동작을 피하려면 같은 우선순위 클래스의 연산자만 결합하는 게 좋아요. 예를 들어 a == b <= ca == b && b <= c와 동일하지만, a <= b == ca <= (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 ||= ba || (a = b)로 변환돼요
  • a &&= ba && (a = b)로 변환돼요

a가 인덱스 접근자([])일 때는 또 다른 특별한 경우가 있어요. 왼쪽이 nil 가능 변형([]?)으로 바뀌어요:

  • a[i] ||= ba[i]? || (a[i] = b)로 변환돼요
  • a[i] &&= ba[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 불가 변형은 그 경우 예외를 던져요. 표준 라이브러리 구현은 보통 KeyErrorIndexError를 던져요.

Operator Description Example Overloadable
[] 인덱스 접근자 ary[i] yes
[]? nil 가능 인덱스 접근자 ary[i]? yes

출처: Crystal 공식 문서

더 알아보기 (Learn more)