Tambourine作業メモ

主にスキル習得のためにやった作業のメモ。他人には基本的に無用のものです。

MacへSubversionを入れたい

2020年にもなって、svnを使っているプロジェクトに関わることになった。まあ、だからどうと言うことはない。 どうと言うことはないんだけども、Macでのsvnには苦い思い出がある。 かなり前のことだけど、WindowsなどとUnicodeの正規化の扱いが違い、濁点が含まれたファイルが2つに分裂するなど、悲しいことが結構起きた。 特別なパッチを当てたsvnをインストールする必要があったを記憶している。

詳細はあんまりわかってないんだけど、APFSでこの扱いが変更されたので今は問題なくなっているのかなーと推測しているが、詳細は全然わかってない。 ググってみたんだけど、まあ、もうみんなsvnなんか使っていないのか、これと言った情報が引っかからない。 そして、さらにOS標準のsvnはCatalinaでなくなっちゃったんだと。あれま。

とりあえず、brewでインストールしてみる

> brew info svn
subversion: stable 1.13.0 (bottled), HEAD
Version control system designed to be a better CVS
https://subversion.apache.org/
Not installed
From: https://github.com/Homebrew/homebrew-core/blob/master/Formula/subversion.rb
==> Dependencies
Build: openjdk ✔, pkg-config ✘, scons ✘, swig@3 ✘
Required: apr ✘, apr-util ✘, gettext ✘, lz4 ✘, openssl@1.1 ✘, perl ✘, sqlite ✘, utf8proc ✘
==> Requirements
Required: macOS is required ✔
==> Options
--HEAD
    Install HEAD version
==> Caveats
svntools have been installed to:
  /usr/local/opt/subversion/libexec

The perl bindings are located in various subdirectories of:
  /usr/local/opt/subversion/lib/perl5

You may need to link the Java bindings into the Java Extensions folder:
  sudo mkdir -p /Library/Java/Extensions
  sudo ln -s /usr/local/lib/libsvnjavahl-1.dylib /Library/Java/Extensions/libsvnjavahl-1.dylib
==> Analytics
install: 8,763 (30 days), 34,364 (90 days), 108,450 (365 days)
install-on-request: 7,790 (30 days), 29,555 (90 days), 83,898 (365 days)
build-error: 0 (30 days)

> brew install svn
Updating Homebrew...
==> Downloading https://homebrew.bintray.com/bottles/apr-1.7.0.catalina.bottle.t
==> Downloading from https://akamai.bintray.com/27/277c42fcf2f5ca298a14279d1325f
######################################################################## 100.0%
==> Downloading https://homebrew.bintray.com/bottles/openssl%401.1-1.1.1g.catali
==> Downloading from https://akamai.bintray.com/19/1926679569c6af5337de812d86f4d
######################################################################## 100.0%
==> Downloading https://homebrew.bintray.com/bottles/apr-util-1.6.1_3.catalina.b
==> Downloading from https://akamai.bintray.com/42/425955a21c3fec8e78f365cd7fc4c
######################################################################## 100.0%
==> Downloading https://homebrew.bintray.com/bottles/gettext-0.20.2_1.catalina.b
==> Downloading from https://akamai.bintray.com/71/71f4ded03e8258b5e6896eebb00d2
######################################################################## 100.0%
==> Downloading https://homebrew.bintray.com/bottles/lz4-1.9.2.catalina.bottle.t
==> Downloading from https://akamai.bintray.com/7d/7de6165d86c7a7ae01d254a5d0ea0
######################################################################## 100.0%
==> Downloading https://homebrew.bintray.com/bottles/perl-5.30.2_1.catalina.bott
==> Downloading from https://akamai.bintray.com/b2/b25dbfa43f3fea68a3acdf7f59e18
######################################################################## 100.0%
==> Downloading https://homebrew.bintray.com/bottles/readline-8.0.4.catalina.bot
==> Downloading from https://akamai.bintray.com/6a/6ae1c8e7c783f32bd22c6085caa4d
######################################################################## 100.0%
==> Downloading https://homebrew.bintray.com/bottles/sqlite-3.31.1.catalina.bott
==> Downloading from https://akamai.bintray.com/e0/e09e8c96db88178e4f47b0cdab647
######################################################################## 100.0%
==> Downloading https://homebrew.bintray.com/bottles/utf8proc-2.5.0.catalina.bot
######################################################################## 100.0%
==> Downloading https://homebrew.bintray.com/bottles/subversion-1.13.0_5.catalin
==> Downloading from https://akamai.bintray.com/0c/0c131c339c9d452563aeda9dffc0a
######################################################################## 100.0%
==> Installing dependencies for subversion: apr, openssl@1.1, apr-util, gettext, lz4, perl, readline, sqlite and utf8proc
==> Installing subversion dependency: apr
==> Pouring apr-1.7.0.catalina.bottle.tar.gz
==> Caveats
apr is keg-only, which means it was not symlinked into /usr/local,
because Apple's CLT provides apr.

If you need to have apr first in your PATH run:
  echo 'set -g fish_user_paths "/usr/local/opt/apr/bin" $fish_user_paths' >> ~/.config/fish/config.fish

==> Summary
🍺  /usr/local/Cellar/apr/1.7.0: 59 files, 1.4MB
==> Installing subversion dependency: openssl@1.1
==> Pouring openssl@1.1-1.1.1g.catalina.bottle.tar.gz
==> Caveats
A CA file has been bootstrapped using certificates from the system
keychain. To add additional certificates, place .pem files in
  /usr/local/etc/openssl@1.1/certs

and run
  /usr/local/opt/openssl@1.1/bin/c_rehash

openssl@1.1 is keg-only, which means it was not symlinked into /usr/local,
because macOS provides LibreSSL.

If you need to have openssl@1.1 first in your PATH run:
  echo 'set -g fish_user_paths "/usr/local/opt/openssl@1.1/bin" $fish_user_paths' >> ~/.config/fish/config.fish

For compilers to find openssl@1.1 you may need to set:
  set -gx LDFLAGS "-L/usr/local/opt/openssl@1.1/lib"
  set -gx CPPFLAGS "-I/usr/local/opt/openssl@1.1/include"

For pkg-config to find openssl@1.1 you may need to set:
  set -gx PKG_CONFIG_PATH "/usr/local/opt/openssl@1.1/lib/pkgconfig"

==> Summary
🍺  /usr/local/Cellar/openssl@1.1/1.1.1g: 8,059 files, 18MB
==> Installing subversion dependency: apr-util
==> Pouring apr-util-1.6.1_3.catalina.bottle.tar.gz
==> Caveats
apr-util is keg-only, which means it was not symlinked into /usr/local,
because Apple's CLT provides apr (but not apr-util).

If you need to have apr-util first in your PATH run:
  echo 'set -g fish_user_paths "/usr/local/opt/apr-util/bin" $fish_user_paths' >> ~/.config/fish/config.fish

==> Summary
🍺  /usr/local/Cellar/apr-util/1.6.1_3: 54 files, 785.7KB
==> Installing subversion dependency: gettext
==> Pouring gettext-0.20.2_1.catalina.bottle.tar.gz
🍺  /usr/local/Cellar/gettext/0.20.2_1: 1,923 files, 18.6MB
==> Installing subversion dependency: lz4
==> Pouring lz4-1.9.2.catalina.bottle.tar.gz
🍺  /usr/local/Cellar/lz4/1.9.2: 22 files, 589.5KB
==> Installing subversion dependency: perl
==> Pouring perl-5.30.2_1.catalina.bottle.tar.gz
==> Caveats
By default non-brewed cpan modules are installed to the Cellar. If you wish
for your modules to persist across updates we recommend using `local::lib`.

You can set that up like this:
  PERL_MM_OPT="INSTALL_BASE=$HOME/perl5" cpan local::lib
  echo 'eval "$(perl -I$HOME/perl5/lib/perl5 -Mlocal::lib=$HOME/perl5)"' >> ~/.config/fish/config.fish
==> Summary
🍺  /usr/local/Cellar/perl/5.30.2_1: 2,444 files, 62MB
==> Installing subversion dependency: readline
==> Pouring readline-8.0.4.catalina.bottle.tar.gz
==> Caveats
readline is keg-only, which means it was not symlinked into /usr/local,
because macOS provides BSD libedit.

For compilers to find readline you may need to set:
  set -gx LDFLAGS "-L/usr/local/opt/readline/lib"
  set -gx CPPFLAGS "-I/usr/local/opt/readline/include"

For pkg-config to find readline you may need to set:
  set -gx PKG_CONFIG_PATH "/usr/local/opt/readline/lib/pkgconfig"

==> Summary
🍺  /usr/local/Cellar/readline/8.0.4: 48 files, 1.5MB
==> Installing subversion dependency: sqlite
==> Pouring sqlite-3.31.1.catalina.bottle.tar.gz
==> Caveats
sqlite is keg-only, which means it was not symlinked into /usr/local,
because macOS already provides this software and installing another version in
parallel can cause all kinds of trouble.

If you need to have sqlite first in your PATH run:
  echo 'set -g fish_user_paths "/usr/local/opt/sqlite/bin" $fish_user_paths' >> ~/.config/fish/config.fish

For compilers to find sqlite you may need to set:
  set -gx LDFLAGS "-L/usr/local/opt/sqlite/lib"
  set -gx CPPFLAGS "-I/usr/local/opt/sqlite/include"

