Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

7장: 문자열과 출력 (Strings and Output)

문자열 처리는 어떤 언어에서든 실질적인 프로그래밍의 기반입니다. FunLang는 간결하지만 충분히 강력한 문자열 함수들을 제공하고, printf 계열 함수로 형식화된 출력도 지원합니다. 이 장에서는 문자열을 만들고 조작하는 방법부터, 화면에 출력하는 다양한 방법까지 살펴봅니다.

문자열 리터럴

문자열 리터럴은 큰따옴표와 표준 이스케이프 시퀀스를 사용합니다:

fn> "hello world"
"hello world"

fn> "tab\there"
"tab	here"

\t, \n, \\ 같은 표준 이스케이프 시퀀스를 모두 사용할 수 있습니다. REPL에서 문자열을 입력하면 따옴표가 포함된 결과가 나타나는데, 이는 “이것이 문자열 값임“을 명시하는 것입니다. 파일 출력(println 등)에서는 따옴표 없이 내용만 출력됩니다.

문자열 연결

+ 연산자로 문자열을 연결합니다:

fn> "hello" + " " + "world"
"hello world"

FunLang에서 +는 정수 덧셈과 문자열 연결 모두에 사용됩니다. 타입 추론 덕분에 컴파일러가 문맥에서 어떤 +인지 판단합니다. 단, 정수와 문자열을 섞어 쓰면 타입 오류가 발생합니다 — "age: " + 30은 안 되고, "age: " + to_string 30이 필요합니다.

문자열 함수

FunLang는 자주 쓰이는 문자열 연산을 Prelude의 String 모듈과 ^^ 연산자로 제공합니다. 각 함수의 동작 방식과 언제 사용하면 좋은지 함께 살펴봅니다.

참고: 이 함수들은 내부적으로 string_length, string_sub, string_concat, string_contains 등의 raw builtin 함수를 기반으로 합니다. raw builtin을 직접 호출할 수도 있지만, 일반적으로는 String 모듈 함수와 ^^ 연산자를 사용하는 것을 권장합니다.

String.length

문자 수를 반환합니다:

fn> String.length "hello"
5

fn> String.length ""
0

빈 문자열의 길이는 0입니다. 인덱스 범위 검사나 반복 횟수 계산에 자주 쓰입니다.

^^ 연산자 (문자열 연결)

^^ 연산자로 두 문자열을 연결합니다 (Core 모듈에서 제공):

fn> "hello" ^^ " world"
"hello world"

+ 연산자와 결과는 같지만, ^^는 문자열 전용 연결 연산자라는 점이 다릅니다. 람다와 함께 사용하면 접두사를 붙이는 함수를 만들 수 있습니다:

$ cat prefix.l3
let add_prefix = fun s -> "prefix:" ^^ s
let result = add_prefix "value"

$ fn prefix.l3
"prefix:value"

fun s -> "prefix:" ^^ s는 “앞에 ’prefix:’를 붙이는 함수“입니다. 이렇게 만든 add_prefix를 파이프라인에서 재사용할 수 있습니다. 예를 들어 리스트의 모든 항목에 접두사를 붙이거나, 고차 함수에 인자로 전달할 때 편리합니다.

String.substring

시작 인덱스와 길이로 부분 문자열을 추출합니다:

fn> String.substring "hello" 1 3
"ell"

String.substring s start len은 인덱스 start부터 len개의 문자를 반환합니다. 인덱스는 0부터 시작합니다.

Python의 슬라이싱 s[1:4]와 비슷하지만, 끝 인덱스가 아니라 길이를 지정한다는 점에 주의하세요. Python에서 s[1:4]는 인덱스 1부터 3까지 3개의 문자를 가져오지만, FunLang에서는 String.substring s 1 3으로 동일한 결과를 얻습니다. start + len이 문자열 길이를 초과하지 않도록 주의하세요.

String.contains

문자열이 부분 문자열을 포함하는지 확인합니다:

fn> String.contains "hello world" "world"
true

fn> String.contains "hello" "xyz"
false

검색 결과를 bool로 반환하므로 if 표현식이나 조건 분기에 바로 사용할 수 있습니다. 파일 경로 검증, 키워드 필터링 같은 간단한 텍스트 검색에 유용합니다.

