Account: please sign in
AA

기호논리학의 논리식이든 프로그래밍 언어의 소스 코드든 간에, 정형적인 기호들의 나열을 다루는 모든 학문은 첫째도 둘째도 정확한 토크나이징과 정확한 파싱에서 출발합니다. 이에, 비록 함수형 프로그래밍에만 특화된 내용은 아니지만, 이후 학습의 편의를 도모하고 오해를 미연에 방지하는 차원에서 정확한 파싱의 일환인 '결합 특성fixity'의 일반적인 개념을 짚고 넘어갑니다. 또한, 이 절에서는 함수형 프로그래밍과 타입 시스템에 특화된 몇 가지 중요한 실제 결합 특성에 대해서도 예를 들어 소개합니다.

결합 특성이란 이항binary 이상의 중위 연산자infix operator에 대해 고려하며, 결합 강도precedence와 결합 방향associativity 두 요소로 이루어집니다. 이것은 결국 파서가 파싱할 때 사용하는 정보이므로 언어의 의미semantics와는 일단 무관하게 언어의 문법syntax을 좌우하는 요소입니다.

여담이지만, 만일 이항 이상의 모든 연산자들이 중위가 아닌 전위나 후위 형태만 일관되게 사용되는 문법 체계라면 결합 특성은 고려할 필요가 없습니다.

결합 강도

결합 강도precedence란 결합 우선순위라고도 하며, 서로 다른 두 중위 연산자들이 하나의 피연산자operand를 사이에 끼고 인접해 있을 때 두 연산자들 중 어느것이 해당 피연산자를 더 강하게 끌어가는가를 정해둔 것입니다.

한 문법 체계 속에 존재하는 모든 중위 이항 연산자들은 그 중 어떤 두 연산자를 취해서 서로간의 결합 강도를 비교하더라도 항상 같거나 어느 한 쪽이 더 강해야하며, transitivity와 anti-symmetricity를 만족해야합니다. 따라서 결합 강도는 동치/대소 비교가 가능한 값에 대응시켜 표현하며, 이런 값으로 대개 자연수나 정수를 이용합니다. (예: "연산자 ☆의 결합 강도는 8이고 ◎의 결합 강도는 6이다.")

예를 들어, 자연수 산술 언어의 올바른 문자열(표현식)인 "1 + 2 × 3"을 우리가 자연스럽게 "1 + (2×3)"으로 파싱하여 받아들이는 것은 '×' 연산자의 결합 강도가 '+' 연산자의 결합 강도보다 높다는 것을 이미 배워서 알고있기 때문입니다.

또한, 대부분의 프로그래밍 언어 교재들도 초반에 연산자 결합 강도 비교표를 제공합니다. 게다가 Haskell이나 Coq 같은 많은 프로그래밍 언어(혹은 논리체계 구현물)들은 아예 프로그래머가 자신이 정한 기호를 이용해 새로운 중위 연산자를 만들 수 있는 기능을 제공하며, 따라서 그런 중위 연산자들의 결합 강도를 선언해주는 기능도 당연히 함께 제공됩니다.

결합 방향

결합 방향associativity이란 서로 같은 두 중위 연산자들이 하나의 피연산자를 사이에 끼고 인접해 있을 때, 이것이 일단 문법적으로 올바른지와, 만일 올바르다면 두 연산자들 중 어느것이 해당 피연산자를 더 강하게 끌어가는가를 정해둔 것입니다.

이런 경우 사이에 낀 인자는 왼쪽으로 먼저 묶일 수도 있고left-associative 오른쪽으로 먼저 묶일 수도 있지만right-associative 아예 묶이지 못할―즉, 문법적으로 틀릴―수도 있습니다.non-associative 따라서 결합 방향은 대부분 '왼쪽, 오른쪽, 혹은 불가능' 셋 중 하나로 지정되며, 실제로 Haskell이나 Coq 등은 사용자가 정의한 새로운 중위 연산자마다 이런 세 가지 결합 방향 중 한 가지를 골라 선언해주는 기능을 제공합니다.