For pkg-config to find sqlite you may need to set:
  set -gx PKG_CONFIG_PATH "/usr/local/opt/sqlite/lib/pkgconfig"

==> Summary
🍺  /usr/local/Cellar/sqlite/3.31.1: 11 files, 4MB
==> Installing subversion dependency: utf8proc
==> Pouring utf8proc-2.5.0.catalina.bottle.tar.gz
🍺  /usr/local/Cellar/utf8proc/2.5.0: 10 files, 650.2KB
==> Installing subversion
==> Pouring subversion-1.13.0_5.catalina.bottle.tar.gz
==> Caveats
svntools have been installed to:
  /usr/local/opt/subversion/libexec

The perl bindings are located in various subdirectories of:
  /usr/local/opt/subversion/lib/perl5

You may need to link the Java bindings into the Java Extensions folder:
  sudo mkdir -p /Library/Java/Extensions
  sudo ln -s /usr/local/lib/libsvnjavahl-1.dylib /Library/Java/Extensions/libsvnjavahl-1.dylib

Bash completion has been installed to:
  /usr/local/etc/bash_completion.d
==> Summary
🍺  /usr/local/Cellar/subversion/1.13.0_5: 234 files, 30.4MB
==> Caveats
==> apr
apr is keg-only, which means it was not symlinked into /usr/local,
because Apple's CLT provides apr.

If you need to have apr first in your PATH run:
  echo 'set -g fish_user_paths "/usr/local/opt/apr/bin" $fish_user_paths' >> ~/.config/fish/config.fish

==> openssl@1.1
A CA file has been bootstrapped using certificates from the system
keychain. To add additional certificates, place .pem files in
  /usr/local/etc/openssl@1.1/certs

and run
  /usr/local/opt/openssl@1.1/bin/c_rehash

openssl@1.1 is keg-only, which means it was not symlinked into /usr/local,
because macOS provides LibreSSL.

If you need to have openssl@1.1 first in your PATH run:
  echo 'set -g fish_user_paths "/usr/local/opt/openssl@1.1/bin" $fish_user_paths' >> ~/.config/fish/config.fish

For compilers to find openssl@1.1 you may need to set:
  set -gx LDFLAGS "-L/usr/local/opt/openssl@1.1/lib"
  set -gx CPPFLAGS "-I/usr/local/opt/openssl@1.1/include"

For pkg-config to find openssl@1.1 you may need to set:
  set -gx PKG_CONFIG_PATH "/usr/local/opt/openssl@1.1/lib/pkgconfig"

==> apr-util
apr-util is keg-only, which means it was not symlinked into /usr/local,
because Apple's CLT provides apr (but not apr-util).

If you need to have apr-util first in your PATH run:
  echo 'set -g fish_user_paths "/usr/local/opt/apr-util/bin" $fish_user_paths' >> ~/.config/fish/config.fish

==> perl
By default non-brewed cpan modules are installed to the Cellar. If you wish
for your modules to persist across updates we recommend using `local::lib`.

You can set that up like this:
  PERL_MM_OPT="INSTALL_BASE=$HOME/perl5" cpan local::lib
  echo 'eval "$(perl -I$HOME/perl5/lib/perl5 -Mlocal::lib=$HOME/perl5)"' >> ~/.config/fish/config.fish
==> readline
readline is keg-only, which means it was not symlinked into /usr/local,
because macOS provides BSD libedit.

For compilers to find readline you may need to set:
  set -gx LDFLAGS "-L/usr/local/opt/readline/lib"
  set -gx CPPFLAGS "-I/usr/local/opt/readline/include"

For pkg-config to find readline you may need to set:
  set -gx PKG_CONFIG_PATH "/usr/local/opt/readline/lib/pkgconfig"

==> sqlite
sqlite is keg-only, which means it was not symlinked into /usr/local,
because macOS already provides this software and installing another version in
parallel can cause all kinds of trouble.

If you need to have sqlite first in your PATH run:
  echo 'set -g fish_user_paths "/usr/local/opt/sqlite/bin" $fish_user_paths' >> ~/.config/fish/config.fish

For compilers to find sqlite you may need to set:
  set -gx LDFLAGS "-L/usr/local/opt/sqlite/lib"
  set -gx CPPFLAGS "-I/usr/local/opt/sqlite/include"

For pkg-config to find sqlite you may need to set:
  set -gx PKG_CONFIG_PATH "/usr/local/opt/sqlite/lib/pkgconfig"

==> subversion
svntools have been installed to:
  /usr/local/opt/subversion/libexec

The perl bindings are located in various subdirectories of:
  /usr/local/opt/subversion/lib/perl5

You may need to link the Java bindings into the Java Extensions folder:
  sudo mkdir -p /Library/Java/Extensions
  sudo ln -s /usr/local/lib/libsvnjavahl-1.dylib /Library/Java/Extensions/libsvnjavahl-1.dylib

Bash completion has been installed to:
  /usr/local/etc/bash_completion.d

さて、これでプロジェクトの適当なドキュメントをチェックアウトし、ファイル名に濁点が入っている(「ベースライン」という言葉が含まれていた)Excelファイルを開き、シートを1つ増やして保存。svn statusしてみた。ちゃんと変更が捉えられていた。昔はこれが、ファイルが2つに増えて、1つは新規と認識されていたのだ。つまり、ファイル名が「へ゛ースライン」と「ベースライン」の2つになっちゃったわけだ。見た目は表示するときに「へ゛」を「べ」にするからおんなじなんだけど。

とりあえず、大丈夫そうで良かった。

ケント・ベックの「テスト駆動開発」の写経をRustでやってみる(17)

元のJavaのテストではfiveBucksはMoneyではなくExpressionだった。ここから派生する汎用化を15章では行っている。例えば、plusメソッドがExpressionを受け付けるようにしたり、Expressionにplusメソッドを追加したりだ。これは結局、何がしたいのかというと、今のままではこういうことが出来ないぞと言うことだ。

#[test]
fn test_continuous_adding() {
    let bank = Bank::new();
    let one = Money::dollar(1.);
    let two = Money::dollar(2.);
    let three = Money::dollar(3.);

    // 1 + (2 + 3)
    let result = bank.reduce(&one.add(&two.add(&three)), USD);
    // (1 + 2) + 3
    let result2 = bank.reduce(&(one.add(&two)).add(&three), USD);
    assert_eq!(Money::dollar(6.), result1.unwrap());
    assert_eq!(Money::dollar(6.), result2.unwrap());
}

つまり、Sumの足す数、足される数のどちらにもMoneyだけじゃなく、Sumも入れられる様にしたい。そのために、MoneyもSumもExpressionを実装して、Expressionを入れられる様にしたいぞというわけだ。一種の再帰的なデータ構造を取りたいわけ。

Javaなら単にSumがExpressionを保持するよっていうので構わないんだけども、Rustではなかなか難しい。というのも、Expressionはトレイトなので参照しか保持できないから。

ちょっとやってみよう。Sumの定義はこうだった。

pub struct Sum {
    augend: Money,
    addend: Money,
}

これを、Expressionのポインタを持つように変える。

pub struct Sum {
    augend: Box<Expression>,
    addend: Box<Expression>,
}

これは問題ない。

Sumの定義が変わったので、Money.add()の実装も変える必要がある。以前はこうだった。

impl Money {
    pub fn add(&self, other: &Money) -> Sum {
        Sum {
            augend: self.clone(),
            addend: other.clone(),
        }
    }

//...

}

otherをExpressionの参照に・・・しかし、Sumが渡ってきたときにはcloneできないし・・・単にBoxに詰めるとライフタイムがおかしい。

impl Money {
    pub fn add<T: Expression>(&self, other: &T) -> Sum {
        Sum {
            augend: Box::new(*self),
            addend: Box::new(*other),
        }
    }
}
error[E0310]: the parameter type `T` may not live long enough
  --> src/main.rs:59:21
   |
