ぷるぷるした直方体

エンジニアの雑記です。My opinions are my ownというやつです

実質的な家賃を計算するツールをRustのwasmで作りました

こちらで公開しています - 実質家賃

3行まとめ + 画像

  • 実質的な家賃計算は面倒なのでツールにした
  • 何となく触りたかったのでRustのwasm_bindgenを使って作ってみた
  • 詳しい人はプルリクください

https://res.cloudinary.com/purucloud/image/upload/t_middle/v1597396943/blog/misc/true-rent-ss-1.png

https://res.cloudinary.com/purucloud/image/upload/t_middle/v1597392871/blog/misc/true-rent-ss-2.png

ツールの背景

家を借りる際の判断基準として大きなものに家賃があります。 一般に「家賃」と表示されている金額に加え、「共益費(管理費)」と表示されている金額を加えたものが月々の支払金額とみなされています。

しかし、実際はその他にも様々な費用が発生します。 代表的なものは礼金(敷引金)、仲介手数料です。 これらは額が大きいので特に重要です。 他にも保証会社手数料、鍵交換費用、固定清掃代金、保険料など様々な分かりにくいコストがかかります。 私は手動で計算していましたが、各部屋でこれをやるのは相当面倒でした。

これらの費用を勘案して「実質的な家賃」を求め、より良いお部屋探しを目指すのが本ツールの目的です。

Motivating Example

例えば、いわゆる「家賃」が5万円、共益費が2,000円という計算しやすいパターンを考えてみましょう。 何も付与されなければ当然毎月52,000円の支払いですが、礼金1か月、仲介手数料1か月、火災保険料が契約時に10,000円、自転車置場が契約時に3,000円、鍵交換費用が契約時に5,000円として2年間住むと、毎月の支払いは56,916円/月になります。 元々の金額と比べ4,916円/月多く払うことになります。

一方、表示上の家賃+共益費が55,000円である別の部屋では、礼金と仲介手数料は0でした。 他の条件が同じ場合、こちらは55,750円/月の支払いとなり、先程の部屋より安くなります。

このように、一見他より安い部屋が他の費用により入れ替わることはザラにあります。 探している部屋数が多いといちいち計算するのも面倒です。

使い方

サイトを見て分かるようになっている……と信じたいです。 一部機能は未開発です。興味がある方はissue/プルリクください。

  • 入力値の復元機能
  • 居住期間を変動させた際の実質家賃をグラフ表示

なお、各項目のiをクリックすると解説コメントが表示されます。 適当に邪推したものなので、詳しい人はこちらもissue/プルリクください。

技術的なところ

ここからはツールの中身の話です。 Githubで公開しているので、そちらと合わせて読むと分かりやすいかもしれません。 また、JS世界とのやり取りをとても簡単に行えるクレートwasm_bindgenドキュメントはこちらです。大半のことはここに書かれています。

本ツールではサーバーを立てる必要もないので、クライアントで完結させstatic siteとして公開することにしました。 Reactでぱぱっと書こうかとも思いましたが、今まで気になっていたものの触っていなかったRustのWebAssembly出力(wasm_bindgen)を試してみることにしました。

今回のWebアプリはフォームがメインである古典的なものです。 パフォーマンスも要らなければロジックもさして複雑ではないため、明らかにwasm向きではないですが、一応実現はできました。 が、JSの世界とのやりとりはかなり苦しいです。 昔ながらのjQueryをいじっている気分になれます。

DOMいじり

まず苦しい点として、DOMをいじるのが大変です。 web_sysクレートがHTMLのidから要素を引っ張ったり、新しい要素を生成したりするのは行ってくれるので低レベルな部分は気にしなくて良いですが、プリミティブな操作なので実際書くと苦行です。 例えば<div class="hoge">fuga</div>parent要素にぶら下げようとすると、次のようなコードになります。

let elem = document.create_element("div")?;
elem.set_attribute("class", "hoge")?;
elem.set_inner_html("fuga");
parent.append_child(&elem)?;

要はJSで一から要素を生成するのと同じですね。 ReactなどではJSXが使えるのでかなり楽ですが、残念ながらRustには現時点でそのようなフレームワークはありません。 マクロ機能があるのでいずれJSXのようなDSLを簡単に使えるクレートが登場すると信じたいですが、そもそも需要があるのかは謎……歯を食いしばって利用例を増やしていきましょう。

自分でDSLやFWを作ると夏休みが潰えて次の夏休みになってしまうので、下記のような「ちょっとだけ楽になる関数」を作ってお茶を濁してみました。 いずれにせよ、コードからどのようなDOMが生成されるかは見難いです。 設計を頑張ったらなんとかなるのかしら。

pub struct HtmlAttr<'a> {
    pub name: &'a str,
    pub value: &'a str,
}