왼쪽부터 결합

왼쪽이 먼저 결합하는 대표적인 연산자로는 +, ×, -, ÷ 등의 사칙연산자들이 있습니다. 즉, 만일 괄호 없이

  • 3 + 5 + 7

라고 쓰여있으면, 우리는 이것을

  • (3 + 5) + 7

로 해석하기로 약속하고 있습니다. 만일 수식을 작성하는 사람이 애초에 의도했던 바가

  • 3 + (5 + 7)

이었다면, 괄호를 생략하지 말고 정확히 저렇게 써야만 합니다.

사실 상기 + 연산자는 그 연산 내용의 특성 때문에 좌측 우선 결합을 하든 우측 우선 결합을 하든 아무 상관이 없는 경우였습니다. 그래서, 사실은 + 연산자의 결합 방향을 신경 쓰는 사람은 (프로그래밍 언어를 설계하거나 파서, 컴파일러 등을 제작하는 사람을 제외하고는) 그다지 많지 않습니다.

하지만, 연산 내용의 특성에 따라서는 결합 방향이 연산 내용에 영향을 미치는 경우도 있습니다. 간단한 예로, 뺄셈 연산자는

  • (3 - 5) - 7 ≠ 3 - (5 - 7)

이기 때문에, 괄호 없이

  • 3 - 5 - 7

이라고만 썼을 때 이것을 어떻게 해석할지 미리 확실하게 정해서 약속해둬야합니다. 잠시 후에 이런 측면을 다시 언급할테니 지금 꼭 기억해두시기 바랍니다: 덧셈은 결합 방향이 어느 쪽이든 간에 연산 자체에도 논리적인 문제가 없고 결과도 달라지지 않았던 반면에, 뺄셈은 사실은 오른쪽이 먼저 결합한다고 정했더라도 연산 자체가 불가능해지는 논리적인 오류는 없지만 결과가 달라지기 때문에 한 쪽으로 (그냥 하필 왼쪽으로) 확실히 정해두었던 경우입니다. "연산 자체가 불가능해지는 논리적 오류"라는 게 무슨 뜻인지는 곧 보시게 됩니다.

오른쪽부터 결합

오른쪽이 먼저 결합하는 대표적인 연산자로는 제곱을 나타내는 ^ 연산자와 (C 언어 등의 일부 절차형 프로그래밍 언어에서) 대입을 나타내는 = 연산자 등이 있습니다. 제곱 연산자가

  • 3 ^ 2 ^ 5

와 같이 괄호 없이 연이어서 쓰여있으면, 이것은

  • (3 ^ 2) ^ 5 = 9 ^ 5 = 59049

라는 뜻이 아니라

  • 3 ^ (2 ^ 5) = 3 ^ 32 = 1853020188851841

라는 뜻입니다. 여기서도 다시 한 번 강조하겠습니다. 제곱 연산자 또한 왼쪽이 먼저 묶인다고 정했더라도 연산 자체가 불가능해지는 논리적인 오류까지는 없었습니다. 그저, 왼쪽이냐 오른쪽이냐에 따라서 연산 결과가 달라지므로 확실히 한 쪽으로 정해두었을 뿐이고, 그게 하필 오른쪽이었을 뿐입니다. ("하필"이라고 하기에는, 실은 잘 뜯어보면 그럴만한 이유가 있기는 합니다. 오른쪽으로 정해두는 게 평소에 괄호를 좀 더 적게 쓸 가능성이 높습니다.)

이것은 수학에서 윗첨자로 올려쓰는 경우에도 마찬가지입니다.

  • \[3^{2^5}\]

라고 쓰여있으면 이것은 $(3^2)^5$라는 뜻이 아니라 $3^{(2^5)}$라는 뜻입니다.