56 |     pub fn add<T: Expression>(&self, other: &T) -> Sum {
   |                -- help: consider adding an explicit lifetime bound `T: 'static`...
...
59 |             addend: Box::new(*other),
   |                     ^^^^^^^^^^^^^^^^
   |
note: ...so that the type `T` will meet its required lifetime bounds
  --> src/main.rs:59:21
   |
59 |             addend: Box::new(*other),
   |                     ^^^^^^^^^^^^^^^^

何か、非常にやりづらい・・・。

このような再帰型のデータ構造を作るときには、Rustにはお決まりのパターンがある。Enumを使うのだ。

Sumには、Moneyが格納されるかも知れないし、別のSumが格納されるかもしれない。Sumを処理する場合には、どちらが格納されているかによって、処理を変えなくてはいけない。

これをJavaではポリモーフィズムで表現している。Cであれば、ポインタを格納してキャストして使用する。Rustでは、Enumを使うのだ。

つまり、ExpressionとはMoneyかSumのことだとするならば、そう定義してしまおう。

pub enum Expression {
    Value(Money),
    Operation(Box<Sum>),
}

ここで、Enumの名前やメンバーの名前をどうするかは大分悩んだ。しっくりこない。いろいろと例を見ないとなんとも言えないかなあという気がする。

とにかく、MoneyかSumのポインタ(Sum自体はサイズが決まらないから、ポインタしか扱えないのだ)を取れると。

Sumの定義は素直にこれを使って行う。

pub struct Sum {
    augend: Expression,
    addend: Expression,
}

これを返すMoney.add()を考える。しかし、Moneyの実体を格納するんだとすれば、add()がSumを返すと、そこに元のMoneyは移動したことになる。なんだかよくわからない気持ちになったので、インスタンスメソッドはやめてMoney::add()のような関数に変えよう。しかし、引数にはExpressionを格納するのだから、Money::add()ではなくて、Expression::add()が適切だろう。

impl Expression {
    pub fn add(one: Expression, other: Expression) -> Sum {
        Sum {
            augend: one,
            addend: other,
        }
    }
}

渡された2つのExpressionをそのままSumにつめて返しているだけ。シンプルだ。

さあ、今回のクライマックスはSum.reduce()だ。ExpressionがMoneyならそれでよし。Sumが入ってたら再帰的にreduceしてやる必要がある。こんな感じ。

impl Exchangable for Sum {
    fn reduce(&self, to: Currency, bank: &Bank) -> Result<Money, &'static str> {
        let augend_reduce = match self.augend {
            Expression::Value(ref m) => m.reduce(to, bank)?.amount,
            Expression::Operation(ref s) => s.reduce(to, bank)?.amount,
        };
        let addend_reduce = match self.addend {
            Expression::Value(ref m) => m.reduce(to, bank)?.amount,
            Expression::Operation(ref s) => s.reduce(to, bank)?.amount,
        };
        Ok(Money {
            amount: augend_reduce + addend_reduce,
            currency: to,
        })
    }
}

パターンマッチがあるので見慣れないと辛いけど、やっていることは非常にシンプルだね。

これで、修正はOK。では、テストを直していきながらこれで良かったのか確認していこう。

足し算は、以前はこうだった

let five = Money::dollar(5.);
let sum = five.add(&five)

こうなった。

let five =Money::dollar(5.);
let sum = Expression::add(Expression::Value(five), Expression::Value(five));

これはエラーになる。 fiveを2つ詰めようと思うと、もともと1つなのでどこかで増やさないといけない。移動しちゃってるからね。かねてからMoneyはValue ObjectなのでCopyトレイトを実装してもいいだろうと思っていたので、このタイミングで自動実装を追加することにした。

それにしても、長い。うん、Money::add()も作ろう。

impl Money {
    // ...

    pub fn add(one: Money, other: Money) -> Sum {
        Expression::add(Expression::Value(one), Expression::Value(other))
    }
}

引数の型の違いによるオーバーロードが出来ないのは、ちょっと不便に感じることもあるね。

というわけで、これで良くなった。

let five = Money::dollar(5.);
let sum = Money::add(five, five)

ただし、本来やりたかったテストである1 + (2 + 3)はこうなってしまった。

#[test]
fn test_continuous_adding() {
    let bank = Bank::new();
    let one = Money::dollar(1.);
    let two = Money::dollar(2.);
    let three = Money::dollar(3.);

    // 1 + (2 + 3)
    let result1 = bank.reduce(
        &Expression::add(
            Expression::Value(one),
            Expression::Operation(Box::new(Money::add(two, three))),
        ),
        USD,
    );
    // (1 + 2) + 3
    let result2 = bank.reduce(
        &Expression::add(
            Expression::Operation(Box::new(Money::add(one, two))),
            Expression::Value(three),
        ),
        USD,
    );
    assert_eq!(Money::dollar(6.), result1.unwrap());
    assert_eq!(Money::dollar(6.), result2.unwrap());
}

ちょっと見栄えはよろしくない・・・。まあ、テストは通ったのでよしとしよう。

最後に、この段階でのソースコード全行を載せておく。

use std::collections::HashMap;

fn main() {}

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Hash)]
pub enum Currency {
    CHF,
    USD,
}

impl Currency {
    fn get_pair(one: Currency, another: Currency) -> (Currency, Currency) {
        if one < another {
            (one, another)
        } else {
            (another, one)
        }
    }
}

use self::Currency::*;

pub trait Exchangable {
    fn reduce(&self, to: Currency, bank: &Bank) -> Result<Money, &'static str>;
}

pub enum Expression {
    Value(Money),
    Operation(Box<Sum>),
}

impl Expression {
    pub fn add(one: Expression, other: Expression) -> Sum {
        Sum {
            augend: one,
            addend: other,
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub struct Money {
    amount: f64,
    currency: Currency,
}

impl Money {
    pub fn dollar(f: f64) -> Money {
        Money {
            amount: f,
            currency: USD,
        }
    }

    pub fn franc(f: f64) -> Money {
        Money {
            amount: f,
            currency: CHF,
        }
    }

    pub fn times(&self, multiplier: f64) -> Money {
        Money {
            amount: self.amount * multiplier,
            currency: self.currency,
        }
    }

    pub fn add(one: Money, other: Money) -> Sum {
        Expression::add(Expression::Value(one), Expression::Value(other))
    }
}

impl PartialEq for Money {
    fn eq(&self, other: &Money) -> bool {
        if self.currency != other.currency {
            false
        } else {
            self.amount == other.amount
        }
    }
}

impl Exchangable for Money {
    fn reduce(&self, to: Currency, bank: &Bank) -> Result<Money, &'static str> {
        let rate = bank.get_rate(self.currency, to);
        match rate {
            Some(r) => Ok(Money {
                amount: self.amount / r,
                currency: to,
            }),
            None => Err("Never set rate these currencies"),
        }
    }
}

pub struct Sum {
    augend: Expression,
    addend: Expression,
}
impl Exchangable for Sum {
    fn reduce(&self, to: Currency, bank: &Bank) -> Result<Money, &'static str> {
        let augend_reduce = match self.augend {
            Expression::Value(ref m) => m.reduce(to, bank)?.amount,
            Expression::Operation(ref s) => s.reduce(to, bank)?.amount,
        };
        let addend_reduce = match self.addend {
            Expression::Value(ref m) => m.reduce(to, bank)?.amount,
            Expression::Operation(ref s) => s.reduce(to, bank)?.amount,
        };
        Ok(Money {
            amount: augend_reduce + addend_reduce,
            currency: to,
        })
    }
}

pub struct Bank {
    rate: HashMap<(Currency, Currency), f64>,
}

impl Bank {
    pub fn new() -> Bank {
        Bank {
            rate: HashMap::new(),
        }
    }

    pub fn add_rate(&mut self, from: Currency, to: Currency, r: f64) {
        let pair = Currency::get_pair(from, to);
        self.rate
            .insert(pair, if pair.0 == from { r } else { 1.0 / r });
    }

    pub fn get_rate(&self, from: Currency, to: Currency) -> Option<f64> {
        if from == to {
            return Some(1.);
        }

        let pair = Currency::get_pair(from, to);
        self.rate
            .get(&pair)
            .map(|i| if pair.0 == from { *i } else { 1.0 / *i })
    }

