라이브러리 이름 안내: 예시에서 사용된 표준 라이브러리 최소 표면 밖의 도우미·멤버 호출(
날씨API.현재날씨,split,첫글자대문자,join,추가,format,Date.parse,CSV.읽기등)은 설명용 placeholder이며 canonical API가 아닙니다.
말하는 방법은 하나. 토파즈는 표면을 작고 닫힌 상태로 유지합니다: 의도마다 하나의 정형이 있고, v5.2 명세에 잠겨 있습니다. 이 가이드는 그 표면을 처음부터 끝까지 다룹니다.
핵심 철학
의도마다 하나의 정형
복잡한 로직도 간결하고 읽기 쉽게 표현할 수 있습니다.
글로벌 문법, 로컬 표현
- 키워드: 영어로 통일 (
function,let,match,case) - 식별자: 한글, 영어, 이모지 자유롭게 사용 가능
모든 것은 표현식
if, match, for, block 같은 구문은 값을 반환합니다.
기본 문법
변수 선언
// 불변 변수 (기본)
let 이름 = "김토파즈"
let 나이 = 25
let 활성화 = true
// 가변 변수
let mut 점수 = 85
점수 = 90 // 변경 가능
// 타입 명시 (선택적)
let 거리: float = 3.14
let 사용자들: Array<string> = ["김철수", "박영희"]null 병합 할당 (??=)
대상 값이 None/null일 때만 초기화하는 문장 전용 연산자입니다.
let mut 이름: Option<string> = None
이름 ??= Some("guest")함수 정의
// 기본 함수
function 인사하기(이름: string) -> string {
"안녕하세요, {이름}님!"
}
// 기본값 매개변수
function 계산하기(x: int, y: int = 10) -> int {
x + y
}
// 레코드 반환값
function 좌표계산() -> { x: int, y: int } {
{ x: 100, y: 200 }
}
// 고차 함수
function 변환(데이터: Array<int>, 함수: (int) -> int) -> Array<int> {
map(데이터, 함수)
}타입 위치에서 Topaz 공개 문서는 함수 타입을 (T) -> U 형태로 표기합니다.
조건문
// if 표현식 (값을 반환)
let 메시지 = if 나이 >= 20 {
"성인입니다"
} else if 나이 >= 13 {
"청소년입니다"
} else {
"어린이입니다"
}
// 삼항 연산자 스타일
let 상태 = if 온라인 { "접속중" } else { "오프라인" }패턴 매칭
// 기본 매칭
let 결과 = match 값 {
case 1 => "하나"
case 2 => "둘"
case 3 => "셋"
case _ => "기타"
}
// 범위 매칭
let 등급 = match 점수 {
case 90..100 => "A"
case 80..<90 => "B"
case 70..<80 => "C"
case _ => "F"
}
// 구조 매칭
let 할인율 = match 고객 {
case { 등급: "VIP", 구매액 } if 구매액 > 1000000 => 0.3
case { 등급: "VIP" } => 0.2
case { 가입연수 } if 가입연수 > 1 => 0.1
case _ => 0.0
}리스트 패턴
나머지(rest) 마커 ..는 한 번만 쓸 수 있으며, 패턴 전체([..rest])
이거나 고정 선두 원소들 뒤의 마지막 원소여야 합니다.
match xs {
case [] => 빈처리()
case [head, ..tail] => 사용(head, tail)
case [x, y, ..rest] if rest.length > 0 => 처리(x, y, rest)
}엔드투엔드 예시(가드 + rest 패턴):
let 설명 = match 숫자들 {
case [] => "빈 리스트"
case [단일] => "하나: {단일}"
case [첫, ..나머지] if 나머지.length > 3 => "{첫}부터 긴 연속"
case [첫, ..나머지] => "첫 {첫}, 나머지={나머지.length}"
}파이프라인 연산
기본 파이프라인
// 데이터가 흐르는 듯한 표현
let 결과 = 원본데이터
|> 정규화()
|> 필터링(x => x > 0)
|> 맵핑(x => x * 2)
|> 정렬()|>(또는 . / ?.)로 시작하는 줄은 이전 표현식을 이어가므로, 위와
같은 여러 줄 파이프라인은 하나의 문장입니다.
단계별 텍스트 파이프라인
let 처리된문자열 = "hello world"
|> split(_, " ")
|> map(_, 단어 => 첫글자대문자(단어))
|> join(_, " ")
|> 추가("!", _)
// 결과: "Hello World!"파이프 슈거
파이프 오른쪽에서 간결한 표기법을 사용할 수 있습니다.
let 이름들 = ["안", "보", "초"]
let 요약 = 이름들
|> .length // 속성 슈거: (xs => xs.length)
|> format("개수=", _) // 플레이스홀더: (n => format("개수=", n))
// 가장 가까운 호출 안의 모든 _ 는 같은 파이프 값을 받습니다
let 한줄 = 이름들 |> format("첫번째 ", _, " 전체 ", _)타입 시스템
타입 추론
// 타입이 자동으로 추론됩니다
let 숫자 = 42 // int
let 문자열 = "안녕" // string
let 배열 = [1, 2, 3, 4] // Array<int>
let 구조체 = { // { 이름: string, 나이: int }
이름: "김토파즈",
나이: 25
}리터럴 타입
// 정확한 값으로 타입 제한
type 신호등 = "빨강" | "노랑" | "초록"
type 주사위 = 1 | 2 | 3 | 4 | 5 | 6
type 상태코드 = 200 | 404 | 500
function 신호처리(색: 신호등) {
match 색 {
case "빨강" => 정지()
case "노랑" => 주의()
case "초록" => 출발()
// 모든 경우를 처리했는지 컴파일러가 확인!
}
}유니온 타입
type 사용자입력 = string | int | null
function 처리(입력: 사용자입력) -> string {
match 입력 {
case 텍스트: string => "문자열: {텍스트}"
case 숫자: int => "숫자: {숫자}"
case null => "입력 없음"
}
}null과 Option<T>는 관련되어 있지만 동일하지 않습니다. T | null 같은 nullable union에는 null을 사용하고, Option<T>에는 Some(...) / None을 사용하세요. ??와 ?.는 두 모델 모두에 동작합니다.
반복문
for 루프 (표현식)
// 범위 반복
for i in 1..10 {
print("숫자: {i}")
}
// 배열 반복
for 항목 in ["사과", "바나나", "체리"] {
print("과일: {항목}")
}
// for도 값을 반환!
let 제곱수들 = for x in 1..5 { x * x } // [1, 4, 9, 16, 25]by를 사용한 범위 스텝
// 증가 스텝
for i in 0..10 by 2 { print("{i}") } // 0,2,4,6,8,10
// 감소 스텝 (음수 stride)
for i in 10..0 by -3 { print("{i}") } // 10,7,4,1while, break, continue
Topaz는 일반 루프를 문장으로 제공합니다. while은 bool 조건이
참인 동안 본문을 실행하고, break는 가장 안쪽 루프를 빠져나가며
continue는 다음 반복으로 건너뜁니다.
let mut 합계 = 0
let mut n = 1
while 합계 + n <= 100 {
합계 = 합계 + n
n = n + 1
}
print("합계: {합계}")
// break / continue 는 가장 안쪽 루프를 대상으로 합니다
for 항목 in [3, -1, 7, 120, 5] {
if 항목 < 0 { continue }
if 항목 > 99 { break }
print("{항목}")
}while은 문장이며 값을 만들지 않습니다. 값을 수집하는 표현식
for에는 그 for를 대상으로 하는 break/continue를 쓸 수
없습니다. 그런 경우 문장 for를 사용하세요.
컬렉션
배열
// 기본 배열
let 숫자들 = [1, 2, 3, 4, 5]
let 과일들 = ["사과", "바나나", "체리"]
// 함수형 helper
let 큰수들 = filter(숫자들, x => x > 2) // [3, 4, 5]
let 제곱들 = map(숫자들, x => x * x) // [1, 4, 9, 16, 25]
let 합계 = reduce(숫자들, (acc, x) => acc + x, 0) // 15멤버십 (in)
// 배열/리스트/집합
let ok1 = 3 in [1, 2, 3]
let ok2 = "a" in Set.of("a", "b")
// 맵 키
let hasId = "id" in 사용자맵.keys
// 범위
let inside = 5 in 1..10
let outside = 10 in 1..<10레코드와 타입 별칭
// 레코드 리터럴
let 사용자 = {
이름: "김토파즈",
나이: 25,
이메일: "topaz@example.com"
}
// 레코드 타입 별칭
type 사용자정보 = {
이름: string,
나이: int,
이메일: string,
}
// 별칭 타입 명시가 붙은 레코드 값
let 새사용자: 사용자정보 = {
이름: "박루비",
나이: 30,
이메일: "ruby@example.com",
}레코드 업데이트 리터럴
불변으로 얕은 복사를 한 뒤 선택한 필드만 갱신합니다.
let 사용자 = { 이름: "Alice", 나이: 20, 도시: "Seoul" }
let 갱신 = 사용자{ 나이: 사용자.나이 + 1, 도시: "Busan" }
// { 이름: "Alice", 나이: 21, 도시: "Busan" }문자열 템플릿
기본 보간
let 이름 = "토파즈"
let 나이 = 25
let 인사 = "안녕하세요, {이름}님! 나이가 {나이}세이시군요."여러 줄 문자열
삼중따옴표 문자열은 여러 줄을 담습니다. 닫는 구분자의 공백 접두사가 각
줄에서 제거되고, {expr} 보간은 그대로 동작합니다.
let 배너 = """
Topaz v5.2
말하는 방법은 하나
"""태그드 템플릿
html"...", 백틱 태그드 템플릿, 사용자 정의 태그는 canonical Topaz가 아닙니다. canonical 태그드 템플릿은 double-quoted string(단일행 또는 삼중따옴표)을 쓰는p"...",r"...",sh"...",sql"..."입니다.
표준 태그와 안전/메타 보존:
let 경로 = p"/home/{사용자}/docs/{파일명}" // 경로 정규화
let 패턴 = r"^[a-z0-9_]+$" // 정규식 이스케이프 간소화
let 명령 = sh"grep {패턴} {파일}" // 안전 셸 템플릿(실행 정책 적용)
// SQL: 보간은 항상 파라미터 바인딩으로 처리 (직접 문자열 삽입 금지)
let 쿼리 = sql"SELECT * FROM users WHERE age > {나이} AND city = {도시}"
// 여러 줄 SQL은 삼중따옴표 형식과 결합합니다
let 보고서 = sql"""
SELECT name, total
FROM orders
WHERE region = {지역}
ORDER BY total DESC
"""비동기 처리
비동기 모델
Topaz는
concurrent를 통해서만 병렬 작업을 정의합니다: 단순 조인 형식과,else가 타임아웃 시에만 실행되는 타임아웃 형식입니다. 네이티브async/await와 automatic async 의미론은 계속 deferred 상태입니다.
병렬 실행
// 여러 작업을 병렬로 실행
let 대시보드 = concurrent(timeout: 3s) {
날씨: 날씨API.현재날씨("서울")
환율: 환율API.달러환율()
뉴스: 뉴스API.헤드라인(5)
} else {
{
날씨: None,
환율: None,
뉴스: []
}
}에러 처리
Result 타입
function 안전한나누기(a: int, b: int) -> Result<int, string> {
if b == 0 {
Err("0으로 나눌 수 없습니다")
} else {
Ok(a / b)
}
}
// ? 연산자로 간단하게
function 복잡한계산(x: int) -> Result<int, string> {
let 결과1 = 안전한나누기(x, 2)?
let 결과2 = 안전한나누기(결과1, 3)?
Ok(결과2 * 10)
}defer
function 로그쓰기(메시지: string) -> Result<(), string> {
let 파일 = open("app.log")?
defer { 파일.close() }
파일.write(메시지)?
return Ok(())
}Topaz canonical 에러 처리는
Result+?와defer를 함께 사용합니다.try키워드 형식은 Topaz v5.2에 없습니다. 후위expr?가 canonical 전파 표기입니다.
고급 기능
람다를 이용한 부분 적용
플레이스홀더 _는 파이프라인 우변 전용입니다. 파이프라인 밖에서의
부분 적용은 람다로 표기합니다.
let 더하기10 = x => 더하기(10, x)
let 결과 = map([1, 2, 3], 더하기10) // [11, 12, 13]옵션-안전 옵셔널 체이닝 (?.)
좌변이 Option<T> 또는 T | null 같은 null 포함 유니온일 때만 사용하는 연산자입니다. 좌→우로 단락 평가하며 내부적으로 Option.map/flatMap으로 환원됩니다.
let 사용자: Option<{ 이름: string, 프로필: Option<{ 도시: string }> }> =
Some({ 이름: "Ann", 프로필: Some({ 도시: "Seoul" }) })
let 이름옵션 = 사용자?.이름
let 도시옵션 = 사용자?.프로필?.도시
// ??와 조합해 기본값 제공
let 도시 = 사용자?.프로필?.도시 ?? "Unknown"
// 일반 객체 체이닝이 아닙니다 — Option 이 아닌 값에는 `.` 사용
let 일반 = { 이름: "Jo" }
let ok = 일반.이름매크로
매크로는 Topaz v5.2에 명시되어 있지 않습니다.
실용적 예제
웹 API 호출
let 사용자데이터 = fetch("https://api.example.com/users/1")
|> json()
|> (data => {
이름: data.name,
이메일: data.email,
가입일: Date.parse(data.created_at)
})데이터 처리 파이프라인
let 분석결과 = CSV.읽기("sales.csv")
|> 필터(행 => 행.매출 > 1000000)
|> 그룹화(행 => 행.지역)
|> 집계(그룹 => {
지역: 그룹.키,
총매출: 그룹.값.합계(행 => 행.매출),
평균매출: 그룹.값.평균(행 => 행.매출)
})
|> 정렬(내림차순: 행 => 행.총매출)다음 단계
이제 토파즈의 기본 문법을 마스터했습니다! 다음으로 학습할 내용:
표면은 작습니다. 이 페이지들이 그 전부를 다룹니다.