표현식이 무엇인지 알아보자

오늘은 표현식에 대해서 좀 이야기해볼까 한다. 표현식(Expressioin)은 너무 직역한 단어라 좀 이상한 감이 없잖아 있는데, 딱히 대체할만한 말이 떠오르지 않는다. 영어권에서는 표현식 대신에 평가식이라는 말도 사용한다. 필수적이거나 중요한 개념은 아닌데, 프로그래밍 언어 가이드를 보다보면 자주 등장하는 용어이고, 알고 있다면 학습에 도움이 될 수는 있겠다 싶다.

표현식이 무엇인지 알아보자 더보기

이니셜라이저 – Swift

Swift의 클래스와 구조체, enum 객체들은 사용하기 전에 반드시 초기화되어야 한다. 그러면 초기화(initialization)이란 무엇인가? 객체의 생성 자체를 초기화과정에 포함시키는 관점과 그렇지 않은 관점이 있지만, 여기서는 “객체를 만들어서 사용가능한 상태로 준비하는 일”이라고 보자. let foo = Foo() 와 같이 특정한 타입의 인스턴스를 생성하는 구문을 실행했을 때 저 아래(?)에서 벌어지는 과정은 다음과 같다.

이니셜라이저 – Swift 더보기

(SQLite3) JOIN을 통한 테이블 연결

지난 글에서 SQLite3에서 SELECT 명령의 사용방법에 대해서 살펴보았는데, FROM을 통해서 단일 테이블 혹은 단일 테이블 내의 범위를 부분적으로 얻어내는 서브 쿼리를 통해서 보다 정교한 범위의 데이터를 얻고, 또 WHERE절을 사용해서 결과를 필터링 하는 방법에 대해서도 살펴보았다. 그외에 GROUP BY나 그외 aggregation 연산을 통한 쿼리 방법에 대해서는 자세하게 다루지 않았는데, 그 전에 JOIN에 대해서 간단하게 짚고 넘어가고자 한다.

(SQLite3) JOIN을 통한 테이블 연결 더보기

파이썬은 처음이라 – 값이란 건 처음이라

이번 글에서는 파이썬에서의 값에 대해서 살펴보겠다. 프로그래밍 언어를 배울 때에는 흔히 문법이나 알고리듬, 자료 구조와 같은 부분에 집중하면서 “값”이라는 부분, 즉 데이터 자체에 대해서는 소홀해지는 경향이 있다. 하지만 컴퓨터 프로그램은 본질적으로 데이터를 다루는 논리적인 기계장치이며, 어떤 입력값을 조작/변형처리하여 출력하는 구조를 가진다. 그 과정에서 다루어지는 값에 대한 특성을 이해하는 것은 값외의 다른 모든 것들을 이해하는 것과 같은 비중으로 중요하다.

컴퓨터에서 모든 값은 0과 1로 구성되는 이진수만이 통용된다는 것이 널리 알려져 있다. 그렇다면 어떻게 정수나 실수1, 텍스트와 같은 값들을 다룰 수 있는 것일까. 이러한 궁금증에서부터 시작하여 프로그래밍 언어에서의 값의 종류(타입)와 더불어 파이썬에서 사용되는 원시 값의 종류에 대해서 살펴보도록 하겠다.

값과 타입

컴퓨터는 전기적 신호를 통해 작동하는 기계이며, 전기신호의 ON/OFF를 구분하며, 이 ON/OFF를 조합하여 서로 다른, 즉 구분이 가능한 신호의 조합을 만들 수 있다. 이 때 ON을 1에, OFF를 0에 대응하면 일련의 ON/OFF 신호의 조합은 10110101011… 과 같은 표현으로 해석할 수 있다. 두 개의 숫자로 이루어지는 이 숫자체계를 수학에서는 이진수라고 하며, 약간의 산수를 통해서 이진수는 우리가 흔히 이해하는 10진수로 바꿔서 표시할 수 있다.

0과 1로 만들어지는 이진수는 결국 어떤 수(number)일 뿐인데, 우리가 개념적으로 사용하는 여러 가지 정보는 정확하게는 수 개념에만 한정되지 않는다. 실제로 우리는 수를 표현하기 위해서 숫자체계라는 것을 이용해서 숫자기호라는 문자를 사용한다. 사과에 오렌지를 더할 수 없듯이 “1”이라는 숫자2에 하나라는 의미의 수 1을 더할 수는 없다. 이렇 듯 우리의 관념에서도 어떤 정보는 그 타입이 구분되며, 어떤 타입들은 서로 호환되지만3 어떤 타입들은 서로 호환되지 않을 수도 있다.