    pub fn reduce<T: Exchangable>(&self, source: &T, to: Currency) -> Result<Money, &str> {
        source.reduce(to, self)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_multiplication() {
        let five_d = Money::dollar(5.);
        assert_eq!(Money::dollar(10.), five_d.times(2.));
        assert_eq!(Money::dollar(15.), five_d.times(3.));
    }

    #[test]
    fn test_equality() {
        assert_eq!(Money::dollar(5.), Money::dollar(5.));
        assert_ne!(Money::dollar(5.), Money::dollar(6.));
        assert_eq!(Money::franc(7.), Money::franc(7.));
        assert_ne!(Money::franc(7.), Money::franc(6.));

        // 通貨が違うので、一致しない。
        assert_ne!(Money::franc(5.), Money::dollar(5.));
    }

    #[test]
    fn test_currency() {
        assert_eq!(USD, Money::dollar(1.).currency);
        assert_eq!(CHF, Money::franc(1.).currency);
    }

    #[test]
    fn test_simple_addition() {
        let five = Money::dollar(5.);
        let sum = Money::add(five, five);
        let bank = Bank::new();
        let reduced: Money = bank.reduce(&sum, USD).unwrap();
        assert_eq!(Money::dollar(10.), reduced);
    }

    #[test]
    fn test_add_returns_sum() {
        let five = Money::dollar(5.);
        let sum = Money::add(five, five);
        assert_eq!(
            five,
            match sum.augend {
                Expression::Value(m) => m,
                _ => panic!("It is not correct"),
            }
        );
        assert_eq!(
            five,
            match sum.addend {
                Expression::Value(m) => m,
                _ => panic!("It is not correct"),
            }
        );
    }

    #[test]
    fn test_reduce_sum() {
        let sum = Sum {
            augend: Expression::Value(Money::dollar(3.)),
            addend: Expression::Value(Money::dollar(4.)),
        };
        let bank = Bank::new();
        let result = bank.reduce(&sum, USD).unwrap();
        assert_eq!(Money::dollar(7.), result);
    }

    #[test]
    fn test_reduce_money_different_currency() {
        let mut bank = Bank::new();
        bank.add_rate(CHF, USD, 2.0);
        let result = bank.reduce(&Money::franc(2.0), USD).unwrap();
        assert_eq!(Money::dollar(1.0), result);

        // ドルをドルに変更する
        let result2 = bank.reduce(&Money::dollar(2.0), USD).unwrap();
        assert_eq!(Money::dollar(2.), result2);
    }

    #[test]
    fn test_get_rate() {
        let mut bank = Bank::new();

        // 未登録の場合にはNoneが返る
        assert!(match bank.get_rate(CHF, USD) {
            None => true,
            _ => false,
        });

        // 登録したらそれが取れる
        bank.add_rate(CHF, USD, 2.0);
        assert_eq!(2.0, bank.get_rate(CHF, USD).unwrap());

        // 逆順に取得したら、逆数が取れる
        assert_eq!(0.5, bank.get_rate(USD, CHF).unwrap());
    }

    #[test]
    fn test_get_pair() {
        assert_eq!((CHF, USD), Currency::get_pair(CHF, USD));
        assert_eq!((CHF, USD), Currency::get_pair(USD, CHF));
    }

    #[test]
    fn test_add_rate() {
        let mut bank = Bank::new();

        // 初回登録
        bank.add_rate(CHF, USD, 2.0);
        assert_eq!(2.0, bank.rate[&(CHF, USD)]);

        // 上書き登録すると書き換えられる
        bank.add_rate(CHF, USD, 4.0);
        assert_eq!(4.0, bank.rate[&(CHF, USD)]);

        // 逆数でも登録できる
        bank.add_rate(USD, CHF, 4.0);
        assert_eq!(0.25, bank.rate[&(CHF, USD)]);
    }

    #[test]
    fn test_mixed_addition() {
        let five_bucks = Money::dollar(5.);
        let ten_francs = Money::franc(10.);
        let mut bank = Bank::new();
        bank.add_rate(CHF, USD, 2.0);
        let result = bank.reduce(&Money::add(five_bucks, ten_francs), USD);
        assert_eq!(Money::dollar(10.), result.unwrap());
    }

    #[test]
    fn test_continuous_adding() {
        let bank = Bank::new();
        let one = Money::dollar(1.);
        let two = Money::dollar(2.);
        let three = Money::dollar(3.);

        // 1 + (2 + 3)
        let result1 = bank.reduce(
            &Expression::add(
                Expression::Value(one),
                Expression::Operation(Box::new(Money::add(two, three))),
            ),
            USD,
        );
        // (1 + 2) + 3
        let result2 = bank.reduce(
            &Expression::add(
                Expression::Operation(Box::new(Money::add(one, two))),
                Expression::Value(three),
            ),
            USD,
        );
        assert_eq!(Money::dollar(6.), result1.unwrap());
        assert_eq!(Money::dollar(6.), result2.unwrap());
    }
}

ケント・ベックの「テスト駆動開発」の写経をRustでやってみる(16)

15章。ついに「5ドル+10フラン」のテストを書くぞと、ケント・ベック先生は意気込んでいる。こんなテストを書いている。

@Test
public void testMixedAddition() {
    Expression fiveBucks = Money.dollar(5);
    Expression tenFrancs = Money.franc(10);
    Bank bank = new Bank();
    bank.addRate("CHF", "USD", 2);
    Money result = bank.reduce(fiveBucks.plus(tenFrancs), "USD");
    assertEquals(Money.dollar(10), result);
}

冒頭に上げられているテストを素直にRustにすると、こんな感じだ。これまで作ってきたものが一堂に会した。

#[test]
fn test_mixed_addition() {
    let five_bucks = Money::dollar(5.);
    let ten_francs = Money::franc(10.);
    let bank = Bank::new();
    bank.add_rate(CHF, USD, 2.0);
    let result = bank.reduce(five_bucks.add(ten_francs), USD);
    assert_eq!(Money::dollar(10.), result);
}

書いている私としても、特に違和感がない。動いてしかるべきという感じがするんだけど、コンパイルエラーになる。出ているエラーは3つ。まず、1つめ。

error[E0308]: mismatched types
   --> src/main.rs:248:49
    |
248 |         let result = bank.reduce(five_bucks.add(ten_francs), USD);
    |                                                 ^^^^^^^^^^
    |                                                 |
    |                                                 expected `&Money`, found struct `Money`
    |                                                 help: consider borrowing here: `&ten_francs`

addに渡すのは参照でなければならない。そうだった。そう作った。 ただ、Moneyは本質的にValue ObjectなのでCopyトレイトを実装して実体渡しが出来るようにしてもいいのかもしれない。

2つめ。上の実体渡し案は、こっちをみるとイマイチに感じる。

error[E0308]: mismatched types
   --> src/main.rs:248:34
    |
248 |         let result = bank.reduce(five_bucks.add(ten_francs), USD);
    |                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^
    |                                  |
    |                                  expected reference, found struct `Sum`
    |                                  help: consider borrowing here: `&five_bucks.add(ten_francs)`
    |
    = note: expected reference `&_`
                  found struct `Sum`

同じようにback.reduceにも参照を渡さなければならない。しかも、こっちはExpressionの参照が期待されているので実体は渡せない(コンパイル時にサイズが決まらないから)。だとしたら、addの参照渡しでいいか・・・。この辺りは何が普通なのか、Rustの経験が不足していてわからない。

3つめ。これも自分で入れた変更を忘れている。

error[E0308]: mismatched types
   --> src/main.rs:249:9
    |
249 |         assert_eq!(Money::dollar(10.), result);
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `Money`, found enum `std::result::Result`
    |
    = note: expected struct `Money`
                 found enum `std::result::Result<Money, &str>`
    = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)

そうだ。いまや、reduceはResultを返すのだった。

というわけで、テストを直した。

#[test]
fn test_mixed_addition() {
    let five_bucks = Money::dollar(5.);
    let ten_francs = Money::franc(10.);
    let bank = Bank::new();
    bank.add_rate(CHF, USD, 2.0);
    let result = bank.reduce(&five_bucks.add(&ten_francs), USD);
    assert_eq!(Money::dollar(10.), result.unwrap());
}

どうだっ!

error[E0596]: cannot borrow `bank` as mutable, as it is not declared as mutable
   --> src/main.rs:247:9
    |
246 |         let bank = Bank::new();
    |             ---- help: consider changing this to be mutable: `mut bank`
247 |         bank.add_rate(CHF, USD, 2.0);
    |         ^^^^ cannot borrow as mutable

はいはい、はいはい。bankはmutじゃないとダメだった。しかし、一度、レートをセットしたあとは、immutableなbankを使いたい感じはある。どうしたら・・・?

それは後で考えることにして、let mut bankのようにmutableな変数を取ることにする。これでコンパイルは通った。

---- tests::test_mixed_addition stdout ----
thread 'tests::test_mixed_addition' panicked at 'assertion failed: `(left == right)`
  left: `Money { amount: 10.0, currency: USD }`,
 right: `Money { amount: 15.0, currency: USD }`', src/main.rs:249:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

テストは失敗。sumをちゃんと実装していないので当然だ。今はこうなってる。

impl Expression for Sum {
    fn reduce(&self, to: Currency, _: &Bank) -> Result<Money, &'static str> {
        Ok(Money {
            amount: self.augend.amount + self.addend.amount,
            currency: to,
        })
    }
}

augendとaddendの両方をあらかじめ換算しておく必要がある。こんなイメージだ。

fn reduce(&self, to: Currency, bank: &Bank) -> Result<Money, &'static str> {
    Ok(Money {
        amount: self.augend.reduce(to, bank).amount + self.addend.reduce(to, bank).amount,
        currency: to,
    })
}

考え方はこれで良いのだけれど、Money.reduceはMoneyではなく、Resultを返すので、エラーになる。

error[E0609]: no field `amount` on type `std::result::Result<Money, &'static str>`
  --> src/main.rs:94:50
   |
94 |             amount: self.augend.reduce(to, bank).amount + self.addend.reduce(to, bank).amount,
   |                                                  ^^^^^^

ここで、Rustのお素敵機能として、Resultを返す関数の中で呼び出した関数がResultを返す時(ややこしいけど、ついてきてる?)、呼び出した関数がErrの方を返したときにはそのままそのErrを関数の呼び出し値としてリターンするという機能がある。

日本語で説明するのは無理っぽい。

えーっと、つまり、丁寧にやるならば、さっきのコードはパターンマッチを使ってResultを場合分けして処理しなくてはいけない。こんな感じだ。

fn reduce(&self, to: Currency, bank: &Bank) -> Result<Money, &'static str> {
    let augend_reduce = match self.augend.reduce(to, bank) {
        Ok(m) => m,
        Err(e) => return Err(e),
    };

    let addend_reduce = match self.addend.reduce(to, bank) {
        Ok(m) => m,
        Err(e) => return Err(e),
    };

    Ok(Money {
        amount: augend_reduce.amount + addend_reduce.amount,
        currency: to,
    })
}

だいぶまだるっこしい。しかし、?演算子を使うと、上と同じコードを以下のように書ける。

fn reduce(&self, to: Currency, bank: &Bank) -> Result<Money, &'static str> {
    Ok(Money {
        amount: self.augend.reduce(to, bank)?.amount + self.addend.reduce(to, bank)?.amount,
        currency: to,
    })
}

超便利。これはナイスになって蘇った(死んでないか)Javaの検査例外のように見える。

これでテストはバッチリ通った。

ただし、元の本では、15章はもうちょっと続く。

ケント・ベックの「テスト駆動開発」の写経をRustでやってみる(15)

さて、もうちょっとコードをキレイにしてから先に進みたい。

