1장: 시작하기 (Getting Started)
FunLang는 F# 스타일의 들여쓰기 구문을 사용하는 정적 타입(statically-typed) 함수형 언어로, 대수적 데이터 타입(algebraic data types, GADTs 포함), 결정 트리 컴파일 방식의 패턴 매칭(pattern matching), 그리고 모듈 시스템을 지원합니다.
처음 FunLang를 접하면 “왜 또 다른 언어인가?“라는 생각이 들 수 있습니다. FunLang의 설계 철학은 간단합니다: F#과 OCaml의 표현력 있는 타입 시스템을 가져오되, 문법적인 군더더기를 최소화하는 것입니다. Haskell처럼 순수 함수형이지만 훨씬 접근하기 쉬운 문법을 목표로 합니다. 이 튜토리얼에서는 언어의 기초부터 차근차근 살펴봅니다.
FunLang 실행하기
FunLang는 세 가지 방식으로 실행할 수 있습니다. 각 방식은 서로 다른 용도에 최적화되어 있으므로 상황에 맞게 선택하세요.
REPL (대화형 모드) 는 인자 없이 실행하면 대화형 세션을 시작합니다:
$ fn
FunLang REPL v14.0
Type :help for commands, #quit or Ctrl+D to exit.
fn> 1 + 2
- : int = 3
fn> let x = 42
val x : int = 42
프롬프트에서 표현식을 입력하면 타입과 값을 즉시 확인할 수 있습니다. let 바인딩은 다음 줄에서도 유지되므로 점진적으로 코드를 구축할 수 있습니다. :type 명령으로 표현식의 타입만 확인하거나, :load로 파일을 로드할 수도 있습니다.
REPL은 언어를 탐험하기에 가장 좋은 방법입니다. Python의 >>> 프롬프트처럼, 아이디어를 빠르게 테스트하고 타입과 동작을 확인할 수 있습니다. 새로운 함수를 작성하기 전에 REPL에서 먼저 논리를 검증하는 습관을 들이면 개발 속도가 크게 향상됩니다.
표현식 모드(Expression mode) 는 커맨드 라인에서 단일 표현식을 평가합니다:
$ fn --expr '1 + 2'
3
쉘 스크립트에서 FunLang를 활용하거나, CI 파이프라인에서 빠른 계산이 필요할 때 유용합니다.
파일 모드(File mode) 는 프로그램 파일을 평가합니다. 마지막 바인딩의 값이 출력됩니다:
$ cat hello.l3
let greeting = "hello"
let result = greeting + " world"
$ fn hello.l3
"hello world"
“마지막 바인딩의 값이 출력된다“는 동작 방식은 처음에는 낯설게 느껴질 수 있습니다. Python처럼 print 없이도 결과가 나온다는 점이 편리하지만, 이는 FunLang가 모든 것을 표현식으로 바라본다는 철학의 반영입니다. 의도적으로 마지막 let result = ...를 프로그램의 “반환값“으로 삼는 코딩 스타일을 자연스럽게 유도합니다.
진단 모드(Diagnostic modes) 는 평가 없이 컴파일 결과를 검사합니다:
$ fn --emit-ast --expr '1 + 2'
Add (Number 1, Number 2)
$ fn --emit-type --expr '1 + 2'
int
--emit-ast는 언어를 학습하거나 컴파일러 동작을 이해할 때 굉장히 유용합니다. 1 + 2가 Add (Number 1, Number 2)라는 트리 구조로 파싱된다는 것을 직접 눈으로 확인할 수 있습니다. --emit-type은 타입 추론 결과를 확인할 때 씁니다 – 복잡한 표현식의 타입이 무엇인지 헷갈릴 때 바로 물어볼 수 있는 도구입니다.
정수와 산술 연산
숫자 계산부터 시작하겠습니다. FunLang의 기본 숫자 타입은 정수(int)이며, 대부분의 언어와 동일한 연산자를 지원합니다.
표준 산술 연산자를 지원하며 일반적인 우선순위를 따릅니다. 나눗셈은 정수 나눗셈(integer division)입니다.
fn> 1 + 2 * 3
7
fn> 10 - 3
7
fn> 10 / 3
3
fn> -5
-5
fn> 10 % 3
1
fn> 7 % 2
1
%는 나머지(모듈로) 연산자입니다.
10 / 3 = 3이라는 결과에 주목하세요. Python 3의 // 연산자와 동일하게 소수점을 버리는 정수 나눗셈입니다. 부동소수점 계산이 필요하다면 이 점을 염두에 두어야 합니다. 수치 계산보다는 알고리즘과 자료구조에 집중하는 언어의 특성상 이 선택은 자연스럽습니다.
불리언 (Booleans)
논리 값과 논리 연산자입니다. 이 부분은 어느 언어나 비슷하지만, FunLang의 단락 평가 동작은 알아두면 도움이 됩니다.
true와 false를 지원하며, 단락 평가(short-circuit) 방식의 &&와 ||를 사용합니다.
fn> true && false
false
fn> true || false
true
단락 평가(short-circuit evaluation)란 불필요한 경우 오른쪽 피연산자를 평가하지 않는 것을 의미합니다:
fn> false && (1/0 = 0)
false
fn> true || (1/0 = 0)
true
1/0은 런타임 에러를 발생시키는 표현식이지만, 위 예제에서는 에러가 발생하지 않습니다. false && ...에서 앞이 false이면 결과는 무조건 false이므로 오른쪽을 평가할 필요가 없습니다. 이는 성능 최적화이기도 하지만, 실용적으로는 “왼쪽 조건이 참이어야만 오른쪽을 안전하게 평가할 수 있는” 가드 패턴에 자주 활용됩니다.
not 함수로 불리언을 부정할 수 있습니다:
fn> not true
false
not이 연산자(!)가 아닌 함수라는 점이 흥미롭습니다. FunLang에서는 not을 다른 고차 함수에 직접 넘길 수 있습니다. 예를 들어 filter not [true; false; true]처럼요 – 이것이 함수형 언어에서 연산자 대신 함수를 선호하는 이유 중 하나입니다.
문자열 (Strings)
문자열 리터럴은 큰따옴표를 사용하며 표준 이스케이프 시퀀스(\n, \t, \\, \")를 지원합니다.
문자열 연결(concatenation)에는 + 연산자를 사용합니다.
fn> "hello" + " world"
"hello world"
fn> "line1\nline2"
"line1
line2"
문자열 연결에 +를 쓰는 것은 Python과 같은 관례입니다. Haskell이나 OCaml에서는 ^나 ^ 같은 별도 연산자를 쓰는 것과 비교하면, FunLang는 직관성을 우선시합니다.
내장 문자열 함수:
fn> String.length "hello"
5
fn> String.substring "hello" 1 3
"ell"
fn> to_string 42
"42"
String.substring "hello" 1 3은 인덱스 1부터 3개의 문자를 추출합니다. to_string은 숫자를 문자열로 바꿀 때 유용합니다 – 특히 숫자와 문자열을 연결하고 싶을 때 "값: " + to_string 42처럼 자주 씁니다. 문자열 함수의 전체 목록과 printf 포맷 출력은 7장: 문자열과 출력에서 다룹니다.
비교 연산자
다른 언어와 비교하면 FunLang의 비교 연산자에서 눈에 띄는 차이점이 있습니다. 처음에 실수하기 쉬운 부분이므로 주의를 기울이세요.
등호는 =입니다 (==가 아닙니다). 부등호는 <>입니다.
fn> 1 = 1
true
fn> 1 <> 2
true
fn> 3 < 5
true
fn> 3 >= 3
true
=는 ML 계열 언어(F#, OCaml, SML)의 전통적인 관례입니다. C, Java, Python에서 온 개발자라면 처음에 ==를 습관적으로 입력할 수 있습니다. 마찬가지로 “같지 않음“을 !=가 아닌 <>로 표현하는 것도 ML 전통입니다. 며칠 쓰다 보면 금방 익숙해집니다.
조건문 (Conditionals)
FunLang에서 if는 문(statement)이 아니라 표현식(expression)입니다. 이 차이가 언어 전체를 관통하는 중요한 설계 원칙입니다.
fn> if 1 < 2 then "yes" else "no"
"yes"
두 분기(branch)는 동일한 타입이어야 합니다. else 분기는 필수입니다.
if가 표현식이라는 의미는, 변수에 직접 담을 수 있다는 뜻입니다: let label = if score > 90 then "A" else "B". C나 Java에서는 삼항 연산자(? :)를 별도로 두는 반면, FunLang에서는 if-then-else 자체가 그 역할을 합니다.
else가 필수인 이유도 여기서 비롯됩니다. “값을 돌려주는 표현식“이 else 없이 존재한다면 조건이 거짓일 때 어떤 값을 돌려줄지 알 수 없기 때문입니다. 타입 시스템이 이를 강제하여 잠재적인 버그를 컴파일 타임에 잡아냅니다.
주석 (Comments)
//로 줄 주석을, (* ... *)로 블록 주석을 작성합니다:
fn> 1 + 2 // this is ignored
3
fn> (* block comment *) 1 + 2
3
줄 주석 //는 C/Java/Python 스타일로 익숙할 것입니다. 블록 주석 (* ... *)는 OCaml과 동일한 형식입니다. 블록 주석은 코드 섹션을 잠시 비활성화하거나, 함수 위에 여러 줄 설명을 붙일 때 쓰면 좋습니다.
유닛 타입 (Unit Type)
유닛 타입은 처음 접하면 “이게 왜 필요하지?“라는 의문이 들 수 있는 개념입니다. 하지만 함수형 언어에서는 없어서는 안 될 중요한 역할을 합니다.
유닛 타입 ()는 “의미 있는 값이 없음“을 나타내며, 부수 효과(side effects)에 사용됩니다:
fn> ()
()
fn> println "hello"
hello
()
println "hello"는 화면에 텍스트를 출력하고 ()를 반환합니다. “화면에 출력하는 행위” 자체가 목적이고, 반환값은 중요하지 않을 때 ()를 씁니다. println 외에 print, printf, sprintf 등의 출력 함수는 7장: 문자열과 출력에서 자세히 다룹니다.
왜 void 대신 ()를 쓸까요? ()는 실제 값입니다 – 타입 시스템 안에서 다룰 수 있습니다. 반면 void는 “값이 없다“는 특수 케이스로 취급해 타입 시스템을 복잡하게 만듭니다. ()를 쓰면 “부수 효과를 수행하는 함수“도 일관된 방식으로 표현할 수 있습니다. Haskell의 IO (), F#의 unit도 같은 개념입니다.
구문 참고 사항
이 챕터를 마치기 전에, FunLang 문법 전반에 걸쳐 알아두어야 할 핵심 규칙들을 정리합니다. 이 규칙들은 처음에는 제약처럼 느껴질 수 있지만, 실제로는 코드를 더 읽기 쉽게 만들어줍니다.
- 들여쓰기 기반 – 블록에 세미콜론이나 중괄호가 필요 없습니다 (F# 스타일)
- 파일 모드: 최상위 레벨에서
let바인딩을 사용하며in이 필요 없습니다; 마지막 바인딩의 값이 출력됩니다 - REPL:
let x = 42로 영속적 바인딩 생성; 표현식도 바로 평가 가능 - 표현식 모드 (
--expr): 지역 바인딩에let x = ... in body를 사용합니다 match파이프는match키워드의 열(column)에 맞춰 정렬해야 합니다
들여쓰기 기반 문법은 Python을 써본 분들에게 친숙할 것입니다. 중괄호나 세미콜론 없이 들여쓰기만으로 블록 구조를 표현합니다. 한 가지 주의할 점은 탭과 스페이스를 섞어 쓰면 예상치 못한 파싱 오류가 생길 수 있다는 것입니다 – 일관되게 스페이스를 사용하는 것을 권장합니다.