정수나 float 값은 실수(Real Number[^3])라는 수 체계에서 볼 때는 같은 값이지만, 컴퓨터에서는 이진수를 사용해서 이를 표현하고 관리하기 위해서 서로 다른 기술적인 체계를 따르게 된다. 따라서 흔히 숫자라고하는 타입은 정수와 실수로 나뉘며, 보통의 프로그래밍 언어에서는 이들을 구분한다. 그외에 프로그래밍 언어에서 사용되는 가장 기초적인 데이터 타입에는 다음과 같은 것들이 있다.

  • 정수 : 0, 1, -1 과 같이 소수점 이하 자리가 없는 수. 수학에서의 정수 개념과 동일하다. 파이썬에서는 int 타입이라고 한다.
  • 실수 : 0.1, 0.5 와 같이 소수점 아래로 숫자가 있는 수. 파이썬에서는 float 이라고 한다.
  • 문자, 문자열 : 숫자 “1”, “a”, “A” 와 같이 하나의 낱자를 문자라 하며, 이러한 문자들이 1개 이상있는 단어/문장와 같은 텍스트를 문자열이라고 한다. 파이썬에서는 str 타입으로 분류한다. 특히 파이썬은 낱자와 문자열 사이에 구분이 없이 모두 str 타입을 적용한다.
  • 불리언값 : 참/거짓을 뜻하는 대수값. 보통 컴퓨터는 0을 거짓, 0이 아닌 것을 참으로 구분한다. 파이썬에는 bool 타입이라 부르며, 이 타입에는 TrueFalse 의 두 멤버만 존재한다. (참고로 각각은 대문자를 써야 한다.)
  • None : 존재하지 않음을 표현하기 위해서 “아무것도 아닌 것”을 나타내는 값이다.

이러한 기본적인 데이터 타입을 조합하여, 여러 개의 값을 하나의 단위로 묶어서 다루는 데이터 타입이 있다. 논리적으로 이들은 데이터 타입인 동시에 데이터의 구조(흔히 말하는 자료 구조)의 한 종류이기도 하다. 보통 다른 데이터들을 원소로하는 집합처럼 생각되는 타입들이다.

  • 리스트 : 순서가 있는 원소들의 집합. 파이썬에서 가장 중요한 데이터 타입 중 하나이다.
  • 튜플 : 순서가 있는 원소들의 묶음. 리스트와 혼동하기 쉬운데 단순히 하나 이상의 값을 묶어서 하나로 취급하는 용도로 사용된다.
  • 사전 : 그룹내의 고유한 이름인 키와 그 키에 대응하는 값으로 이루어지는 키값 쌍(key-value pair)들의 집합이다.
  • 집합/셋(set) : 순서가 없는 고유한 원소들의 집합.

자료 구조에 해당하는 데이터 타입은 매우 중요한데, 이 글에서 한 꼭지로 다루기에는 너무 중요하기 때문에 각각 별도의 글에서 다룰 예정이다.

숫자값들

수를 나타내는 타입으로 정수와 실수가 있다고 했다. 각각은 int 타입과 float 타입이다. 더하기, 빼기등의 산술 연산을 적용하는 값이며 가장 기본적인 수 개념들을 표현한다.

리터럴

어떤 값을 “써서 표현하는 방법”을 리터럴이라고 한다. 수를 표현하는 기본 리터럴은 다음과 같다.

  • 정수의 경우, 그냥 숫자들로 숫자값을 쓰면 된다. 0, 100, 123 과 같은 표현을 쓴다.
  • 실수의 경우, 중간에 소수점이 들어간다. 0.1,  4.2, 3.13123 와 같은 식으로 쓴다.
  • 0. 으로 시작하는 실수값에서는 흔히 앞에 시작하는 0을 뺄 수 있다. .5 는 0.5를 줄여쓴 표현이다.
  • 부호를 나타내는 – , +를 앞에 붙일 수 있다. (-1, +2.3 등)

보통 우리가 숫자를 쓰는 체계는 10진법이다. 파이썬에서는 드물게 다른 진법으로 숫자를 표현하기도 한다.

  • 기본적으로 그냥 숫자만 사용하는 경우, 이는 십진법 값으로 해석된다.
  • 파이썬이 인식할 수 있는 숫자 리터럴 체계에는 10진법외에도 이진법, 8진법, 16진법이 존재한다.
  • 이진법 숫자는 0b로 시작한다. 0b1010110 은 십진법 86을 숫자로 쓴 것이다.
  • 8진법 숫자는 0o로 시작한다. 위의 86은 8진법으로 썼을 때 0b126이다.
  • 16진법숫자는 0x로 시작한다. 16진법은 숫자외에도 a~f 까지의 문자를 포함하며, 이 글자들은 대소문자를 구분하지 않는다. 86은 16진법으로 표현시 0x56이다.

한편 큰 숫자를 다룰 때에는 _를 쓸 수 있다. 우리가 일상생활에서 큰 자리 숫자를 표현할 때 세자리마다 컴마를 찍는 것과 비슷하게 숫자 리터럴 중간에 _ 를 쓰는 것은 무시된다. 백만을 쓸 때 1_000_000 이라고 표기하면 정상적으로 1백만으로 인식되는데 1000000으로 쓰는 것보다 읽기 쉬을 수 있다. 언더스코어는 세자리마다 써야 하는 것은 아니고 원하는 아무자리에나 쓰면 된다.

숫자는 아니지만 참/ 거짓을 의미하는 부울대수값이 있다. 이들은 그 자체가 키워드로 True/False를 사용하여 표현한다.

True, False 가 키워드가 된 것은 파이썬3의 일이다. 파이썬2에서는 상수처럼 쓰이는 그냥 이름이었다. 따라서 True = False 와 같은 괴상한 구문이 아무런 문제없는 파이썬 코드였다.

연산