今、通貨は文字列で定義されている。Java版だとStringだ。私のRustのコードだと、ここが&'static strになっている。

Rustの&strはヒープに確保された領域に書き込まれたUTF-8シーケンスに対する参照、というかポインタだ。そして、&'static strは文字列リテラル、つまりプログラム開始時に特別な領域に作られて、プログラム中で共有される文字列に対するポインタなので、プログラム中のどの"USD"も厳密に同じアドレスを指すポインタになる。

そして、メソッドの引数の型が&'static strしか受け付けないということは、そのように絶対に無くならない領域に対するポインタしか受け付けませんということになる。非常にRustらしくて面白い。面白いんだけど、&'static strは長いし、ずっと同じアドレスということはもうそれは文字列である必要は全然ないんで、ちゃんと列挙型にしてやるべきだろう。タイプミスもちゃんとコンパイルエラーになるし。

というわけで、サクッと定義する。

enum Currency {
    CHF,
    USD,
}

Rustの列挙型はいろんなことが出来て面白い。JavaでいうところのOptionalなんかも列挙型で実現されている。けど、まあ、この例は、ごく普通だ。

さて、これで通貨を扱っていたところを全部置き換える。するといろいろ怒られる。

まず、そのままだと大小比較が出来ない。

error[E0369]: binary operation `<` cannot be applied to type `Currency`
  --> src/main.rs:14:16
   |
14 |         if one < another {
   |            --- ^ ------- Currency
   |            |
   |            Currency
   |
   = note: an implementation of `std::cmp::PartialOrd` might be missing for `Currency`

Debugトレイトも実装されてないので、MoneyのDebugの自動実装がエラーになる。

error[E0277]: `Currency` doesn't implement `std::fmt::Debug`
  --> src/main.rs:31:5
   |
31 |     currency: Currency,
   |     ^^^^^^^^^^^^^^^^^^ `Currency` cannot be formatted using `{:?}`
   |
   = help: the trait `std::fmt::Debug` is not implemented for `Currency`
   = note: add `#[derive(Debug)]` or manually implement `std::fmt::Debug`
   = note: required because of the requirements on the impl of `std::fmt::Debug` for `&Currency`
   = note: required for the cast to the object type `dyn std::fmt::Debug`

Cloneも実装されていないので、MoneyのCloneの自動実装もエラーになる。

error[E0277]: the trait bound `Currency: std::clone::Clone` is not satisfied
  --> src/main.rs:31:5
   |
31 |     currency: Currency,
   |     ^^^^^^^^^^^^^^^^^^ the trait `std::clone::Clone` is not implemented for `Currency`
   |
   = note: required by `std::clone::Clone::clone`

==での比較も出来ない。

error[E0369]: binary operation `!=` cannot be applied to type `Currency`
  --> src/main.rs:66:26
   |
66 |         if self.currency != other.currency {
   |            ------------- ^^ -------------- Currency
   |            |
   |            Currency
   |
   = note: an implementation of `std::cmp::PartialEq` might be missing for `Currency`

EqとHashが実装されていないので、ハッシュのキーに出来ない

error[E0277]: the trait bound `Currency: std::cmp::Eq` is not satisfied
   --> src/main.rs:107:19
    |
107 |             rate: HashMap::new(),
    |                   ^^^^^^^^^^^^ the trait `std::cmp::Eq` is not implemented for `Currency`
    |
    = note: required because of the requirements on the impl of `std::cmp::Eq` for `(Currency, Currency)`
    = note: required by `std::collections::HashMap::<K, V>::new`


error[E0277]: the trait bound `Currency: std::hash::Hash` is not satisfied
   --> src/main.rs:107:19
    |
107 |             rate: HashMap::new(),
    |                   ^^^^^^^^^^^^ the trait `std::hash::Hash` is not implemented for `Currency`
    |
    = note: required because of the requirements on the impl of `std::hash::Hash` for `(Currency, Currency)`
    = note: required by `std::collections::HashMap::<K, V>::new`

はいはい、すいませんね・・・ということで、まとめて自動実装をお願いする。

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Hash)]
pub enum Currency {
    CHF,
    USD,
}

これでOK。ラクチン。

さらに、enumにはメソッドも定義できる。「通貨レートの表示の際にFromとToをどちらにするのか」は金融機関ではなく、通貨の定義で決まっているべきなので、Bank::get_pairはCurrencyに移動しよう。

impl Currency {
    fn get_pair(one: Currency, another: Currency) -> (Currency, Currency) {
        if one < another {
            (one, another)
        } else {
            (another, one)
        }
    }
}

これで、通貨は文字列では無く、こんな感じで指定できるようになった。

bank.add_rate(Currency::CHF, Currency::USD, 2.0);

見づらい。useしてやれば修飾しなくても良くなる。

use self::Currency::*;

これでOKになる。素晴らしい。まあ、やるべきでないときも多いと思うけど。

bank.add_rate(CHF, USD, 2.0);

これでだいぶ見やすくなった。最後に今の時点のコードを全部上げておく。次の章は15章、いよいよ「5ドル+10フラン=10ドル」ができるようになるときが来る、らしい。

use std::collections::HashMap;

fn main() {}

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Hash)]
pub enum Currency {
    CHF,
    USD,
}

impl Currency {
    fn get_pair(one: Currency, another: Currency) -> (Currency, Currency) {
        if one < another {
            (one, another)
        } else {
            (another, one)
        }
    }
}

use self::Currency::*;

pub trait Expression {
    fn reduce(&self, to: Currency, bank: &Bank) -> Result<Money, &'static str>;
}

#[derive(Debug, Clone)]
pub struct Money {
    amount: f64,
    currency: Currency,
}

impl Money {
    pub fn dollar(f: f64) -> Money {
        Money {
            amount: f,
            currency: USD,
        }
    }

    pub fn franc(f: f64) -> Money {
        Money {
            amount: f,
            currency: CHF,
        }
    }

    pub fn times(&self, multiplier: f64) -> Money {
        Money {
            amount: self.amount * multiplier,
            currency: self.currency,
        }
    }

    pub fn add(&self, other: &Money) -> Sum {
        Sum {
            augend: self.clone(),
            addend: other.clone(),
        }
    }
}

impl PartialEq for Money {
    fn eq(&self, other: &Money) -> bool {
        if self.currency != other.currency {
            false
        } else {
            self.amount == other.amount
        }
    }
}

impl Expression for Money {
    fn reduce(&self, to: Currency, bank: &Bank) -> Result<Money, &'static str> {
        let rate = bank.get_rate(self.currency, to);
        match rate {
            Some(r) => Ok(Money {
                amount: self.amount / r,
                currency: to,
            }),
            None => Err("Never set rate these currencies"),
        }
    }
}

pub struct Sum {
    augend: Money,
    addend: Money,
}
impl Expression for Sum {
    fn reduce(&self, to: Currency, _: &Bank) -> Result<Money, &'static str> {
        Ok(Money {
            amount: self.augend.amount + self.addend.amount,
            currency: to,
        })
    }
}

pub struct Bank {
    rate: HashMap<(Currency, Currency), f64>,
}

impl Bank {
    pub fn new() -> Bank {
        Bank {
            rate: HashMap::new(),
        }
    }

    pub fn add_rate(&mut self, from: Currency, to: Currency, r: f64) {
        let pair = Currency::get_pair(from, to);
        self.rate
            .insert(pair, if pair.0 == from { r } else { 1.0 / r });
    }

    pub fn get_rate(&self, from: Currency, to: Currency) -> Option<f64> {
        if from == to {
            return Some(1.);
        }

        let pair = Currency::get_pair(from, to);
        self.rate
            .get(&pair)
            .map(|i| if pair.0 == from { *i } else { 1.0 / *i })
    }