to_string

모든 타입의 값을 문자열 표현으로 변환합니다:

fn> to_string 42
"42"

fn> to_string true
"true"

fn> to_string "already a string"
"already a string"

ADT, 리스트, 튜플, 레코드 등 모든 복합 타입도 지원합니다:

fn> to_string (Some 42)
"Some 42"

fn> to_string [1; 2; 3]
"[1; 2; 3]"

fn> to_string (1, true)
"(1, true)"

to_string은 FunLang에서 가장 유연한 함수 중 하나입니다. 디버깅할 때 복잡한 값을 출력해보고 싶을 때, 또는 로그 메시지를 만들 때 항상 to_string으로 시작하면 됩니다.

참고: 문자열은 그대로 반환됩니다 (따옴표 없음, F# string 함수와 동일). 복합 타입 내부의 문자열에는 따옴표가 포함됩니다: to_string (Some "hi")Some "hi".

이 동작이 처음에는 약간 헷갈릴 수 있습니다. to_string "hello""hello"(따옴표 없이 hello)를 반환하지만, to_string (Some "hello")"Some \"hello\"" 형태로 내부 문자열에 따옴표가 붙습니다. 컨테이너 안의 문자열이라는 맥락에서 따옴표가 필요하기 때문입니다.

string_to_int

문자열을 정수로 파싱합니다:

fn> string_to_int "123"
123

외부에서 입력받은 숫자 문자열을 계산에 사용하기 전에 정수로 변환할 때 필요합니다. 파싱이 실패하는 경우(숫자가 아닌 문자열)에 대한 처리는 현재 별도로 필요합니다.

출력 함수

FunLang는 네 가지 출력 함수를 제공합니다. 각각 줄바꿈과 형식화 여부가 다릅니다. 어떤 상황에 어떤 함수를 쓰면 좋은지 함께 알아봅니다.

print

줄바꿈 없이 문자열을 출력하고, unit을 반환합니다:

fn> print "hello"
hello()

대화형 세션에서는 부수 효과 텍스트가 () 결과 바로 앞에 나타납니다. 여러 값을 같은 줄에 이어 출력하거나, 진행 상황을 한 줄에 표시할 때 사용합니다.

println

줄바꿈을 포함하여 문자열을 출력하고, unit을 반환합니다:

fn> println "hello"
hello
()

가장 자주 쓰이는 출력 함수입니다. 한 줄씩 메시지를 출력할 때 기본 선택입니다. print와의 차이는 출력 후 자동으로 줄바꿈(\n)을 추가한다는 것입니다.

printf

지정자를 사용한 형식화 출력:

지정자타입예제
%dintprintf "%d" 42
%sstringprintf "%s" "hi"
%bboolprintf "%b" true
%%리터럴 %printf "100%%"

printf는 커링되어 있으며, 각 지정자가 하나의 인자를 소비합니다:

$ cat printf_demo.l3
let _ = printf "%s is %d years old\n" "Alice" 30
let result = 0

$ fn printf_demo.l3
Alice is 30 years old
0

C의 printf나 Python의 % 포맷팅에 익숙하다면 자연스럽게 느껴질 것입니다. 중요한 차이는 FunLang의 printf가 커링되어 있다는 점입니다 — printf "%s is %d" "Alice"는 아직 인자를 하나 더 받아야 하는 함수를 반환합니다. 이 덕분에 부분 적용이 가능합니다. 예를 들어 let log_int = printf "value: %d\n"으로 정수를 로깅하는 함수를 만들 수 있습니다.

복수 지정자:

$ cat printf_multi.l3
let _ = printf "name=%s, active=%b\n" "Bob" true

$ fn printf_multi.l3
name=Bob, active=true
0

형식 문자열의 지정자 순서와 인자의 순서가 일치해야 합니다. 지정자의 타입과 실제 인자의 타입이 다르면 타입 오류가 발생합니다 — 이것이 단순 문자열 연결보다 printf가 안전한 이유입니다.

printfn

printf와 동일하지만 자동으로 줄바꿈을 추가합니다:

$ cat printfn_demo.l3
let _ = printfn "name=%s, age=%d" "Alice" 30
let result = 0

$ fn printfn_demo.l3
name=Alice, age=30
0

printf "...\n"을 쓸 필요 없이 printfn "..."을 사용하세요. 줄바꿈을 형식 문자열 끝에 매번 붙이는 것을 잊기 쉽습니다. 한 줄씩 형식화된 출력을 할 때는 printfn이 실수를 줄여줍니다.

sprintf

형식화된 문자열을 반환합니다 (출력하지 않음):

fn> sprintf "%d + %d = %d" 1 2 3
"1 + 2 = 3"

fn> sprintf "name=%s" "Alice"
"name=Alice"

sprintf는 출력하지 않고 형식화된 문자열 값을 만들어 돌려줍니다. 바로 출력하는 대신 나중에 사용하거나 다른 함수에 전달할 형식화된 문자열이 필요할 때 유용합니다. Python의 str.format()나 f-string과 역할이 비슷합니다.

+to_string 대신 sprintf를 사용하면 더 간결합니다:

$ cat sprintf_vs.l3
// 이전: 수동 연결
let old = "result=" ^^ to_string 42

// 이후: sprintf
let new_ = sprintf "result=%d" 42

let result = (old, new_)

$ fn sprintf_vs.l3
("result=42", "result=42")

두 방법의 결과는 같지만, sprintf가 형식을 한눈에 파악하기 좋습니다. 특히 여러 값을 조합할 때 차이가 뚜렷합니다 — "name=" + to_string name + ", age=" + to_string age보다 sprintf "name=%s, age=%d" name age가 훨씬 읽기 좋습니다.

문자열 슬라이싱 (String Slicing)

String.substring보다 간결한 슬라이싱 구문을 제공합니다. s.[start..stop]은 인덱스 start부터 stop까지(양쪽 포함)의 부분 문자열을 반환합니다:

$ cat str_slice.l3
let s = "hello"
let _ = println (s.[1..3])
let _ = println (s.[0..0])
let t = "abcdef"
let _ = println (t.[0..2])
let _ = println (t.[3..5])

$ fn str_slice.l3
ell
h
abc
def
()

s.[start..] 형태로 끝 인덱스를 생략하면, start부터 문자열 끝까지를 반환합니다:

$ cat str_slice_open.l3
let s = "hello"
let _ = println (s.[2..])
let _ = println ("hello world".[6..])

$ fn str_slice_open.l3
llo
world
()

String.substring이 시작+길이를 사용하는 반면, 슬라이싱은 시작+끝 인덱스를 사용합니다. Python의 s[1:4]와 비슷하지만 끝 인덱스가 포함(inclusive)된다는 차이가 있습니다.

String 모듈 함수

Prelude의 String 모듈은 문자열 검사 및 변환 함수를 제공합니다:

$ cat str_module.l3
let _ = println (to_string (String.endsWith "hello.txt" ".txt"))
let _ = println (to_string (String.endsWith "hello.txt" ".csv"))
let _ = println (to_string (String.startsWith "hello" "he"))
let _ = println (to_string (String.startsWith "hello" "wo"))
let _ = println (String.trim "  spaces  ")

$ fn str_module.l3
true
false
true
false
spaces
()
함수설명
String.endsWith s suffix문자열이 suffix로 끝나는지 확인
String.startsWith s prefix문자열이 prefix로 시작하는지 확인
String.trim s양쪽 공백 제거
String.length s문자열 길이
String.substring s start len부분 문자열 추출
String.contains s needle부분 문자열 포함 여부
String.concat sep lst구분자로 문자열 리스트를 연결

StringBuilder

문자열을 여러 번 +^^로 연결하면 매번 새 문자열이 생성되어 비효율적입니다. StringBuilder는 문자열 조각들을 모아두었다가 한 번에 합치는 가변 버퍼입니다:

$ cat sb_basic.l3
let sb = StringBuilder.create ()
let _ = StringBuilder.add sb "hello"
let _ = StringBuilder.add sb " "
let _ = StringBuilder.add sb "world"
let _ = println (StringBuilder.toString sb)

$ fn sb_basic.l3
hello world
()

StringBuilder.add는 문자열과 문자(char) 모두 받을 수 있습니다:

$ cat sb_char.l3
let sb = StringBuilder.create ()
let _ = StringBuilder.add sb "hi"
let _ = StringBuilder.add sb ' '
let _ = StringBuilder.add sb '!'
let _ = println (StringBuilder.toString sb)

$ fn sb_char.l3
hi !
()
함수설명
StringBuilder.create ()빈 StringBuilder 생성
StringBuilder.add sb s문자열 또는 문자를 추가
StringBuilder.toString sb축적된 내용을 문자열로 반환

루프 안에서 문자열을 반복적으로 조립할 때, +보다 StringBuilder가 훨씬 효율적입니다.

부수 효과 순서 지정

함수형 언어에서 출력은 “부수 효과(side effect)“입니다 — 값을 계산하는 것이 아니라 세계에 변화를 가져다줍니다. FunLang에서 unit을 반환하는 연산을 순서대로 실행하려면 let _ =를 사용합니다:

$ cat sequence.l3
let _ = println "first"
let _ = println "second"
let _ = println "third"
let result = "done"

$ fn sequence.l3
first
second
third
"done"

let _ =는 unit 결과를 바인딩하고 버림으로써, 부수 효과가 순서대로 실행되도록 보장합니다.

let _ =가 필요한가요? FunLang는 기본적으로 표현식 기반 언어입니다. println "first"는 unit 값 ()을 반환하는 표현식입니다. 이 결과를 이름 없이 버리면서 다음 표현식으로 넘어가는 방법이 let _ =입니다. _는 “이 이름에는 관심이 없다“는 관례적인 표기입니다. 시퀀싱이 자연스러운 명령형 언어와 달리, 순수 함수형 언어에서는 부수 효과의 순서를 명시적으로 표현해야 합니다.

문자열 함수와 파이프

문자열 함수는 파이프 연산자와 자연스럽게 결합됩니다:

fn> "hello" |> String.length
5

문자열 변환으로 파이프라인을 구성합니다:

$ cat string_pipe.l3
let result = 42 |> to_string |> fun s -> "answer: " ^^ s

$ fn string_pipe.l3
"answer: 42"

42 |> to_string"42"를 만들고, |> fun s -> "answer: " ^^ s"answer: ""42"^^로 연결하여 "answer: 42"를 만듭니다. 파이프와 람다가 함께 쓰이는 전형적인 패턴입니다. 변환 단계가 많을수록 파이프라인 방식이 중첩 함수 호출보다 훨씬 읽기 좋습니다.

실용 예제: 형식화된 보고서

지금까지 배운 것을 조합해 구조화된 출력을 만드는 예입니다:

$ cat report.l3
let format_line label value =
    label + ": " + to_string value
let _ = println (format_line "width" 800)
let _ = println (format_line "height" 600)
let result = "report complete"

$ fn report.l3
width: 800
height: 600
"report complete"

format_line은 레이블과 임의의 값을 받아 형식화된 문자열을 만드는 함수입니다. to_string이 모든 타입을 받기 때문에, format_line은 정수뿐 아니라 문자열, 튜플, 레코드 등 어떤 타입의 값도 출력할 수 있습니다. 이처럼 작은 헬퍼 함수를 만들어 반복을 줄이는 방식이 함수형 프로그래밍의 기본 사고방식입니다.

참고 사항

이 장에서 다룬 함수들의 핵심 특징을 정리합니다:

  • String.substring은 시작+길이를 사용합니다 (시작+끝이 아님): String.substring "hello" 1 3 = "ell"
  • ^^는 문자열 연결 연산자입니다: "hello" ^^ " world" = "hello world"
  • **to_string**은 모든 타입을 받습니다 — 문자열은 그대로, 복합 타입은 구조적 표현
  • **printf**는 커링되어 있습니다: 각 % 지정자가 하나의 추가 인자를 소비합니다
  • 순서 지정: 모듈 수준에서 부수 효과를 체이닝하려면 let _ =를 사용하세요

문자열이 여러 문자의 시퀀스라면, 개별 문자를 다루는 방법도 필요합니다. 다음 장에서는 char 타입과 문자 변환 함수를 알아봅니다.