rokdocs
v0.1

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 차이

두 트레잇은 목적이 다르며, 다음 표에 그 차이를 정리해 두었습니다.

측면DisplayDebug
대상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, s10 진
f고정 소수점 ({5:.2f} → 5.00)
b o x X2 / 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. 모듈 매핑

각 타입별로 어떤 트레잇이 구현되어 있는지는 다음 표에 정리됩니다.

타입DisplayDebug
정수, 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/DebugPython __str__/__repr__rok
인간 / 디버그 분리✓ 트레잇 분리✓ 메서드 분리✓ 트레잇 분리
Debug derive#[derive(Debug)]자동 default#[derive(Debug)]
Display derive불가불가 (직접 구현)불가
진법 / 지수트레잇 6 개format specspec 을 실은 Formatter
동적 spec위치 인자 ({0:.*})f-string {:.{prec}}${...} 식 ($-sigil)
spec syntaxPython-likePEP 3101Python-like + ${...}

rok 이 가장 두드러지게 차별화되는 지점은, 동적 spec 삽입을 ${...} 로 시각적으로 분리하여 Python 의 {:.{prec}} 같은 중첩 괄호 모호성을 회피했다는 데 있습니다. 이 표기는 JS, Kotlin, Scala 의 $ sigil 과도 동형을 이룹니다.

2.10.14. derive 정책 요약

fmt 트레잇 가운데 derive 할 수 있는 것은 Debug 뿐입니다. Display 는 derive 대상이 아닙니다. 사용자에게 보이는 표면은 도메인마다 결정이 달라지는 영역이라 자동 도출이 어울리지 않으며, Display 의 의미 있는 default 형태도 마땅하지 않기 때문입니다. 따라서 표시가 필요한 타입에는 Display 를 직접 구현하는 편이 권장됩니다.

GitHub에서 이 페이지 고치기