C 언어에서 대입문 자체는 대입된 값으로 평가되는 표현식이기도 하며, 따라서 C 언어에서 기호 '='는 대입문을 만드는 문법 요소이자 동시에 표현식을 만드는 이항 중위 연산자이기도 합니다. 이항 중위 연산자로서, = 연산자 또한 오른쪽부터 먼저 묶입니다. 예를 들어,

 x = y = 3;

이라고 쓰여있으면 이것은

 x = (y = 3);

과 같습니다. 즉, "먼저 대입문으로서 y에 3을 대입한 후, 그 대입 표현식을 평가한 결과값인 3을 이번에는 x에 대입하라"는 뜻입니다.

앞에서 두 번이나 언급했던 "연산 자체가 불가능한 논리적인 오류"의 예를 지금 소개해보겠습니다. 만일, 대입 연산자가 왼쪽으로 먼저 묶인다고 정하면 어떻게 될까요? 연산 결과야 달라질지 모르지만 그건 넘어간다고 치고, 일단 그런 연산 자체가 논리적으로 가능하긴 할까요? 설명의 편의를 위해 한 문장이 더 있었다고 칩시다.

 y = 7;
 (x = y) = 3;

여기서 두번째 문장을 괄호 순서대로 해석해보면 이렇게 됩니다:

  • "먼저 변수 y에 들어있는 값(즉, 7)을 변수 x에 대입하고, 그 대입문의 결과값인 7 3을 대입하라"

즉, 변수에 값을 대입하는 게 아니라 값에 값을 대입하는 형국이 되어버립니다. 이것은 "대입문 자체는 대입된 값으로 평가된다"라는 약속과 논리적인 충돌을 일으켰기 때문입니다. 이런 종류의 논리적인 충돌을 흔히 타입 오류type error라고 합니다.

즉, 대입 연산자의 경우는 결합 방향을 어느쪽으로 약속하든 아무 상관 없었거나 어느쪽도 가능하지만 의미가 달라서 한쪽으로 정해둔 것이 아니라, 애초에 오른쪽이 먼저 결합할 수 밖에 없었던 것입니다.

대입 연산자처럼 한 쪽으로 밖에 결합할 수 없는 연산자들의 공통적인 특징을 설명할 방법이 있을까요? 다시 말해서, 연산자의 어떤 부분을 관찰하면 "이 연산자는 ○○쪽으로 밖에 결합할 수 없구나" 라고 결론내릴 수 있을까요? 바로 타입으로 이런 특성을 설명할 수 있습니다.

전술했던 덧셈이나 뺄셈, 제곱 연산자의 타입은 다음과 같았고:

  • + : R × RR
    - : R × RR
    ^ : R × RR

대입 연산자의 타입은 (Var가 변수들의 집합이고 Exp가 표현식들의 집합이라고 할 때):

  • = : Var × Exp → Exp

이었습니다. 즉, 만일 이항 연산자(즉, 이항 함수)의 두 정의역과 공변역이 모두 다 같으면 이런 연산자는 (내용 상 결과가 달라질 수는 있어도, 최소한 논리적으로는) 어느 쪽으로든 결합할 수 있는 연산자인 반면에, 만일 두 정의역이 서로 다르고 그 중 하나만 공변역과 같으면 해당 연산자는 그쪽 (공변역과 같은 정의역 쪽)이 반드시 먼저 묶여야만 논리적으로 말이 되게 됩니다.

결합 불가

앞 소절의 마지막 설명을 곰곰히 생각해보면 이런 질문을 해볼 수 있습니다:

  • "그럼 공변역이 두 정의역 중 어느 쪽과도 같지 않으면 어떻게 될까?"

만일 어떤 중위 연산자 ☆와 □의 타입이 각각 다음과 같다면:

  • ☆ : A × A → C
    □ : A × B → C

이런 중위 연산자들은 어느 쪽으로도 결합할 수 없습니다.

가장 흔히 볼 수 있는 예(이자 흔히 잘못 알려진 예)가 부등호 "≤" 입니다. 실수에 한정해서 말하자면, 부등호의 타입은

  • ≤ : R × R → Bool