pub fn make_tag(document: &Document, tag_name: &str,
                attr: Vec<HtmlAttr>, inner: Option<&str>,
                parent: Option<&Element>) -> Result<Element, JsValue> {
    let elem = document.create_element(tag_name)?;
    attr.into_iter().map(|a|
        elem.set_attribute(&a.name, &a.value)
    ).collect::<Result<_, _>>()?;
    if let Some(i) = inner {
        elem.set_inner_html(i);
    }
    if let Some(p) = parent {
        p.append_child(&elem)?;
    }
    return Ok(elem);
}

// 利用例
make_tag(&document, "span", vec![
    HtmlAttr { name: "class", value: "font-weight-bold" }
], Some(&self.text), Some(&label))?;

ID管理

DOMの問題に伴い、ID管理が大変です。 昔jQueryで色々やったことのある方はそれを想像してください。 うまいこと設計をして緩和することは出来ますが、ReactやAngularの楽さを考えると落差が凄いです。 こちらも今後に期待です。

イベントハンドラー

JSでは息をするように書けるイベントハンドラーですが、Rustで書くと所有権やライフタイムの都合上きっちりと書くことになります。

具体的にはclone()で必要なオブジェクトを複製しておき、Closureを作ってそれをイベントリスナーに登録した上で、作ったClosureのforget()を使ってリソースリークをさせるという方法で実現されます。

公式サンプルで見るとこのような形です。

    {
        let context = context.clone();
        let pressed = pressed.clone();
        let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
            context.begin_path();
            context.move_to(event.offset_x() as f64, event.offset_y() as f64);
            pressed.set(true);
        }) as Box<dyn FnMut(_)>);
        canvas.add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref())?;
        closure.forget();
    }

rustwasm.github.io

JSでは気にしていなかった点が明確になって面白いですね。 ところでforget()ってunsafeでは?と疑問に思ったのですが、こちらの記事にわかりやすく理由が説明されていました。

イベントハンドラーの詳細はwasm_bindgenのClosureのページもご覧ください。

rustwasm.github.io

IDEの支援

web_sysは次のような問題があり、intellij-rustおよびrlsでの補完が効きません。 これは開発する上でなかなか辛く、実際コンパイルしてエラーを見る必要があります。 IntelliJがメインなので試してはいませんが、凌ぎたい場合はVSCodeでrust-analyzerのNightlyを使うと動いた、という報告も下記issueにあるのでご確認ください。

テスト

JSの世界とやり取りせずRust内で完結する場合は、いつも通り非常に快適にテストが書けます。 wasm_bindgenが上手くやってくれるので、上手くレイヤーを切ってRustの世界でどれだけ完結させられるかが肝になりそうです。

その他四方山話

  • 英単語はUSの消費者庁のページから拾ってきました。情報がしっかりまとまっていること、ポップなデザインでフレンドリーなのに加え、詐欺への警告もちゃんとしており良いサイトでした。公式のパワー
  • 最近まとまった量のコードを書いていなかったので、モジュールを上手く切れていない感じがします
  • material-uiを使うほどでも無さそうだったので、久々にbootstrapを使ってみました
  • リリース前にはちゃんとclippyを通しましょう
    • ある程度の規模ならCircleCIなどに組み込むので自然と行うはず
  • OSSなWebアプリをリリースする前にやることは地味に多くて大変です。やらなくてもいいものも多いですが……。
    • Githubでリポジトリの公開
    • Netlifyへのデプロイ設定(Github連携)
    • カスタムドメインの設定
    • faviconの設定
    • ogpの設定
    • Google Analyticsの設定

まとめ

プリミティブな処理はwasm_bindgenweb_sysが行ってくれます。 型も細かく用意されているので、JSをそのまま書くよりは楽にWebアプリを作ることができます。 そのためRustのメリットを活かして書くことは可能だと感じましたが、いかんせんこのような古典的なWebアプリだとReactで書いた方が圧倒的に効率が良いでしょう。

WebAssembly自体の用途を考え、パフォーマンスが必要な処理、特にWebGLやWebRTCを使う場合やcanvasへの描画がほとんどな場合などではメリットが多く出てくることでしょう。 機会があればそのようなアプリも作ってみたいものです。

あと家賃はわかりやすくしてください。

追記

後々情報を頂いたり調べたりしていると、WebフレームワークのyewはJSXライクなDSLを提供していたり(Thanks to @n_s_7さん)、

github.com

Mozillaブログに書かれているDodrioがあったりと

hacks.mozilla.org

github.com

既に色々と作られていました。さすがRust。 いずれもproduction用途は想定していない模様ですが、今後が楽しみです。 この辺りを使って書き直してみたり、新しく作ってみたりしたいですね。