Any 트레잇에 대한 글은 이곳을 참고해봅시다.

HashMap을 쓰던 Rusty한 하루였습니다.

#![allow(unused)]
fn main() {
HashMap::from([(1, 2), (3, 4)]);
}

from 함수를 사용해서(또는 .into()) HashMap을 생성할 수 있었습니다.

해시 맵이 아닌 json (JavaScript Object Notation) 값을 다루려면 serde-rs/json 등으로 json 값을 다룰 수 있습니다.

필자는 단순한 코드를 원하고, 크레이트를 사용하기 원하지 않았죠. 하지만 러스트에선 자바스크립트 계열 언어(타입스크립트 등)에 존재하는 json 기능이 없습니다. (자바스크립트도 원래는 JSON이 기본 기능이 아니긴 했습니다.)

그래서 만능 매크로를 선언 해보았습니다.

#![allow(unused)]
fn main() {
macro_rules! json {
    ($($key:expr => $value:expr),*) => {{
        use std::collections::*;

        let mut map: HashMap<&str, _> = HashMap::new();
        $(
            map.insert($key, $value);
        )*

        map
    }};
}
}

key: value 으로 작성했으면 좋겠지만, 파서의 한계로 => 를 사용하였습니다.

만약 serde-rs/json 같은 크레이트와 같은 매크로를 선언하고 싶다면, 절차적 매크로를 사용해봅시다.

이 매크로는 다음과 같이 사용할 수 있습니다:

#![allow(unused)]
fn main() {
json! {
    "a" => "foo",
    "b" => "bar",
    "c" => "baz"
};
}

사실 json이 아닌 HashMap이긴 합니다. 그런데 이것은 심각한 문제가 있습니다: 매크로에서 value 타입을 &str로 단정 짓는 바람에 다른 타입을 쓸 수 없었습니다:

json! {
    "a" => "foo",
    "b" => "bar",
    "c" => 3 // mismatched types
};

Any

이러면 의미가 없으니, 필자는 Any 트레잇을 사용해보았습니다. Any 트레잇은 모든 타입을 받을 수 있습니다. Any 트레잇은 TypeId와 같이 자주 사용되지만, 이 글에선 다루지 않습니다. 좀 복잡해질 수 도 있기 때문에 json 모듈을 따로 구현해두었습니다.

#![allow(unused)]
fn main() {
pub mod json {
    #[macro_export]
    macro_rules! json {
        ($($key:expr => $value:expr),*) => {{
            use std::{any::*, collections::*};

            let mut map: HashMap<&str, Box<dyn Any>> = HashMap::new();
            $(
                map.insert($key, Box::new($value));
            )*
            map
        }};
    }
}

let json = json! {
    "a" => 1,
    "b" => "qwerty",
    "c" => json! {
        "d" => [1, 2, 3]
    }
};
}

이제 드디어 모든 타입을 받을 수 있게 되었습니다.

Any 트레잇은 컴파일 시간에 크기를 알 수 없기 때문에, Box<T>를 사용하였습니다. 이제 downcast_ref 또는 downcast_mut으로 값에 접근할 수 있습니다:

#![allow(unused)]
fn main() {
if let Some(v) = json["b"].downcast_ref::<&str>() {
    assert_eq!(*v, "qwerty");
}
}

값 가져오기

그런데 누가 json 값을 가져오는데 downcast_...같은 복잡한 함수를 쓸까요? 그런건 아무도 안씁니다. 때문에 get 헬퍼 함수 및 가변 downcast 헬퍼 함수 get_mut을 구현해보았습니다.

#![allow(unused)]
fn main() {
pub mod json {
    use std::any::*;

    pub struct JsonValue<T: Any + ?Sized>(pub T);

    impl JsonValue<dyn Any> {
        pub fn get<T: Any>(&self) -> Option<&T> {
            self.0.downcast_ref::<T>()
        }

        pub fn get_mut<T: Any>(&mut self) -> Option<&mut T> {
            self.0.downcast_mut::<T>()
        }
    }

    #[macro_export]
    macro_rules! json {
        ($($key:expr => $value:expr),*) => {{
            use std::{any::*, collections::*};

            let mut map: HashMap<&str, Box<JsonValue<dyn Any>>> = HashMap::new();
            $(
                map.insert($key, Box::new(JsonValue($value)));
            )*
            map
        }};
    }
}
}

JsonValue 구조체를 정의해주었습니다. 제네릭 TAny를 바운드하였으며, 컴파일 타임에 알 수 없기 때문에 ?Sized를 붙여주었습니다.

코드는 복잡해 보이지만, 한층 더 편리한 러스트 프로그래밍을 할 수 있습니다.

#![allow(unused)]
fn main() {
use json::*;

let json = json! {
    "a" => 1,
    "b" => "qwerty",
    "c" => json! {
        "d" => [1, 2, 3]
    }
};

if let Some(v) = json["b"].get::<&str>() {
    assert_eq!(*v, "qwerty");
};

if let Some(v) = json.get_mut("b") {
    if let Some(v) = v.get_mut::<&str>() {
        *v = "foo";

        assert_eq!(*v, "foo");
    }
};
}