입니다. (Bool은 진리값들의 집합, 즉 { True, False } 입니다.) 이 부등호가 다음과 같이 연이어 쓰여있으면:

  • 3 ≤ 5 ≤ 4

이 문장은 만일 부등호가 왼쪽부터 결합한다고 치면

  • (3 ≤ 5) ≤ 4

이고, 3 ≤ 5의 연산 결과는 True이므로 결국

  • True ≤ 4

가 됩니다. 즉, 진리값과 실수의 대소 관계를 비교하겠다는, 논리적으로 전혀 불가능한 이야기가 되어버립니다. 부등혹가 오른쪽부터 결합한다고 쳐도

  • 3 ≤ 5 ≤ 4
    3 ≤ (5 ≤ 4)
    3 ≤ False

과정을 거쳐 역시 논리적으로 틀린 문장이 됩니다.

이 쯤에서 갸우뚱하는 분도 계실 것 같습니다. 실제로 "3 ≤ 5 ≤ 4"와 같은 문장은 여기저기에서 많이 쓰이고 있으며, 흔히 "3은 5보다 작거나 같고, 5는 7보다 작거나 같다"라고 아무 문제 없이 읽곤 하기 때문입니다.

일단 결론부터 말씀드리면, 널리 사용되는 저 표기는 암묵적인 축약형일 뿐이고 엄밀히 말하자면 틀린 표기힙니다. 심지어 많은 수학 관련 문서에서도 저런 표기를 쓰곤 하는데, 저런 의미로 쓰려고 했다면 사실은

  • 3 ≤ 5 ∧ 5 ≤ 4

라고 확실하게 분리해서 썼어야 맞습니다. 단지, 수학 문서들은 기계(컴퓨터)에게 보여주기 위해 만드는 문서들이 아니라 명백히 사람에게 보여주기 위해 만드는 문서이므로, 사람의 "직관"을 믿고 암묵적으로 축약형을 묵인해왔던 것 뿐입니다. (혹은, 문서 초반에 아예 "이제부터 a ≤ b ≤ c는 항상 a ≤ b ∧ b ≤ c로 해석하기로 한다"라고 명시적으로 약속해두는 경우도 간혹 있습니다.)

더 엄밀하게 말하자면

  • (a ≤ b) ∧ (b ≤ c)

라고 괄호를 써야하지만, ≤가 ∧보다 결합 강도가 강하다고 정의되어 있다면 (실제로 대부분 이렇게 정의되어 있습니다) 괄호를 생각할 수 있습니다.

"기계도 읽을 수 있다"고 생각할지도 모르겠습니다. 실제로 C 언어에서 저런 문장은 오류을 일으키지 않는데, 단지 오류만 일어나지 않을 뿐이고 실제로 값 계산 결과는 의도했던 것과는 달라질 수도 있습니다. C 언어(적어도 ISO C89 표준)에는 내부적으로 진리값을 위한 타입이 따로 없고, 대신 0은 거짓, 그 외의 수는 참으로 인식하고 있으며, 참을 숫자로 바꿀 때에는 1로 바꾸곤 합니다. 따라서

  • 3 ≤ 5 ≤ 4
    (3 ≤ 5) ≤ 4
    1 ≤ 4
    1

이라는 과정을 거쳐서 1로 (즉, C 언어에 한정해서 "참"으로) 평가되는 것입니다. (그리고, 이것은 원래 의도했던 것과는 분명히 다른 결과입니다.)

C 언어보다는 확실히 나은 경우로 Python 같은 언어도 있습니다. Python은 아예 저렇게 연이어 나오는 부등호들에 대해서

  • _a ≤ _b ≤ _c

라는 3항 연산자 문법syntax을 미리 만들어두고 이것의 의미semantics"(_a ≤ _b) ∧ (_b ≤ _c)"와 같도록 미리 대응시켜 두었습니다. 따라서 타입 오류가 발생하지 않을 뿐만 아니라 내용 상으로도 흔히 생각하는 바에 맞게 동작합니다.