값을 가지고는 여러 연산을 할 수 있다. 다음은 기본적으로 지원되는 연산이다.

  • 더하기, 빼기, 곱하기, 나누기 :  각각 +, -, *, / 문자를 사용한다. 수식을 표현하는 방식은 계산기를 사용하는 방식과 똑같다.
  • 몫과 나누기 : // , % 를 사용한다.  (7 // 5, 13 % 8)
  • 거듭제곱 : ** 를 사용한다 ( 3 ** 2, 4 ** 3)
  • 비트연산 : & (and), | (or), ^ (xor), ~ (not) 이 있고, 시프트에는 << , >> 를 사용한다.
  • 비교연산 : 동등 및 대소를 비교할 수 있다.  참고로 ‘대소’비교는 ‘전후’비교가 사실은 정확한 표현이다. 비교 연산은 숫자값 뿐만 아니라 문자열에 대해서도 적용할 수 있다.
    • == , != : 같다, 같지 않다.
    • <, <=, >, >= : 작다, 작거나 같다, 크다, 크거나 같다. (좌변 기준으로)
  • 멤버십연산 : 멤버십 연산은 특정한 집합에 어떤 멤버가 속해있는지를 판단하는 것으로 비교연산에 기반을 둔다.
    • is, is not : 값의 크기가 아닌 값 자체의 정체성(identity)이 완전히 동일한지를 검사한다.
    • in, not in : 멤버십 연산. 어떠한 집합 내에 원소가 포함되는지를 검사한다. ('a' in 'apple')
  • 논리연산 : 비교 연산의 결과는 보통 참/거짓이다. 이러한 불리언값은 다음의 연산을 적용받는다. 참고로 불리언외의 타입의 값도 논리연산을 적용받을 수 있다. 논리연산을 위한 평가는 뒤에서 설명하겠다.
    • and :  두 값이 모두 참일 때 참
    • or : 한 값이 참이면 참
    • not : 단항 연산으로 우변의 값을 반전한다.

표현식

어떤 값 혹은 값들과 연산자를 함께 사용해서 수식을 표현한 것을 표현식(expression) 이라고 한다. 표현식을 평가식이라고도 하는데, 그 자체로 평가되어 하나의 결과값으로 축약된다. 따라서 1 + 1 과 같은 수식도 표현식이며, 0 과 같이 값 리터럴로 값을 표현해놓은 것 그 자체도 표현식이 될 수 있다. 이어서 소개할 문자열 역시 그 자체가 값이므로 표현식이며, 문자열과 관련되는 연산자들도 있다. 표현식은 궁극적으로 “평가”되며, 평가된다는 것은 표현식은 결국 하나의 값으로 수렴한다는 의미이다. 표현식은 간단한 수식으로 취급되고 있으나, 언어의 구조의 근간을 이루는 매우 중요한 개념이다.

앞에서 bool 타입이 아닌 숫자나 문자등의 다른 값들이 참/거짓으로 평가될 때 다음과 같이 판단된다.

  • bool : True는 참, False는 거짓이다.
  • int / float : 0이되면 거짓, 그외의 값은 참으로 평가한다.
  • str : 길이가 0인 빈 문자열은 거짓, 그외에는 참으로 평가한다.
  • 리스트, 사전 : 빈 컨테이너는 거짓, 그외에는 참으로 평가한다.
  • 임의의 객체 : None이면 거짓이며, None이 아닌 경우 값으로 평가하여 위의 규칙을 따르게 한다.

문자열

문자열은 글자 혹은 글자가 모여서 만드는 단어나 문장을 말한다. 크게는 이런 단어, 문장이 모여서 여러 줄의 단락이나 글 전체가 하나의 문자열이기도 하다.

리터럴

문자열 리터럴의 방식은 기본적으로 따옴표를 사용하는 것이다. 파이썬에서는 큰 따옴표와 작은 따옴표를 구분하지 않고 모두 문자열 리터럴에 쓴다. 하지만 괄호처럼 양쪽 따옴표가 맞아야 한다. (문자열을 둘러싸는 따옴표와 다른 따옴표는 문자열 내의 일반 글자로 해석된다. )

  • "apple" , 'apple' 은 모두 문자열 리터럴로 apple이라는 단어를 표현한 것이다.
  • 두 개의 문자열 리터럴이 공백이나 줄바꿈으로 분리되어 있는 경우에 이것은 하나의 문자열 리터럴로 해석한다. "apple,"   "banan""apple,banana"라고 쓴 표현과 동일하다.

따옴표를 세 개 연이어 쓰는 방법도 문자열 리터럴의 한 방법이다. 따옴표 세 개를 연이어서 쓰는 경우에는 문자열 내에서 줄바꿈이 그대로 허용된다. 흔히 함수나 모듈의 간단한 문서화 텍스트를 표현할 때 많이 쓰인다.

"""He said "I didn't go to 'SCHOOL' yesterday"."""  
=> He said "I didn't go to 'SCHOOL' yesterday". 를 그대로 표현할 수 있다.

=> 여러 줄에 대한 내용을 쓸 때.
'''HOMEWORK:
1. print "hello, world"
2. print even number between 2 and 12
3. calculate sum of prime numbers up to 100,000
''' 