    pub fn reduce<T: Expression>(&self, source: &T, to: Currency) -> Result<Money, &str> {
        source.reduce(to, self)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_multiplication() {
        let five_d = Money::dollar(5.);
        assert_eq!(Money::dollar(10.), five_d.times(2.));
        assert_eq!(Money::dollar(15.), five_d.times(3.));
    }

    #[test]
    fn test_equality() {
        assert_eq!(Money::dollar(5.), Money::dollar(5.));
        assert_ne!(Money::dollar(5.), Money::dollar(6.));
        assert_eq!(Money::franc(7.), Money::franc(7.));
        assert_ne!(Money::franc(7.), Money::franc(6.));

        // 通貨が違うので、一致しない。
        assert_ne!(Money::franc(5.), Money::dollar(5.));
    }

    #[test]
    fn test_currency() {
        assert_eq!(USD, Money::dollar(1.).currency);
        assert_eq!(CHF, Money::franc(1.).currency);
    }

    #[test]
    fn test_simple_addition() {
        let five = Money::dollar(5.);
        let sum = five.add(&five);
        let bank = Bank::new();
        let reduced: Money = bank.reduce(&sum, USD).unwrap();
        assert_eq!(Money::dollar(10.), reduced);
    }

    #[test]
    fn test_add_returns_sum() {
        let five = Money::dollar(5.);
        let sum = five.add(&five);
        assert_eq!(five, sum.augend);
        assert_eq!(five, sum.addend);
    }

    #[test]
    fn test_reduce_sum() {
        let sum = Sum {
            augend: Money::dollar(3.),
            addend: Money::dollar(4.),
        };
        let bank = Bank::new();
        let result = bank.reduce(&sum, USD).unwrap();
        assert_eq!(Money::dollar(7.), result);
    }

    #[test]
    fn test_reduce_money_different_currency() {
        let mut bank = Bank::new();
        bank.add_rate(CHF, USD, 2.0);
        let result = bank.reduce(&Money::franc(2.0), USD).unwrap();
        assert_eq!(Money::dollar(1.0), result);

        // ドルをドルに変更する
        let result2 = bank.reduce(&Money::dollar(2.0), USD).unwrap();
        assert_eq!(Money::dollar(2.), result2);
    }

    #[test]
    fn test_get_rate() {
        let mut bank = Bank::new();

        // 未登録の場合にはNoneが返る
        assert!(match bank.get_rate(CHF, USD) {
            None => true,
            _ => false,
        });

        // 登録したらそれが取れる
        bank.add_rate(CHF, USD, 2.0);
        assert_eq!(2.0, bank.get_rate(CHF, USD).unwrap());

        // 逆順に取得したら、逆数が取れる
        assert_eq!(0.5, bank.get_rate(USD, CHF).unwrap());
    }

    #[test]
    fn test_get_pair() {
        assert_eq!((CHF, USD), Currency::get_pair(CHF, USD));
        assert_eq!((CHF, USD), Currency::get_pair(USD, CHF));
    }

    #[test]
    fn test_add_rate() {
        let mut bank = Bank::new();

        // 初回登録
        bank.add_rate(CHF, USD, 2.0);
        assert_eq!(2.0, bank.rate[&(CHF, USD)]);

        // 上書き登録すると書き換えられる
        bank.add_rate(CHF, USD, 4.0);
        assert_eq!(4.0, bank.rate[&(CHF, USD)]);

        // 逆数でも登録できる
        bank.add_rate(USD, CHF, 4.0);
        assert_eq!(0.25, bank.rate[&(CHF, USD)]);
    }
}

ケント・ベックの「テスト駆動開発」の写経をRustでやってみる(14)

さて、ここでちょっと立ち止まって、コードを眺めてみよう。 コード全体は今でも100行ちょっとに過ぎない。

#[derive(Debug, Clone)]
pub struct Money {
    amount: f64,
    currency: &'static str,
}

impl Money {
    pub fn dollar(f: f64) -> Money {
        Money {
            amount: f,
            currency: "USD",
        }
    }

    pub fn franc(f: f64) -> Money {
        Money {
            amount: f,
            currency: "CHF",
        }
    }

    pub fn times(&self, multiplier: f64) -> Money {
        Money {
            amount: self.amount * multiplier,
            currency: self.currency,
        }
    }

    pub fn add(&self, other: &Money) -> Box<Sum> {
        Box::new(Sum {
            augend: self.clone(),
            addend: other.clone(),
        })
    }
}

impl PartialEq for Money {
    fn eq(&self, other: &Money) -> bool {
        if self.currency != other.currency {
            false
        } else {
            self.amount == other.amount
        }
    }
}

impl Expression for Money {
    fn reduce(&self, to: &'static str, bank: &Bank) -> Result<Money, &'static str> {
        let rate = bank.get_rate(self.currency, to);
        match rate {
            Some(r) => Ok(Money {
                amount: self.amount / r,
                currency: to,
            }),
            None => Err("Never set rate these currencies"),
        }
    }
}

pub struct Sum {
    augend: Money,
    addend: Money,
}
impl Expression for Sum {
    fn reduce(&self, to: &'static str, _: &Bank) -> Result<Money, &'static str> {
        Ok(Money {
            amount: self.augend.amount + self.addend.amount,
            currency: to,
        })
    }
}

pub struct Bank {
    rate: HashMap<(&'static str, &'static str), f64>,
}

impl Bank {
    pub fn new() -> Bank {
        Bank {
            rate: HashMap::new(),
        }
    }

    fn get_pair(one: &'static str, another: &'static str) -> (&'static str, &'static str) {
        if one.to_string() < another.to_string() {
            (one, another)
        } else {
            (another, one)
        }
    }
    pub fn add_rate(&mut self, from: &'static str, to: &'static str, r: f64) {
        let pair = Bank::get_pair(from, to);
        self.rate
            .insert(pair, if pair.0 == from { r } else { 1.0 / r });
    }

    pub fn get_rate(&self, from: &'static str, to: &'static str) -> Option<f64> {
        if from == to {
            return Some(1.);
        }

        let pair = Bank::get_pair(from, to);
        self.rate
            .get(&pair)
            .map(|i| if pair.0 == from { *i } else { 1.0 / *i })
    }