참고

중위 연산자와는 달리, 전위prefix 연산자나 후위postfix 연산자들은 괄호가 없더라도 쓰여있는 위치만으로도 무엇이 해당 연산자의 피연산자들이고 무엇을 먼저 계산해야하는지 알 수 있습니다. 예를 들어, ☆이 이항 연산자일 때, 다음과 같은 중위 표현식

  • 3 ☆ ((2 ☆ 5) ☆ 7)

에서 괄호를 생략하고 싶으면 ☆의 결합 방향을 미리 정해둬야할 뿐만 아니라, 심지어 어떻게 정하더라도 위 표현식에서 줄일 수 있는 괄호는 결국 한 쌍 뿐입니다. (왼쪽 우선 결합이라고 정하면 안쪽 괄호만 없앨 수 있고, 오른쪽 우선 결합이라고 정하면 바깥쪽 괄호만 없앨 수 있습니다.)

반면에, 상기 표현식을 전위 표기법으로 바꾸면 다음과 같이:

  • ☆ 3 ☆ ☆ 2 5 7

일체의 괄호 없이 원하는 바를 나타낼 수 있습니다.

추가 고찰: 문법 오류 vs. 타입 오류

이것이 정확하게 '문법' 오류인지 '타입' 오류인지를 논하는 것은 이 문서의 범위를 벗어나므로 따로 분리해서 기술하겠습니다:

  • 만일 어떤 언어의 문법 체계가 연산자 결합 방향에 대해 'non-associative'라는 개념을 제공하지 않고 단순히 '왼쪽 혹은 오른쪽'이라는 개념만 제공한다면, 이 언어의 구문 분석기는 저것을 문법 오류로서 검출해낼 방법이 없습니다.
    • 다만, 만일 이 언어가 어느정도 견고한 타입 시스템을 제공하고 '≤' 연산자의 공변역이 정수가 아닌 명제나 진리값이라면, 의미 검사 (타입 검사) 과정에서 타입 오류로서 검출될 여지는 있습니다.
    • 만일 이 언어가 어느정도 견고한 타입 시스템을 제공하긴 하지만 '≤' 연산자의 공변역이 정수라면 (예: C 언어), 저 식은 결국 "0 ≤ 8", "1 ≤ 8", "3 ≤ 0", "3 ≤ 1" 중 하나를 거쳐 최종적으로 0이나 1이 나올겁니다. 이것이 프로그래머가 애초에 의도했던 바라면 할 말 없지만, 아마 많은 경우에 프로그래머의 원래 의도는 이게 아니었을겁니다.
    • 만일 이 언어가 타입 시스템을 제공하지 않거나 너무 약하다면, 저 식은 이를테면 "true ≤ 8"로 평가된 뒤 뭔가 모종의 동작을 할겁니다. 프로그래머가 그 모종의 동작까지 미리 제대로 예상하고 의도했던 것이라면 역시 할 말 없습니다만.... :P
  • 만일 어떤 언어의 문법 체계가 연산자 결합 방향에 대해 'non-associative'라는 개념을 제공한다면, 저런 논리식은 명백히 문법 오류가 되며 구분 분석기가 즉시 잡아냅니다.

추가 고찰: 중첩식 연산들의 결합 방향

이른바 '중첩식' 연산들에 대해서는 살짝 다른 맥락에서 고민해볼 여지가 또 있습니다: 예를 들어, 데카르트 곱(Cartesian product)을 만드는 이항 중위 연산자 '×'의 결합 방향은 어떨까요? 일단 개념적으로, 두 집합의 데카르트 곱 결과는 다시 집합이기 때문에, 이 결과는 다른 데카르트 곱 연산의 좌항으로도 우항으로도 쓰일 수 있습니다. 하지만, 데카르트 곱 연산은 연산 결과가 중첩된다는 특성이 있습니다.