그외에 이스케이프를 허용하지 않는 raw string 리터럴, 다른 값을 삽입하는 format string 리터럴, 바이트배열을 정의하는 bite string 리터럴등이 있는데, 조금 특수한 케이스이므로 여기서는 다루지 않겠다. (각각 r'...', f'...', b'...'와 같은 식으로 쓴다는 것 정도만 알고 넘어가자.)

연산

문자열도 엄연한 값이며, 가능한 연산이 있다.

  • 문자열 + 문자열 : 두 문자열을 연결할 수 있다.
  • 문자열 in 문자열 : 문자열 내의 특정 글자가 있는지 검사한다. ('a' in 'apple', 'c' not in 'banana')
  • 문자열 * 정수 : 문자열를 정수값만큼 반복하여 문자열을 만든다 ('abc' * 3 --> 'abcabcabc')

문자열에 실수를 곱하거나 문자열에 정수를 더하는 연산은 우리가 그냥 생각해도 어째야 할지를 모르겠고, 실제로도 정의되지 않았다. 이와 같은 연산들은 모두 ValueError 에러를 내게 되니 참고하자.

내삽 (interpolation)

내삽은 문자열에 대한 특별한 연산이다. 예를 들어 "Tom has 3 bananas and 4 apples." 라는 문자열이 있다고 할 때, 이것을 리터럴로 정의하는 것은 그 내용이 소스코드에 고정되는, 이른 바 하드 코딩(hard coding)이다. 문자열은 한 번 생성된 이후로 변경되지 않는 불변의 고정값이므로 Tom이 가지고 있는 사과나 바나나의 개수가 바뀌었을 때, 그 내용을 적절히 변경해 줄 수가 없다. 내삽(interpolation)은 문자열내에 동적으로 변할 수 있는 값을 삽입하여 상황에 따라 다른 문자열을 만드는 방법이다. 문자열 내삽의 기본원리는 문자열 내에 다른 값으로 바뀔 치환자를 준비해두고, 필요한 시점에 치환자를 실제 값의 내용으로 바꿔 문자열을 생성하는 것이다. 내삽의 방법에는 다음의 세 가지 방법이 있다.

  1. 전통적인 포맷 치환자를 사용하는 방법 : 문자열 % (값, ...)의 형식을 이용해서 문자열 내로 변수값을 밀어넣는 방법
  2. 문자열의 .format() 메소드를 사용하는 방법 : 치환자의 구분없이 사용할 수 있으며, 각 값을 포맷팅할 수 있는 장점이 있다.
  3. 문자열 포맷 리터럴을 사용하는 방법 : 포맷 메소드를 사용하지 않고 리터럴만으로 2.의 방법을 사용할 수 있다 (파이썬 3.6 이상)

전통적인 포맷 치환 방법

문자열 내에 어떤 값을 집어넣는것에 대한 필요는 아주 오래전부터 있어왔고, 이러한 치환자의 종류와 형식은 파이썬이 만들어지기 이전부터 일종의 표준으로 정의되어 자리잡고 있었다. 문자열 치환자는 퍼센트 문자 뒤에 포맷형식을 붙여서 치환자를 정의한다. 치환자를 포함하는 문자열과 각 치환자에 해당하는 값의 튜플을 (튜플을 아직 배우지는 못했지만…) % 기호로 연결하여 표현한다.

주요 치환자에는 다음과 같은 종류가 있다.

  • %d : 정수값을 나타낸다. d 앞에는 자리수와 채움문자를 넣을 수 있다. 예를 들어 %04d 라고 쓰면 앞의 0은 채움문자이고 뒤는 포맷의 폭이다. 즉 %04d 는 0으로 시작하는 네자리 정수를 의미한다. 13이라는 값을 포맷팅할 때, %d 에 치환하면 “13”이 되지만, %04d 에 치환되는 경우에는 “0013”으로 치환된다.
  • %f : 실수값을 나타낸다. f 앞에는 .3 과 같이 소수점 몇 째자리까지 표시할 것인지를 결정하는 확장정보를 넣을 수 있다. "%.3f" 라는 템플릿은 1.5를 “1.500”으로 표시해준다.
  •  그 외에 정수값은 숫자 리터럴과 같이 %b, %o, %x를 이용해서 각각 이진법, 8진법, 16진법으로 표시할 수 있다.
  • %s : 문자열을 의미한다.
  • %r : “representation”으로 타입을 구분하지 않는 값의 표현형을 말한다. 표시되고자 하는 값의 타입이 분명하지 않을 때 사용한다. 위에서 소개된 %d, %f, %s 에 대해서 올바르지 않은 타입의 값을 치환하려 하면 TypeError가 발생한다. 이럴 때 사용할 수 있다.

format 메소드를 사용하는 방법