    pub fn reduce(&self, source: Box<Expression>, to: &'static str) -> Result<Money, &str> {
        source.reduce(to, self)
    }
}

まず、違和感があるのはMoney.sumだ。

pub fn add(&self, other: &Money) -> Box<Sum>

timesがMoneyを新しく作ってそれをそのまま返しているのに、addはSumのポインタを返している。これはもともとExpressionに汎用化してどーたらこーたらやっている名残でこうなっているわけで、別にSumをそのまま返せないことはない。

なので、シンプルに

pub fn add(&self, other: &Money) -> Sum {
    Sum {
        augend: self.clone(),
        addend: other.clone(),
    }
}

これで良いはずだ。

このSumはBank.reduceに渡されていたワケだが、そっちは

pub fn reduce(&self, source: Box<Expression>, to: &'static str) -> Result<Money, &str>

というシグネチャになっている。ここでもExpressionを実装した型の参照を受け取りたいが、&Epressionとは書くことができないので、トレイトオブジェクトを渡している。

これもJavaのコードに引きずられた形だ。このケースの場合、このようなトレイトオブジェクトを使うのでは無く、ジェネリクスで書くのがRustのスタイルのようだ。このようにポリモーフィズムを表現するのにRustではジェネリクスかトレイトオブジェクトかを選択して使うことになる・・・らしい。どちらも、JavaC#ではおなじみに使う構文によく似ているのだが、この2つを並列に並べて、「どっちが使おうか」なんてあんまり考えたことがないので、ちょっと戸惑う。

ともかく、ここで無駄にポインタを使うのではなく、ジェネリクスを使うと随分スッキリしたイメージになる。こうだ。

pub fn reduce<T: Expression>(&self, source: &T, to: &'static str) -> Result<Money, &str>

これで、sourceはExpressionを実装した型の参照が来るよ、という意味になる。しかし、ライフタイムのパラメータと型パラメータをどの位置に書けば良いのか覚えられない。慣れなのかな・・・。

これでちょっとキレイになって満足だ。

ケント・ベックの「テスト駆動開発」の写経をRustでやってみる(13)

14章に入る。いよいよ通貨の変更を実装する。2フランを1ドルに換算したい。 本で示されたテストはこうだ。

@Test
public void testReduceMoneyDifferentCurrency() {
    Bank bank = new Bank();
    bank.addRate("CHF", "USD", 2);
    Money result = bank.reduce(Money.franc(2), "USD");
    assertEquals(Money.dollar(1), result);
}

ぎゃふん。Bankが為替レートを保持している。いや、当然そうか。 私のRustのコードだと、bankはreduce関数だけを持ったモジュールになっている。 これでは為替レートを保持する場所が無い。さて、どうするか。

もちろん、為替レートをグローバルに保持するという考え方もあるかもしれないが、 為替レートは銀行(というか、両替の機能を持つ事業者)ごとにあってしかるべきだから、 複数の為替レートが持てるように銀行をオブジェクト化するべきだろう。

まず、bank::reduceを使っているテストから書き換えよう。今、こうなっている。

mod bank {
    use super::*;

    pub fn reduce(source: Box<Expression>, to: &'static str) -> Money {
        source.reduce(to)
    }
}
#[cfg(test)]
mod tests {
    //...

    #[test]
    fn test_reduce_sum() {
        let sum = Box::new(Sum {
            augend: Money::dollar(3),
            addend: Money::dollar(4),
        });
        let result = bank::reduce(sum, "USD");
        assert_eq!(Money::dollar(7), result);
    }
}

let result = bank::reduce(sum, "USD");の部分が

let bank = Bank {};
let result = bank.reduce(sum, "USD");

になる。まあ、大した変更じゃない。対応してBank構造体を作る。

pub struct Bank {}

impl Bank {
    pub fn reduce(&self, source: Box<Expression>, to: &'static str) -> Money {
        source.reduce(to)
    }
}

reduceメソッドはselfへの参照を引数に入れたこと以外、何も変えていない。 この程度の手間で修正が出来るのなら、必要となった今の時点まで構造体にしていなかったのは正しかったのかもしれない。

さて、では、冒頭のテストを追加してみよう。

#[test]
fn test_reduce_money_different_currency() {
    let bank = Bank {};
    bank.add_rate("CHF", "USD", 2);
    let result = bank.reduce(Box::new(Money::franc(2)), "USD");
    assert_eq!(Money::dollar(1), result);
}

特に悩ましいことは何もない。テストはちゃんと失敗する。

test tests::test_reduce_money_different_currency ... FAILED

failures:

---- tests::test_reduce_money_different_currency stdout ----
thread 'tests::test_reduce_money_different_currency' panicked at 'assertion failed: `(left == right)`
  left: `Money { amount: 1, currency: "USD" }`,
 right: `Money { amount: 2, currency: "CHF" }`', src/main.rs:142:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

さて、このテストを通すための実装は、ちょっと本を離れて自分の歩幅で進んでみようかと思う。その方が楽しそうだ。

Moneyのreduceの実装は、ダミーだ。

impl Expression for Money {
    fn reduce(&self, to: &str) -> Money {
        self.clone()
    }
}

これを本物にしたい。2フランが1ドルに変換されて返るんだから、

  • amountを2から1へ
  • currencyを"CHF"から"USD"へ

この両方ほ変更をしなくてはならない。下の方は簡単だ。変える先は引数で貰えているから。上は、変換レートを入手しなければならない。どこから貰うかというと、Bankからだ。そのためにBankを修正したんだから。

ということは、Bankにレートを提示する機能が必要になる。そっちから実装しよう。 まず、テストを書く。

#[test]
fn test_get_rate_from_bank() {
    let bank = Bank {};
    bank.add_rate("CHF", "USD", 2.0);
    assert_eq!(2.0, bank.get_rate("CHF", "USD"));
    assert_eq!(0.5, bank.get_rate("USD", "CHF"));
}

書いてみて思ったのだが、フランからドルへの換算レートをセットしたら、 当然のことながらドルからフランへの換算レートも教えて欲しい。 ということは、レートは整数ではダメだと言うことだ。

さて、Bankはどのようなデータ構造で為替レートを保持するのが良いだろうか。もちろん、ハッシュが使いたい。Rustにハッシュはあるのかというと、ある。std::collections::HashMapだ。ドキュメントを読むと、キーはEqトレイトとHashトレイトを実装していなければならない。ちゃんとチェックしてくれるのはありがたい。

さて、何をキーとするかといえば、通貨の組だ。この組は順書がある。単純に考えると、タプルでいいと思うんだが、タプルがEqトレイトとHashトレイトを実装しているのかは自明じゃ無い。とりあえず、書いてみようかな。

pub struct Bank {
    rate: HashMap<(&'static str, &'static str), f64>,
}

別にエラーにはならないみたい。ずっと&strを使っているのは気分が良くないけど、どうなんだろう。

さて、まずは、get_rateの方から考えよう。登録されていない為替レートを要求されたときどうするかがいきなり気になるけど、それはTo Doにして進めることにする。

pub fn get_rate(&self, from: &'static str, to: &'static str) -> f64 {
    self.rate.get((from, to))
}

怒られる。getの戻りはOptionだからだ。パターンマッチで展開しなければならない。

pub fn get_rate(&self, from: &'static str, to: &'static str) -> f64 {
    match self.rate.get(&(from, to)) {
        Some(r) => *r,
        None => 0.0,
    }
}

もちろん、見つからなかった場合は0じゃダメなんだけど、とりあえず。

add_rateも暫定の実装をしておこう。

pub fn add_rate(&self, from: &'static str, to: &'static str, r: f64) {
    self.rate.insert((from, to), r);
}

おっと、怒られた。insertをすると自分自身が変更されるのだからmutableな参照を借用しないといけない。

error[E0596]: cannot borrow `self.rate` as mutable, as it is behind a `&` reference
  --> src/main.rs:71:9
   |
70 |     pub fn add_rate(&self, from: &'static str, to: &'static str, r: f64) {
   |                     ----- help: consider changing this to be a mutable reference: `&mut self`
71 |         self.rate.insert((from, to), r);
   |         ^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
pub fn add_rate(&mut self, from: &'static str, to: &'static str, r: f64) {
    self.rate.insert((from, to), r);
}

テストも、

let mut bank = Bank::new();

のようにしておく必要がある。ともかく、これでadd_rateして、そのままget_rateすることはできるようになった。

しかし、ここで考える。("CHF", "USD")と("USD", "CHF")が両方登録できるのはいかがなものだろうか。アプリケーションで登録時にどちらの組み合わせが登録されているかチェックするのは1つの方法だけど、バグっていて登録できてしまったらもう読むときにどうしていいのかわからないのは良くない気がする。

では、どちらかに保持するとして、どちらが適切かはどうやって決めよう。別にどう決めてもいい気もするけど、たぶんこういうのは慣例があって決まってるもののような気もする。わからないから、順番を決めるメソッドを用意して、それで保持することにしよう。

#[test]
fn test_get_pair() {
    assert_eq!(("CHF", "USD"), get_pair("CHF", "USD"));
    assert_eq!(("CHF", "USD"), get_pair("USD", "CHF"));
}

こんな感じで良いだろうか。とりあえず、Stringの比較に従うことにする。

実装はま、こんなもんで良いだろう。

pub fn get_pair(one: &'static str, another: &'static str) -> (&'static str, &'static str) {
    if one.to_string() < another.to_string() {
        (one, another)
    } else {
        (another, one)
    }
}

これを使って、add_rateを実装してみよう。まずは、テストだ。

#[test]
fn test_add_rate() {
    let mut bank = Bank::new();

    // 初回登録
    bank.add_rate("CHF", "USD", 2.0);
    assert_eq!(2.0, bank.rate[&("CHF", "USD")]);

    // 上書き登録すると書き換えられる
    bank.add_rate("CHF", "USD", 4.0);
    assert_eq!(4.0, bank.rate[&("CHF", "USD")]);

    // 逆数でも登録できる
    bank.add_rate("USD", "CHF", 4.0);
    assert_eq!(0.25, bank.rate[&("CHF", "USD")]);
}

とりあえず、こんな感じで良いだろうか。 今までも実装でも2つめのassetまではクリアする。 最後の例をクリアしなくては。こんな素直な実装で良いかと思う。テストは通過した。

pub fn add_rate(&mut self, from: &'static str, to: &'static str, r: f64) {
    let pair = Bank::get_pair(from, to);
    self.rate
        .insert(pair, if pair.0 == from { r } else { 1.0 / r });
}

最後に、もう一度、get_rateのテストを見直そう。 登録されていないときの動作と、逆数を返さなくてはいけないときを考慮する。

#[test]
fn test_get_rate() {
    let mut bank = Bank::new();

    // 未登録の場合にはNoneが返る
    assert!(match bank.get_rate("CHF", "USD") {
        None => true,
        _ => false,
    });

    // 登録したらそれが取れる
    bank.add_rate("CHF", "USD", 2.0);
    assert_eq!(2.0, bank.get_rate("CHF", "USD").unwrap());

    // 逆順に取得したら、逆数が取れる
    assert_eq!(0.5, bank.get_rate("USD", "CHF").unwrap());
}

未登録の場合の処理を考えると、get_rateの戻りはOptionになっているのが適切だろう。というわけで、未登録だとNoneが返り、登録済みの場合はunwrap()して取り出している。

実装しよう。実は、HashMapのgetの戻りはOption<&T>なので、ほとんどそのまま返してやれば良い。つまり、Some<&f64>だった場合にSomeに直して、Noneだった場合はそのまま戻せば良い。それはmap()を使うとできる。参照は単純に*で戻してやればいい(んだと思う。コピーしなきゃダメなのかな?f64なら勝手にコピーされる??)

pub fn get_rate(&self, from: &'static str, to: &'static str) -> Option<f64> {
    let pair = Bank::get_pair(from, to);
    self.rate
        .get(&pair)
        .map(|i| if pair.0 == from { *i } else { 1.0 / *i })
}

ついに材料は揃った。Money.reduceを実装すべき時だ・・・とその前に、Moneyの保持している金額も、f64に変更してしまおう。f64はEqトレイトを実装しないので、MoneyもEqトレイトを自動実装できなくなるので、そこは修正する。

#[derive(Debug, PartialEq, Clone)]
pub struct Money {
    amount: f64,
    currency: &'static str,
}

コンストラクタやテストも軒並み変更。めんどくさい。

さて、いよいよMoney.reduceだ。為替レートを取得するためにreduceにはBankを渡してやる必要がある。そして、為替レートがセットされていなかったらエラーにならなければいけないので、戻り値はResultで包まれることになる。エラーの場合は今はエラーメッセージを返せば十分だと思うから、Result<Money, &'static str>にする。&strだとやっぱりライフタイム関連のエラーが出る。めんどくさい。

impl Expression for Money {
    fn reduce(&self, to: &'static str, bank: &Bank) -> Result<Money, &'static str> {
        let rate = bank.get_rate(self.currency, to);
        match rate {
            Some(r) => Ok(Money {
                amount: self.amount / r,
                currency: to,
            }),
            None => Err("Never set rate these currencies"),
        }
    }
}

これでExpressionトレイトのreduceメソッドの型を変えてしまったので、Sum.reduceも修正が必要だ。しかし、こっちはまだ何にもやってない。とりあえず、後回しにしよう。コンパイルエラーを無くすために全体をOk()で包んでおく。Bankの仮引数を_にしているのは、これは使われてませんよという警告を出なくするためだ。どう考えても使わないと本来はおかしいはずなので、このメソッドは次にちゃんと直さなくてはいけない。

impl Expression for Sum {
    fn reduce(&self, to: &'static str, _: &Bank) -> Result<Money, &'static str> {
        Ok(Money {
            amount: self.augend.amount + self.addend.amount,
            currency: to,
        })
    }
}

これでやっと、最初に作ったtest_reduce_money_different_currencyが通った。やれやれ。

・・・と思ったけど、このMoney.reduceはドルをドルに変更しようとするとマズいんじゃ無いだろうか。テストを追加しよう。

#[test]
fn test_reduce_money_different_currency() {
    let mut bank = Bank::new();
    bank.add_rate("CHF", "USD", 2.0);
    let result = bank.reduce(Box::new(Money::franc(2.0)), "USD").unwrap();
    assert_eq!(Money::dollar(1.0), result);

    // ドルをドルに変更する
    let result2 = bank.reduce(Box::new(Money::dollar(2.0)), "USD").unwrap();
    assert_eq!(Money::dollar(2.), result2);
}

案の定、失敗した。

thread 'tests::test_reduce_money_different_currency' panicked at 'called `Result::unwrap()` on an `Err` value: "Never set rate these currencies"', src/main.rs:180:23

("USD", "USD")という為替レートの登録を探しに行ってしまう。fromとtoが同じ時には、渡されたモノをそのまま返すことにしよう。

fn reduce(&self, to: &'static str, bank: &Bank) -> Result<Money, &'static str> {
    if self.currency == to {
        return Ok(self.clone());
    }
    let rate = bank.get_rate(self.currency, to);
    match rate {
        Some(r) => Ok(Money {
            amount: self.amount / r,
            currency: to,
        }),
        None => Err("Never set rate these currencies"),
    }
}

これでテストは通った。うーん、疲れた・・・。さて、本とはどのぐらい違っているだろうか。

ケント・ベックの「テスト駆動開発」の写経をRustでやってみる(12)

13章「実装を導くテスト」に入る。

最終的にどんなコードを目指しているのか見失ったままストーリーが続いているのだが (いや、一度、この部分は読んでいるんだけど・・・これが写経の効果だな)、 とりあえず、著者のインサイトに従っていくことにする。

さて、addメソッド(本ではplusメソッド)の戻りはSumというクラスでなくては ならないらしい。なるほど・・・Expressionは演算する式をイメージしているのに、 演算結果をMoneyで返していてはイカンということなんだな。

本ではplusメソッドの戻りであるresultがSumのインスタンスであることを キャストするコードをテストに入れる事によって強制している。

Sum sum = (Sum) result;

ちゃんとSumクラスを実装すれば、これはJavaでは実行時エラーになる。

では、Rustではどうだろうか。

単純に、Javaのコードを書き写せばこうなるんだろうか。

let result: Box<Expression> = five.add(&five);
let sum: Box<Sum> = result as Box<Sum>;

Sum構造体の定義を追加しさえすれば、 Javaならこれでコンパイルは通るんだけど・・・

error[E0605]: non-primitive cast: `std::boxed::Box<dyn Expression>` as `std::boxed::Box<Sum>`
  --> src/main.rs:99:29
   |
99 |         let sum: Box<Sum> = result as Box<Sum>;
   |                             ^^^^^^^^^^^^^^^^^^
   |
   = note: an `as` expression can only be used to convert between primitive types. Consider using the `From` trait

実行時エラーになるようなものは、そもそもコンパイルが通らないのがRustである。

エラーメッセージでは、「asで型変換したけりゃFromトレイトを実装しろ」と言っている。Fromトレイトはある型から別の型を作る(例えば、4つの整数の組や文字列からIPv4アドレスを作るとか)ための仕組みで、そーゆーのもあるにはあるんだけど、今、ここでやりたいのはポリモーフィズムとかダックタイプなので、Fromトレイトを使うのは適切ではない。

ちなみに、実行時にメソッドバインディングするという意味でのポリモーフィズムは、今まさにやっているようにBoxでトレイトの参照を持つこと(トレイトオブジェクトという)で可能だ。もちろん、Rustにとってそれはベストなやり方ではない(静的にバインドされてる方が性能的に有利だし、そういう方向性がRustの指向するところだ)。

というわけで、キャストする方向は止めておこう。普通にSumオブジェクトを使ったテストにしておく。addメソッドはSumを返す。Sumはメンバーとして足す数と足される数を持っている。

さて、Sumはどういうものなのだろうか。次に本で作られたテストはこういうモノだ。

@Test
public void testPlusReturnsSum() {
    Money five = Money.dollar(5);
    Expresson result = five.plus(five);
    Sum sum = (Sum) result;
    assertEquals(five, sum.augent);
    assertEquals(five, sum.addend);
}

Sumにはaugent(足される数)とaddend(足す数)という2つのMeney型のフィールドがあるようだ。Moneyのplusメソッドは、Sumのインスタンスを作り、自分自身をaugentに、引数をaddendにセットして返す。

ただし、Rustで単にSumにMoneyの参照を保持するようにするのは違うような気がする。値の演算を保持することを考えると、渡されたMoneyの複製を保持するべきだろう。渡されるMoneyがイミュータブルであるとは限らないし。 上のJava版のテストのRust版はこうした。

#[test]
fn test_add_returns_sum() {
    let five = Money::dollar(5);
    let sum = five.add(&five);
    assert_eq!(five, sum.augend);
    assert_eq!(five, sum.addend);
}

対応するaddの実装はこうなる。amountはi32なので、複製が作られる。currencyはどれも同じスライスを指している。

pub fn add(&self, other: &Money) -> Box<Sum> {
    Box::new(Sum {
        augend: Money {
            amount: self.amount,
            currency: self.currency,
        },
        addend: Money {
            amount: other.amount,
            currency: other.currency,
        },
    })
}

まあ、シンプルだ。しかし、Moneyの複製を作るコードが重複しているのは良くない。ここで、本の流れから逸れて、複製のためのメソッドを作ってしまおう。

と言っても、例によって何も考えずにフィールドを複製すれば複製が作られるようなものなら、Rustが勝手にやってくれる。Cloneトレイトを使う。デフォルト実装で避ければ、#[derive(Clone)]をクラスに付ければいいだけだ。

#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Money {
    amount: i32,
    currency: &'static str,
}

impl Money {
    (略)
    pub fn add(&self, other: &Money) -> Box<Sum> {
        Box::new(Sum {
            augend: self.clone(),
            addend: other.clone(),
        })
    }
}

もう一歩進んでCopyトレイトの事も考えたいが、それは後に置いておくことにしよう。

次は、reduceメソッドだ。今のreduceメソッドは何を入れても10ドルを返すダミーメソッドだが、これをちゃんとしよう。まず、ちゃんとしていないとエラーになるようなテストを書く。

#[test]
fn test_reduce_sum() {
    let sum = Box::new(Sum {
        augend: Money::dollar(3),
        addend: Money::dollar(4),
    });
    let result = bank::reduce(sum, "USD");
    assert_eq!(Money::dollar(7), result);
}

テストがfailすることを確認したら、実装に移ろう。

さて、bank::reduceメソッドのシグネチャ

pub fn reduce(source: Box<Expression>, to: &str) -> Money

である。

ここでExpressionはImposterパターンを使うために導入されたことを 思いだそう。Imposter(偽物)パターンは、まとめて扱いたいものを あたかも同一種のように扱うためのパターンである。 つまり、Expressionトレイトを実装していれば、 それがどんなモノでもreduceが行えるような仕組みにする必要がある。 というわけで、Expressionトレイトにreduceというメソッドを作り、 渡ってきたオブジェクトに処理を委譲する。 Sumのreduceの実装では、そこに足し算の処理も入れてしまえばいい。

やっとやりたいことが見えてきた。なるほど。

というわけで、Expressionトレイトはこうなる。

pub trait Expression {
    fn reduce(&self, to: &str) -> Money;
}

MoneyもExpressionトレイトを実装しているので reduceメソッドを実装しなければならない。 まあ、Moneyを返せば良いので自分を複製して返すことにする。 本当はここで通貨の両替が実装されるのである。 いずれテストが書かれ、クソ実装が暴かれるだろう。

impl Expression for Money {
    fn reduce(&self, to: &str) -> Money {
        self.clone()
    }
}

Sumの実装も、両替はほったらかし。とりあえず3+4が計算できるようにして、テストを通そう。

impl Expression for Sum {
    fn reduce(&self, to: &str) -> Money {
        Money {
            amount: self.augend.amount + self.addend.amount,
            currency: to,
        }
    }
}

これでばっちり・・・とならないところが残念。コンパイルエラーである。

error[E0312]: lifetime of reference outlives lifetime of borrowed content...
  --> src/main.rs:74:23
   |
74 |             currency: to,
   |                       ^^
   |
   = note: ...the reference is valid for the static lifetime...
note: ...but the borrowed content is only valid for the anonymous lifetime #2 defined on the method body at 71:5
  --> src/main.rs:71:5
   |
71 | /     fn reduce(&self, to: &str) -> Money {
72 | |         Money {
73 | |             amount: self.augend.amount + self.addend.amount,
74 | |             currency: to,
75 | |         }
76 | |     }
   | |_____^

ライフタイムである。reduceに渡ってきた文字列スライスtoが、戻り値に入っているけど、このtoがいつまで有効かわからんからヌルポになる可能性があるよ、といわれているわけだ。そもそも、Moneyのcurrencyに入れるのは'staticな参照だけのつもりなので、それしか入れられないように引数に指定をしてしまえばいい。

こんな感じだ。

pub trait Expression {
    fn reduce(&self, to: &'static str) -> Money;
}

これでテストは全て通る。両替は次の章で実装されるってことで、今回はここまで。