2.10. fmt - 포맷 트레잇
rok::core::fmt 모듈은 값을 사람이 읽을 수 있는 문자열로 변환하는 트레잇 패밀리를 정의합니다. f-string f"{x}", f"{x:?}", f"{n:08b}" 등 모든 형식 지정자(format spec) 는 본 장의 트레잇으로 dispatch 됩니다.
refs-lang 1.7.4 f-string format spec 의 렉시컬 분해와 이 장의 트레잇 dispatch 가 함께 f-string 의 의미를 결정합니다.
2.10.1. dispatch
f-string 의 구멍 {expr[:spec]} 은 spec 의 type 이 ? 이면 Debug::fmt, 아니면 Display::fmt 를 부릅니다. spec 의 나머지 (fill / align / sign / # / 0 / width / precision 과 b o x X e E f) 는 Formatter 에 실립니다.
- 수 (모든 정수, 부동소수,
int,float) 는 자기 출력에 spec 을 직접 적용합니다 (2.10.5, 2.10.6). - 그 밖의 값의
fmt는 spec 을 보지 않습니다 (f.width()는none). 값을 그대로 쓰면, Formatter 가 그 뒤에 fill / align / width 를 적용하고 precision 으로 자릅니다.
d / g / s 는 기본 표기의 다른 이름입니다. 각 기호와 flag 의 의미는 refs-lang 1.7.3 이 정의합니다. f-string 은 구멍마다 중간 문자열을 만들지 않고 Formatter 하나의 buffer 에 차례로 씁니다.
2.10.2. Formatter
struct Formatter { .. } // 출력 buffer + 현재 spec
enum Alignment { Left, Right, Center, Equal }
enum Sign { Plus, Minus, Space }
impl Formatter {
fn new() -> Formatter;
fn with_capacity(n: usize) -> Formatter;
fn with_spec(spec: FormatSpec) -> Formatter;
fn finish(self) -> str; // 쓴 텍스트
fn write_str(ref self, s: str) throws (FmtError);
fn write_char(ref self, c: char) throws (FmtError);
fn pad(ref self, s: str) throws (FmtError); // fill / align / width / precision 적용
fn pad_integral(ref self, is_nonnegative: bool, prefix: str, digits: str) throws (FmtError);
// 부호 + # 접두 + 0 / fill 채움
fn fill(self) -> char;
fn align(self) -> Alignment | none; // none: 기본 (수는 오른쪽, 그 밖은 왼쪽)
fn sign(self) -> Sign | none;
fn alternate(self) -> bool; // #
fn pad_zero(self) -> bool; // 0
fn width(self) -> usize | none;
fn precision(self) -> usize | none;
}
FormatSpec 은 파싱한 spec 값입니다. 정적 spec 은 컴파일 때, 동적 spec ({..} / ${..} 를 담은 것) 은 런타임에 한 번 파싱합니다.
struct FormatSpec {
fill: char; align: Alignment | none; sign: Sign | none; alt: bool; zero: bool;
width: usize | none; precision: usize | none; ty: char;
}
impl FormatSpec {
fn new() -> FormatSpec; // 빈 spec
fn parse(text: str) -> FormatSpec; // 잘못된 spec 이면 panic
}
2.10.3. Display
#[lang("display_trait")]
trait Display {
fn fmt(self, ref f: Formatter) throws (FmtError);
}
f"{x}" 의 기본 dispatch 입니다. 원시 수의 fmt 는 런타임 호출 하나로 spec 을 적용해 씁니다.
impl Display for str {
fn fmt(self, ref f: Formatter) throws (FmtError) { try f.write_str(self) }
}
struct Point { x: f64; y: f64; }
impl Display for Point {
fn fmt(self, ref f: Formatter) throws (FmtError) {
try f.write_str("(");
try Display::fmt(self.x, ref f);
try f.write_str(", ");
try Display::fmt(self.y, ref f);
try f.write_str(")");
}
}
f"{Point { x: 1.5, y: 2.5 }}"; //→ "(1.5, 2.5)"
f"{Point { x: 1.5, y: 2.5 }:>12}"; //→ " (1.5, 2.5)" (Point::fmt 안의 f.width() 는 none)
- 호출 자리에는
ref가 필요합니다 (Display::fmt(x, ref f)).x.fmt(f)꼴은Display와Debug를 함께 구현한 타입에서 모호하므로 경로로 적습니다. Display는 derive 하지 않습니다. 표현은 타입 설계자가 정합니다.
2.10.4. Debug
#[lang("debug_trait")]
trait Debug {
fn fmt(self, ref f: Formatter) throws (FmtError);
}
impl Debug for str { .. } // 유효한 rok 문자열 리터럴
impl Debug for char { .. } // 유효한 rok 문자 리터럴
f"{x:?}" 의 dispatch 이며, 다시 읽을 수 있는 표현을 목표로 합니다. str / char 의 Debug 는 rok 리터럴입니다. 따옴표, \\, \n / \r / \t 등은 escape 하고, 그 밖의 제어 문자와 bidi 문자 (U+202A..U+202E, U+2066..U+2069 등) 는 \u{..} 로 씁니다.
Display vs Debug 차이
두 트레잇은 목적이 다르며, 다음 표에 그 차이를 정리해 두었습니다.
| 측면 | Display | Debug |
|---|---|---|
| 대상 | end-user | 개발자 |
| 정확성 | 가독성 우선 | 정보 손실 없음 (round-trip 가능 지향) |
| 따옴표 | 문자열 그대로 (따옴표 없음) | 문자열을 "..." 로 감쌈 (이스케이프 포함) |
| 컨테이너 | 대개 정의되지 않음 | 모든 컬렉션 자동 |
| derive | 불가 (수동 구현) | 가능 (#[derive(Debug)]) |
let s = "hello\nworld";
let d1 = f"{s}"; //→ hello
// world (Display - 그대로)
let d2 = f"{s:?}"; //→ "hello\nworld" (Debug - 이스케이프)
사용자 정의 / derive
#[derive(Debug)]
struct Point { x: f64; y: f64; }
let p = Point { x: 1.5, y: 2.5 };
let s = f"{p:?}"; //→ "Point { x: 1.5, y: 2.5 }"
Debug 는 모든 필드의 Debug 출력을 합성하는 방식으로 자동 derive 가 가능합니다.
2.10.5. 정수의 spec
type | 정수 |
|---|---|
(없음), d, g, s | 10 진 |
f | 고정 소수점 ({5:.2f} → 5.00) |
b o x X | 2 / 8 / 16 진 (# 이면 0b / 0o / 0x) |
e E | 지수 표기 |
부동소수에 b o x X 를 주면 컴파일 에러 (정적 spec) 또는 panic (동적 spec) 입니다. 고정 폭 정수의 음수는 그 타입 폭의 2 의 보수로 나옵니다 ({-1i8:x} 는 "ff"). int 는 부호와 크기로 씁니다 (int 값 -255 는 {n:x} 로 "-ff").
let n: u32 = 0xCAFE;
f"{n:b}"; //→ "1100101011111110"
f"{n:#x}"; //→ "0xcafe"
f"{n:X}"; //→ "CAFE"
f"{42:08b}"; //→ "00101010"
f"{42:e}"; //→ "4.2e1"
2.10.6. 부동소수 출력
{}는 다시 읽어 같은 값이 되는 가장 짧은 자릿수이며 지수 표기를 쓰지 않습니다.1.0은1,0.1은0.1, NaN 은NaN, 무한대는inf/-inf입니다.{:e}는 같은 자릿수를 지수 표기로 씁니다 (0.15는1.5e-1).{:E}는E를 씁니다.- precision 이 있으면 그 자릿수로 반올림합니다 (
{x:.3e},{x:.2}). - promote 된
float(지수가 f64 밖) 는{}에서도 지수로 씁니다 (1.5e400).
let x: f64 = 12345.678;
f"{x}"; //→ "12345.678"
f"{x:e}"; //→ "1.2345678e4"
f"{x:.3e}"; //→ "1.235e4"
f"{1.0}"; //→ "1"
2.10.7. f-string format spec 전체 문법
권위는 refs-lang 1.7.3 이며, 여기서는 요약합니다.
spec = [fill][align][sign][#][0][width][.precision][type]
align = "<" | ">" | "^" | "="
sign = "+" | "-" | " "
type = "" | "s" | "d" | "g" | "f" | "?" | "b" | "o" | "x" | "X" | "e" | "E"
width = unsigned-int | hole
precision = unsigned-int | hole
hole = "{" expr "}" | "${" expr "}" // 중첩 구멍, 자기 spec 없음
fill = any single char (default: space)
동적 spec
let s1 = f"{x:.{prec}}"; //→ "3.1416" (precision 동적)
let s2 = f"{x:>${width}.${prec}}"; //→ " 3.1416" (width + precision 동적)
let s3 = f"{x:{full_spec}}"; //→ ... (전체 spec 동적)
spec 안의 $$ 는 $ 하나입니다. 본문의 $ 는 그대로입니다 (f"cost: $100"). 정적 spec 은 컴파일 타임에 검사하고, 동적 spec 이 펼친 결과가 올바르지 않으면 런타임에 panic 합니다. 렉서의 토큰 분해는 refs-lang 1.7.3 의 "어휘 처리" 를 따릅니다.
2.10.8. align / fill / width / precision
Formatter::pad 는 spec 의 align, fill, width 를 자동으로 처리해 주므로, 사용자 impl 은 컨텐츠만 만들면 됩니다.
align 을 적지 않으면 수 (정수, 부동소수) 는 오른쪽 정렬, 그 밖의 타입은 왼쪽 정렬입니다. < / > / ^ (와 fill) 를 적으면 어떤 타입이든 그 정렬을 따릅니다. ? (Debug) 도 width / align 을 받습니다.
let n: i32 = 42;
f"{n}"; //→ "42" (기본)
f"{n:5}"; //→ " 42" (width 5, 수는 기본 오른쪽 정렬)
f"{n:<5}"; //→ "42 " (left)
f"{n:>5}"; //→ " 42" (right)
f"{n:^5}"; //→ " 42 " (center)
f"{n:0>5}"; //→ "00042" (fill 0)
f"{n:*^7}"; //→ "**42***" (fill *, center, width 7)
f"{n:=+5}"; //→ "+ 42" (= : 부호와 숫자 사이를 채움, + : 양수 부호)
f"{n:+=5}"; //→ "+++42" (fill '+', align '=')
f"{-42:=6}"; //→ "- 42"
f"{-42:06}"; //→ "-00042" (0 : 0 으로 채움, = 정렬)
f"{n:+}"; //→ "+42" (+ : 양수에도 부호)
f"{"ab":5}"; //→ "ab " (문자열은 기본 왼쪽 정렬)
또한 precision 도 같은 방식으로 적용됩니다.
let x: f64 = 3.141592653589793;
f"{x:.2}"; //→ "3.14" (소수점 이하 2)
f"{x:.0}"; //→ "3"
f"{x:10.4}"; //→ " 3.1416" (width 10, precision 4, 수는 기본 오른쪽 정렬)
f"{x:<10.4}"; //→ "3.1416 " (왼쪽 정렬은 < 명시)
문자열에 적용되는 precision 은 최대 길이 잘라내기로 해석되며, 정수의 precision 은 무시됩니다.
let s = "hello world";
f"{s:.5}"; //→ "hello"
f"{n:.3}"; //→ "42"
2.10.9. 모듈 매핑
각 타입별로 어떤 트레잇이 구현되어 있는지는 다음 표에 정리됩니다.
| 타입 | Display | Debug |
|---|---|---|
정수, int, 부동소수, float | ✓ | ✓ |
bool, char, str | ✓ | ✓ |
List<T>, Set<T> | ✗ | ✓ (T: Debug) |
Dict<K, V> | ✗ | ✓ (K, V: Debug) |
(A, B, ..) | ✗ | ✓ (모두 Debug) |
T | none | ✗ | ✓ (T: Debug) |
Result<T, E> | ✗ | ✓ (T, E: Debug) |
Display 는 스칼라, 문자열, 직접 구현한 사용자 타입에만 있고 컨테이너에는 없습니다. Debug 는 거의 모든 타입에 있고 derive 로 만듭니다.
2.10.10. #[derive(Debug)]
#[derive(Debug)]
struct User { id: u64; name: str; }
let u = User { id: 1, name: "Alice" };
let s = f"{u:?}"; //→ User { id: 1, name: "Alice" }
derive 의 동작 방식은 다음과 같습니다. 우선 struct 는 TypeName { field1: <field1:?>, field2: <field2:?>, ... } 형태로 출력되며, tuple struct 는 TypeName(<f1:?>, <f2:?>, ...) 형태로 출력됩니다. enum 의 경우에는 VariantName 또는 VariantName(...) / VariantName { ... } 형태로 출력됩니다. 다만 derive 를 적용하려면 모든 필드가 (재귀적으로) Debug 를 구현하고 있어야 합니다.
한편 #[derive(Display)] 는 존재하지 않습니다. 표현을 결정하는 일은 사용자의 책임이기 때문입니다.
2.10.11. REPL / print / 자동 출력
refs-lang 9.7 JIT 셀 실행 흐름 의 REPL 절에서 다음과 같이 정의합니다.
> let x = 42;
> x
i32: 42
> 1 + 2
i32: 3
> "hello"
str: "hello"
REPL 은 마지막 식을 자동으로 출력할 때 값을 Debug 로 포맷해 type: value 로 출력하며, Debug 가 없는 타입은 타입만 출력합니다 (refs-lang 9.7.7, 2.12.4).
이와 달리 println 이나 print 는 오로지 Display 만 dispatch 합니다.
println(f"x = {x}"); // Display
println(f"x = {x:?}"); // Debug (spec 명시)
2.10.12. FmtError
struct FmtError;
stream 에 쓰다 실패할 때의 에러입니다. 메모리에 쓰는 경우 (f-string, Formatter::new 등) 에는 일어나지 않습니다. fmt 메서드는 모두 throws (FmtError) 이므로 호출을 try 로 표시해 전파합니다.
2.10.13. 다른 언어와의 비교
다음 표는 Rust, Python 과 비교하여 rok 의 fmt 설계가 어디에 위치하는지를 보여 줍니다.
| 측면 | Rust Display/Debug | Python __str__/__repr__ | rok |
|---|---|---|---|
| 인간 / 디버그 분리 | ✓ 트레잇 분리 | ✓ 메서드 분리 | ✓ 트레잇 분리 |
| Debug derive | #[derive(Debug)] | 자동 default | #[derive(Debug)] |
| Display derive | 불가 | 불가 (직접 구현) | 불가 |
| 진법 / 지수 | 트레잇 6 개 | format spec | spec 을 실은 Formatter |
| 동적 spec | 위치 인자 ({0:.*}) | f-string {:.{prec}} | ${...} 식 ($-sigil) |
| spec syntax | Python-like | PEP 3101 | Python-like + ${...} |
rok 이 가장 두드러지게 차별화되는 지점은, 동적 spec 삽입을 ${...} 로 시각적으로 분리하여 Python 의 {:.{prec}} 같은 중첩 괄호 모호성을 회피했다는 데 있습니다. 이 표기는 JS, Kotlin, Scala 의 $ sigil 과도 동형을 이룹니다.
2.10.14. derive 정책 요약
fmt 트레잇 가운데 derive 할 수 있는 것은 Debug 뿐입니다. Display 는 derive 대상이 아닙니다. 사용자에게 보이는 표면은 도메인마다 결정이 달라지는 영역이라 자동 도출이 어울리지 않으며, Display 의 의미 있는 default 형태도 마땅하지 않기 때문입니다. 따라서 표시가 필요한 타입에는 Display 를 직접 구현하는 편이 권장됩니다.