자연수 집합 N과 알파벳 소문자들의 집합 A가 있을 때, N×A 집합은 다음과 같은 순서쌍들의 집합입니다:

  • N×A = {(0,a), (0,b), ..., (0,z), (1,a), (1,b), ...}

이제, 이 집합에 새로운 별칭을 줍니다. T라고 하죠. 즉,

  • T = {(0,a), (0,b), ..., (0,z), (1,a), (1,b), ...}

이 상황에서, T×A 라는 집합이 무엇인지를 물으면 답은 명백합니다:

  • T×A = {((0,a),a), ((0,a),b), ..., ((0,a),z), ((0,b),a), ((0,b),b), ..., ((0,z),z), ((1,a),a), ...}

그리고, 우리는 T = N×A 임을 알고있습니다. 따라서:

  • (N×A)×A = {((0,a),a), ((0,a),b), ..., ((0,a),z), ((0,b),a), ((0,b),b), ..., ((0,z),z), ((1,a),a), ...}

자, 그렇다면 N×A×A 집합은 무엇인가요? 평소에, 우리는 흔히 관례적으로 이 집합을 다음과 같이 생각해왔습니다:

  • N×A×A = {(0,a,a), (0,a,b), ..., (0,a,z), (0,b,a), (0,b,b), ..., (0,z,z), (1,a,a), ...}

그럼 우리는 결국 (N×A)×A ≠ N×A×A 라고 결론지어야할까요? 물론, 이 문제는 '×' 연산자의 결합 방향을 오른쪽인 것처럼 설정을 바꿔서 같은 장난을 쳐봐도 비슷한 결론에 도달합니다: A×(N×A) ≠ A×N×A

이 문제도 (최소한 이 문서에서는) '≤' 연산자의 경우와 같은 방식으로 해석하기로 합니다: 세 집합으로부터 3-tuple들의 집합을 만들어내는 '×3'라는 함수를 만들고

  • ×3(N, A, A) = {(0,a,a), (0,a,b), ..., (0,a,z), (0,b,a), (0,b,b), ..., (0,z,z), (1,a,a), ...}

(혹은 juxtapositioning으로)

  • ×3 N A A = {(0,a,a), (0,a,b), ..., (0,a,z), (0,b,a), (0,b,b), ..., (0,z,z), (1,a,a), ...}

다음과 같은 3항 연산자를 따로 만들어서:

  • _a × _b × _c

이 3항 연산자를 함수 적용 ×3 _a _b _c에 대한 syntactic sugar로 간주하는 것입니다.

물론, 이 방식에는 약간의 문제가 있습니다: 임의의 n항 튜플을 만드는 구문

  • _1 × _2 × _3 × ... × _n

을 모든 n에 대해 편안하게 사용할 수 없고, 필요한 n마다 그에 대응하는 함수를 일일이 정의하고 syntactic sugar로서 설정을 해줘야 한다는 문제입니다. (실제로 이 문제는 Haskell 진영의 오랜 난제 중 하나입니다.) 하지만, 제가 가장 익숙하고 이 문서에서도 간간이 예로 들 Haskell 언어가 현재 이 방식을 취하고 있기에, 저도 이 방식을 채택하겠습니다.