파이썬의 기본 내장함수 중에서도 format() 함수가 있는데, 이 함수도 문자열의 format 메소드와 동일한 동작을 한다. format(문자열, 치환값)` 의 형식으로 사용하며 그외 내용은 이 절에서 설명하는 것과 동일하다.

전통적인 치환자구분이 타입을 가린다는 제약이 있고, 포맷팅의 방법이 제한되어 있다는 부분때문에 최근에 대세를 이루는 방식이다. 이 방식에서는 %d 와 같은 표현 대신에, { } 를 사용한다. { } 자체가 하나의 값을 의미하며, 번호를 부여해서 한 번 받은 값을 여러번 사용할 수 있다. 이 포맷팅 방식을 미니포맷이라고 불리는데 대략 다음과 같은 문법을 가지고 있다.

{ 치환자 }

치환자  ::= [필드이름] [!변환형] [: 포맷정의]
포맷정의 ::= [[채우기]정렬][부호][#][0][폭][그룹옵션][.소수점자리][타입]

뭔가 포맷방식이 엄청난데, 채우기와 폭은 전통적인 포맷 치환자에서도 지원하던 것인데, 왼쪽 정렬이나 중앙정렬도 설정할 수 있다. 숫자값인 경우에는 특별히 d, f, x, b, o 등의 표현타입도 정의해줄 수 있다. 이 부분을 더 깊게 파고들려면 많은 분량이 필요하므로, 앞으로 진도를 나갈 때 예제 등에서 사용하면서 설명하도록 하겠다.

None

객체의 개념에 대해서 아직 설명하기 전이라 None을 명확하게 설명하기는 어렵다. 다만 어떤 값이 없는 상태를 가리킬만한 표현이 마땅히 없기 때문에 “아무것도 없다”는 것으로 약속해놓은 어떤 값을 하나 만들어 놓은 것이다. None 이라고 대문자로 시작하도록 쓰며, 실제 출력해보아도 아무것도 출력되지 않는다. 값이 없지만 False 나 0 과는 다르기 때문에 어떤 값으로 초기화하기 어려운 경우에 쓰기도 한다.

 

정리

이상으로 가장 기본적인 파이썬의 값 타입들에 대해서 살펴보았다. 물론 리스트와 사전, 그리고 튜플과 같은 다른 타입들이 있지만, 분량 관계상 이 글에서 계속 다루는 것 보다는 이쯤에서 한 번 끊고 가는 것이 좋겠다. 계속해서 다음 포스팅으로 이어나가시라….


  1. floating number. 0.5와 같이 소수점 이하 자리를 갖는 수를 말한다. 
  2. 이 글에서는 관례를 따라 숫자값을 표현할 때는 숫자만 쓰고, 문자 자체를 표현할 때는 따옴표를 적용할 것이다. 
  3. 1 + 0.5 를 계산할 수 있듯이 정수와 실수는 몇 가지 연산에서 호환이 가능하다. 이 타입간 호환은 사용하는 프로그래밍 언어에 따라 제약이 있을 수 있다. 

LiveScript 살펴보기 – 03 함수

LS에서 함수는 일반 문법 편에서 잠깐 언급했듯이 화살표를 써서 간단히 정의할 수 있다. 이 함수 표현에서 중요한 점 두 가지는 첫 째 우변은 하나 이상의 표현식이라는 점과 표현식이 순서대로 나열되는 경우 맨 마지막 표현식의 결과가 자동으로 리턴된다는 것이다.

함수

LS는 함수형 프로그래밍 언어의 스타일을 많이 도입했다고 하였다. 비록 LS가 진짜 순수한 함수형 언어는 아니지만, 함수형 언어의 스타일을 도입한다는 것은 LS내의 함수라는 것은 가급적 아래와 같은 특징을 갖도록 디자인되어야 한다는 것이다.

  1. 순수성 : 함수의 결과값이 순수하게 파라미터에만 의존할 것. 따라서 입력된 인자가 같다면 항상 리턴될 출력값도 같음을 보장한다.
  2. 간결성 : 사실 억지로 만든 말이기는 한데, 함수에 대한 연산 (커링, 바인딩, 파이핑, 합성)이 다양하고, 함수 자체가 1급 시민인 점(이는 JS로부터 자연스럽게 물려받는 특징이다.)에 착안하여, 가능한 간결하고 명료한 함수들을 정의하고, 이러한 함수들을 조합하여 필요한 함수를 생성하는 방법을 지향하는 것이다.

이전에 예에서 리스트의 최대값을 찾는 getMax 함수를 잠깐 언급한 바, 있는데 이 함수는 JS로 쓴다면 아래와 같을 것이다.

function getMax(arr) {
  if(arr.length < 1) { return null }
  var x = arr[0];
  var i = 1;
  var result = x;
  while(i < arr.length) {
    if (x < arr[i] ) {
      x = arr[i];
      result = x;
    }
    i += 1;
  }
  return result;
}

그리고 LS에서는 다음과 같이 쓴다.

get-max = (arr) -> arr.reduce (>?)

너무 심하게 부풀려서 비교한다고 생각할 수 있는데, 물론 JS에서도 함수형 스타일로 맵/필터/리듀스를 할 수 있는 API가 Array 타입에 존재하고 있다. 따라서 위 LS 코드를 JS로 쓴다면 (그것은 마치 함수형 스타일의 JS 코드겠지만) 다음과 같이 쓸 수 있다.

var getMax = function(arr){ 
  return arr.reduce(function(x, y){ 
    return x > y ? x : y; 
  }
);

다만, 여기서 말하고자 하는 것은 LS로 쓰면 함수를 엄청 짧은 코드로 쓸 수 있다는 점이 아니라, “두 수 중에서 큰 수를 판단하는 함수”를 리스트의 각 원소에 대해서 순차적으로 적용하면 리스트 내에서 가장 큰 수를 찾을 수 있다”는 간단한 아이디어에 관한 것이다. 두 개의 연산(함수)에 대해 이해하고 그것을 연관지어 원하는 기능을 쉽게 조합할 수 있는 것이 함수형 스타일의 가장 큰 특징이라 할 수 있겠다.

리턴

함수의 본체가 하나의 표현식인 경우에는 LS의 함수 정의 문은 one-liner로 작성된다. 두 개 이상의 표현식이 함수의 본체를 구성하는 경우에는 -> 뒤에서 줄을 바꾸고 들여써서 블럭을 구분하여 표기할 수 있다.

times = (x, y) -> x * y
sum = (arr) ->
  s = 0
  for i in arr
    s += i
  s

마지막 표현식은 자동으로 리턴되므로, 명시적으로 리턴이 없는 함수를 정의하고 싶다면 !-> 기호를 써서 정의한다.

호출

함수 호출 시, ()를 생략한다. 또한 인자들은 callable하지 않다면 콤마를 생략하고 나열할 수 있다. 인자를 받지 않는 함수는 !를 써서 호출됨을 표시한다. 그리고 인자 뒤에 오는 and, or, xor, 한 칸 띈 후의 ., ?. 등은 모두 암묵적으로 호출 구문을 완료한다. 따라서 공백을 기준으로 체이닝 표기를 간단히 할 수 있다.

$ \h1 .find \a .text! #=> h1 a 의 내용
# $('h1').find('a').text()

f!
[1 2 3].reverse!slice 1 #= [2, 1]

익명함수의 경우, 인자가 없는 케이스에는 do를 -> 에 쓰면 이를 호출한다. 간단하게 익명함수를 이용해서 여러 표현식을 묶은 블럭을 만들고 실행하는 방법으로 이해.

do -> 3 + 2 
#=> 5
#(function(){
#  return 3 + 2;
#})();

do를 이름이 있는 함수1와 썼을 때, 그리고 do가 표현식에 쓰인게 아니라면 이름지은 함수는 유지된다. 즉, do를 평가하기 전에 함수를 참조할 수 있고, do 문을 만났을 때 한 번 더 실행하는 것이다.

i = 0
f 9 #f를 한 번 실행. i == 1
i #=> 1
do function f x # 여기서 한 번 더 실행
  ++i
  x
i # => 2

축약된 객체 블럭 문법은 함수 호출 구문에서 사용할 수 없다. 단, 한 줄에 쓰여진 리터럴은 인식된다. 함수의 인자 중 하나로 객체 블럭을 쓰고자 한다면 do를 이용해서 블럭을 명시해야 한다.

func
  a:1
  b:2
## 컴파일 되지 않음

## 대신 이렇게 쓸 수 있다.
func a:1, b:2

## 보통은 do를 사용한다.
func do
  a: 1
  b: 2

이처럼 do는 함수 호출 시에 요긴하게 많이 쓰인다. 특히 개별 인자를 각 라인의 표현식으로 사용하려할 때 유용하다.

함수의 중위표현

파라미터를 2개 받는 함수는 백팃으로 둘러 싸서 중위연산자처럼 쓸 수 있다.

add = (x, y) -> x + y
2 `add` 3 #=> 5

g = (a, b) ->
  add ...     # 함수 내에서 쓰이는 `...`은  인자를 그대로 넘긴다는 의미로 해석할 수 있다. 
# 11

그리고 함수의 본체 내에서 …을 쓰면 암묵적으로 모든 인자의 리스트로 인식한다. 이는 특히 super를 호출할 때 유용하다.

파라미터

파라미터는 표현식을 쓰는 표기로도 확장된다. 즉 객체나 다른 파라미터에 관한식으로 파라미터를 받으면, 해당 값으로 맵핑된다는 이야기이다. 이를 통해서 함수 본체에서 각 파라미터간의 관계를 재설정해야 하는 부담을 줄 일 수 있다. 아래의 예제를 보자.

set-person-params = (
  person
  person.age        # 두 번째 인자는 첫 번째 인자의 .age 키에 배당된다.
  person.height ) -> person

p = set-person-params {}, 21, 180cm
# {age: 21, height: 180}

# 'this' 확장하기
set-text = (@text) -> this
# var setText = function(text){ this.text = text; return this; }

위의 setPersonParams() 함수는 세 개의 인자를 받는데, 그 중 두 번째, 세 번 째 인자는 첫 번째 인자의 프로퍼티로 명시되어 있다. 따라서 함수 호출 시에 해당 값이 주어지면, 이는 첫번째 인자로 넘겨진 객체의 프로퍼티로 자동으로 세팅된다. 따라서 함수 본체 내에서 person.age = age와 같은 처리를 따로 하지 않고 인자 자체를 확장하여 person.age로 쓰는 것으로 대체가능하다.

특히 이 확장은 객체의 메소드나 함수를 작성할 때, 특히 this를 다룰 때 매우 간결하게 쓰일 수 있다.

디폴트 값

파라미터에는 디폴트 값을 미리 지정해줄 수 있으며, (파이썬 스타일), 좀 헷갈릴 수는 있는데, 논리 연산을 수행하여 디폴트값/오버라이드를 적용할 수 있다.

add = (x && 4, y || 3) -> x + y
# x는 무조건 4로 오버라이드되고
# y는 없으면 3이 된다.

또한 객체를 통으로 받아서 특정 키를 분해해 낼 수 있다.

set-coords = ({x, y}) -> "#x, #y"
set-coords y:2, x:3 #=> "3, 2"

# 그리고 그 와중에 다시 디폴트 값을...
set-coords = ({x = 1, y = 3}) -> "#x, #y"

그리고 ...y 등과 같이 일련의 인자들을 하나의 리스트로 취급하는 것도 가능하다.

f = (x, ...ys) -> x + ys.1
f 1 2 3 4 #=> 4

이외에도 캐스팅 연산자를 파라미터에 붙일 수 있는데, 그러면 자동으로 평가된 후 들어간다.

f = (!!x) -> x
f 'truely' # true

g = (+x) -> x
g ' ' # 0

obj = {prop: 1}
h = (^^x) ->
  x.prop = 99
  x
h obj
obj.prop # 1

파라미터의 생략

JS의 함수는 호출 시 파라미터 개수에 크게 구애받지 않는다. 즉 선언된 파라미터보다 부족한 개수의 인자가 넘겨지면, 빈 인자는 undefined를 갖게되고, 인자가 과하게 많이 넘겨지면 함수의 로컬 스코프에 매핑되지 못한 인자는 모두 무시된다.

LS의 함수에 있어서 정의를 파라미터 없이 함수를 만들었다 하더라도 함수 본체에서는 파라미터를 참조하는 것이 가능하다. 사실 이는 JS의 스펙에 정의된 arguments2 객체에 의한 것이다.

단 인자 함수의 파라미터는 it으로 지칭한다. 그외의 파라미터는 모두 &0, &1 과 같은 식으로 번호 순서대로 참조하여 처리할 수 있다.

바운드 함수

바운드 함수는 특정 객체에 소유권이 묶인 함수를 말한다. 보통 함수 내에서 this를 참조하는 것은 해당 함수를 호출한 문맥이 되는데, 바운드 함수는 처음 바운드한 시점의 문맥이 유지된다. 바운드 함수의 자세한 내용에 대해서는 별도로 찾아보도록 하고, 바운드 함수를 작성하는 것은 ~> 를 써서 웨이브진 화살표를 쓴다는 점만 기억하자.

obj = new
  @x = 10
  @normal = -> @x  
  @bound = ~> @x

obj2 = x: 5
obj2.normal = obj.normal
obj2.bound = obj.bound

obj2.normal! # this.x 이고 이 때 this는 obj2 이므로 5
obj2.bound # 10. bound메소드는 obj에 바인딩되어 있으므로, 내부의 this는 obj를 가리킨다.

흔히 바운드 함수는 특정 객체의 메소드를 이벤트 핸들러를 사용하며, 그 내부에서 this를 참조할 때 유용하다.

- 대신 ~를 쓰는 것이 바운드 함수를 의미한다는 것은 매우 일관적으로 적용되며, !~>, ~~>, ~function 등의 표현이 그대로 사용될 수 있다.

커링

커링은 하스켈 커리의 이름을 따서 명명되었는데, 모든 다 인자 함수는 단인자 함수들이 합성된 상태로 볼 수 있다는 것을 말한다. 예를 들어 두 정수를 더하는 add 라는 함수가 있다면 다음과 같이 정의하고 호출할 수 있다.

add = (x, y) -> x + y
add 3 2

이 때, 이 호출식을 (add 3) 2라고 생각하는 것이다. 그러면 add 3은 정수 하나를 받아서 3을 더한 값을 리턴하는 단인자 함수가 된다.

그렇다면 다시, adda 라는 정수를 받아서 “b라는 정수를 받아 여기에 a를 더해서 리턴하는 함수”를 리턴하는 함수라고 생각할 수 있다.

add = (a) ->
  (b) -> a + b

add-one = add 1
add-one 2
# 3

이렇게 커링을 이용하면 쉽게 부분적용된 함수를 만드는 것이 가능해진다. 그리고 LS에서는 자동으로 커리되는 함수를 만들 수 있는 선언법으로 -->를 쓰는 것을 지원한다. 역시 같은 맥락에서 바운드된 커리드 함수는 ~~> 으로 선언할 숭 있다.

add = (a, b) --> a + b
add-one = add 1
add-one 2 #=> 3

접근자 단축

접근자 메소드/함수의 경우에 맵이나 필터 동작에 적용되는 경우 예를 들어 (x) -> x.prop과 같은 식으로 처리하는 것은 (.prop)으로 줄여 쓸 수 있다. 이는 특정 프로퍼티를 호출하는 것을 괄호로 둘러싼 것이기 때문에 메소드 호출 역시 같은 식으로 처리할 수 있다.

map (.length) <[ hello there you ]> #=> [5, 5, 3]
filter (.length < 4), <[ hello there you ]> #=> ['you']

map (.join \|) [[1 2 3], [7 8 9]]
#=> ['1|2|3', '7|8|9']

반대로 (obj.) 이라고 쓰는 것은 (it) -> (obj.it)의 단축 표현이 된다.

obj = one:1, two:2, three:3
map (obj.) <[one, three]> #=> [1, 3]

부분 적용

표현식 내에 언더스코어(_)를 사용하여 해당 표현식을 1개 인자로 받는 함수로 간주하고, 언더 스코어는 해당 인자의 위치를 표시하는 플레이스 홀더로 생각할 수 있다. 이는 커리드 함수에서 적절한 위치에 있지 않은 파라미터를 가변으로 남기고 싶을 때 유용하다.

# 여기서 쓰인 filter는 별도로 정의된 top-level의 함수라 가정한다.
filter = (fn, arr) --> arr fn
# 이 떄, 특정한 리스트에 대해서 고정하고 필터링 함수만 변경하려할 때,
# 다음과 같이 쓰게 되는데
filter-nums = (fn) -> filter fn, [1 to 5] # 
# 인자로 받게되는 fn의 위치를 `_`를 이용해서 표시해주면 된다.
filter-nums = filter _, [1 to 5]


filter-nums (<3) # [1, 2]

이러한 부분적용된 함수는 특히 고차 함수를 파이핑으로 연결할 때 유용하다. 아래 예제는 underscore.js를 이용해서 특정한 리스트를 조작하는 코드이다. “인자로 받은 객체가 다시 인자로 전해질 때”를 상정하기 때문에, _의 사용이 혼동되지 않고 해석될 수 있다.

[1 2 3]
|> _.map _, (* 2)
|> _.reduce _, (+), 0
# => 12

백 콜

콜백으로 주어지는 표현식들은 결국 블럭으로써, 들여쓰기를 적용해야 한다. 백 콜은 이러한 방식을 거꾸로 표현하여 콜백을 들여쓰지 않고 표현할 수 있게 해준다.

map (-> it * 2), [1 to 3] 

# 백 콜로 전환
x <- map _, [1 to 3]
x * 2  # 여기서부터는 _ 에 들어갈 표현을 쓸 수 있다. 

백 콜은 콜백을 들여쓰지 않게 해주기 때문에, 중첩되는 콜백지옥을 깔끔하게 처리해주는 장점을 가지고 있다. 만약 top레벨의 코드 중간에 백 콜을 쓰게 된다면, 이후 끝라인까지의 모든 내용이 콜백 내의 코드로 간주된다. 백 콜의 코드가 중간에서 끝나야 한다면 미리 do를 사용해서 들여쓰기 블록을 시작해주자.

do
  data <-! $.get 'ajaxtest'
  $ '.result' .html data  # $.get \ajaxtest의 콜백이며, 콜백의 인자는 data
  processed <-! $.get 'ajaxprocess', data  # 콜백 내에서 `ajaxprocess`에 대한 처리를 또 호출
  $ '.result' .append processed # 여기는 콜백 내의 콜백이지만, 들여쓰기는 더 이상 없다.

alert \hi

위 코드는 다음과 같이 컴파일 된다.

$.get('ajaxtest', function(data){
  $('.result').html(data);
  $.get('ajaxprocess', data, function(processed){
    $('.result').append(processed);
  });
});
alert('hi');

LET/NEW

함수와 관련하여 let, new에 대한 표현을 짚고 마무리하도록 하겠다. let은 특정한 익명함수를 생성하는데, 해당 함수 내에서의 특정한 문맥을 생성해준다. 즉 let A = B { 블럭} 의 형태이며, 이 때 블럭 내에서 언급되는 A는 모두 B로 치환된다.

let $ = jQuery
  $.isArray []

위의 이 표현은 $jQuery가 되는 블럭 스코프를 생성한 후에 $.isArray []를 평가하였으므로 true가 된다. 이 때 외부 스코프에 $이 있더라도 여기서는 자체 스코프만을 참조할 것이다. 비슷하게 아래와 같은 코드도 작성할 수 있다.

x = let @ = a:1, b:2
  @b ^ 3
x #=> 8

new는 새로운 익명 컨스트럭터를 만들어서 즉시 호출하는 개념이다.

doc = new
  @name = \spot
  @mutt = true

# {name: 'spot', mutt: true}

보너스

다음은 nodejs를 통한 간단한 HTTP 서버의 기본 구현을 LS로 작성한 것이다. 콜백속에서 또 콜백을 전달하는 형태의 함수호출 패턴이 존재하고, 체이닝이 쓰인다. 이를 do와 백콜을 이용하여 깔끔하게 작성할 수 있다.

require! <[ fs http url ]>
const ROOTDIR = 'html/'

do
  (req, res) <-! htttp.create-server
  url-obj = url.parse req.url, yes no
  do 
    err, data <-! fs.read-file ROOTDIR + url-obj.pathname
    if err? 
      res.write-head 404
      res.end <| JSON.stringify err
    else
      res.write-head 200
      res.end data
.listen 8080

참고자료

  • LiveScript.net
  • HTTP 서버 구현: https://mylko72.gitbooks.io/node-js/content/chapter7/chapter7_4.html

  1. named function. 여기서는 function 키워드를 써서 정의한 함수를 말한다. 
  2. arguements에 대한 MDN 설명 참조.