이 문제를 해결하기 위한 또다른 의견 중에는 "모든 n-튜플이란 결국 순서쌍(2-tuple)들의 (한쪽으로 일정하게 쏠린) 중첩일 뿐이다"라고 정해버리자는 의견도 있습니다. 즉, (0,a,a)은 원래부터 ((0,a),a)이고 심지어 (1,2,3,4)는 (((1,2),3),4) 라는 식입니다. 하지만 이 방법은 현실적으로 큰 문제를 야기합니다. 이에 관한 설명은 문답 식으로 진행하겠습니다:

  • 문: (0,a,a)의 가장 왼쪽 원소는 자연수 0인 반면, ((0,a),a)의 가장 왼쪽 원소는 (0,a)라는 순서쌍인데요?
  • 답: (0,a,a)이든 ((0,a),a)이든 공히, "리스트(n-tuple)로서의 '첫번째' 원소"는 0인 반면 "순서쌍(2-tuple)로서의 '왼쪽' 원소"는 (0,a)라고 통일시키면 됩니다. 즉, 리스트로서의 n번째 원소 참조 연산과 순서쌍으로서의 왼쪽/오른쪽 원소 참조 연산을 명백히 분리하는겁니다.
  • 문: 음... 저는 ((a,b), 2, 3, 4)라는 4-tuple을 만들고 싶습니다. 즉, "리스트로서의 첫번째 원소"가 명백하게 (a,b)라는 순서쌍인 4-tuple이죠. 당신의 방식으로 만들어보셔요.
  • 답: 네. 그것은 결국 ((((a,b), 2), 3), 4)입니다.
  • 문: 그 ((((a,b), 2), 3), 4)의 "리스트로서의 첫번째 원소"는 a인데요? 즉, (a, b, 2, 3, 4)라는 5-tuple 이랑 어떻게 구별하죠?
  • 답: ...

물론, 제가 생각해내지 못하고있을 뿐 이에 대한 또다른 해법이 이어질지도 모릅니다만, 저는 이 방식은 채택하지 않겠습니다. :^P

3항 이상의 중위 연산자

중위 연산자가 3항 이상인 경우에도 결합 특성은 고려해야합니다.

예를 들어, C 언어의 대표적인 3항 연산자인 "_c ? _t : _e"의 경우에도, 만일 이것이 인접해 있을 경우 어떻게 해석해야하는지가 문제입니다:

  • (a ? b : c) ? d : e   ⇦   a ? b : c ? d : e   ⇨   a ? b : (c ? d : e)

다만, 연산자마다 하나 하나 짚어보는건 이 문서의 범위를 넘어서므로, 추후 함수형 언어를 선택하고 공부하다가 3항 연산자가 나오면 그때 "아, 이것도 결합 특성을 생각해야지"라고 떠올릴 수 있기만 하면 됩니다.

꼭 기억해둬야할 주요 결합 특성들

앞으로 이 문서를 계속 읽는 동안이나 추후 실제로 함수형 언어를 다루게 될 때 꼭 알아둬야하는 중요한 결합 특성들이 몇 가지 있습니다:

  • Juxtapositioning에 쓰이는 '빈 칸'도 엄밀히 따지면 일종의 중위연산자로 간주할 수 있으며, 이렇게 간주할 경우:
    • 결합 방향은 좌측우선결합입니다. 예를 들어 f x y(f x) y라고 파싱해야합니다. 왜 이렇게 설정되어있는지는 이 문서에서 나중에 고계 함수(higher-order functions)를 파악하고 나면 이해가 되실겁니다.
      참고로, 만일 명시적으로 괄호를 사용해서 f (g x) 방식으로 묶어주면, 이는 (f ∘ g) x와 같은 의미입니다. 함수 g를 인자 x에 적용한 결과에 다시 함수 f를 적용한다는 것은, 결국 f와 g의 합성함수 f∘g를 통째로 인자 x에 적용하는 것과 같기 때문입니다.

    • 결합 강도는 거의 대부분의 일반적인 다른 중위 연산자들보다 항상 강합니다. 즉, f x + y af (x+y) a가 아니라 (f x) + (y a)로 파싱해야합니다.

  • 논리적 함축implication을 뜻하는 논리연산자(이자, 함수 타입을 나타내는 타입 연산자)인 '→'는:

    • 결합 방향은 우측우선결합입니다. 예를 들어, A → B → C는 A → (B → C)라고 파싱해야합니다. 왜 이렇게 설정되어있는지는, 역시 이 문서에서 나중에 고계 함수(higher-order functions)를 파악하고 나면 이해가 되실겁니다.

    • 결합 강도는 일반적인 다른 논리 연산자들이나 타입 연산자들보다 대체로 약합니다. 즉, A × B → C는 (A × B) → C로 파싱해야합니다.