use monadify::applicative::kind::Applicative;
use monadify::identity::{Identity, IdentityKind};
use monadify::mdo;
use monadify::monad::kind::Bind;
use monadify::transformers::reader::{MonadReader, Reader, ReaderT, ReaderTKind};
use proptest::prelude::*;
#[derive(Clone, Debug, PartialEq)]
struct Config {
base: i32,
factor: i32,
}
type ReaderKind = ReaderTKind<Config, IdentityKind>;
type ConfigReader<A> = Reader<Config, A>;
fn ask_env() -> ConfigReader<Config> {
<ReaderKind as MonadReader<Config, Config, IdentityKind>>::ask()
}
fn run_i32(computation: &ConfigReader<i32>, cfg: Config) -> i32 {
let Identity(value) = (computation.run_reader_t)(cfg);
value
}
#[test]
fn reader_mdo_same_env_threaded_to_every_bind() {
let computation: ConfigReader<i32> = mdo! {
ReaderKind;
base <- ReaderT::new(|cfg: Config| Identity(cfg.base));
factor <- ReaderT::new(|cfg: Config| Identity(cfg.factor));
ReaderKind::pure(base * factor)
};
assert_eq!(run_i32(&computation, Config { base: 3, factor: 7 }), 21);
assert_eq!(
run_i32(
&computation,
Config {
base: 0,
factor: 99
}
),
0
);
assert_eq!(
run_i32(
&computation,
Config {
base: -2,
factor: 5
}
),
-10
);
assert_eq!(run_i32(&computation, Config { base: 1, factor: 1 }), 1);
}
#[test]
fn reader_mdo_different_envs_produce_different_results() {
let computation: ConfigReader<i32> = mdo! {
ReaderKind;
base <- ReaderT::new(|cfg: Config| Identity(cfg.base));
factor <- ReaderT::new(|cfg: Config| Identity(cfg.factor));
ReaderKind::pure(base + factor)
};
let r1 = run_i32(
&computation,
Config {
base: 10,
factor: 20,
},
);
let r2 = run_i32(&computation, Config { base: 1, factor: 2 });
assert_eq!(r1, 30);
assert_eq!(r2, 3);
assert_ne!(r1, r2);
}
#[test]
fn reader_mdo_ask_exposes_full_environment() {
let computation: ConfigReader<i32> = mdo! {
ReaderKind;
cfg <- ask_env();
let derived = cfg.base + cfg.factor;
ReaderKind::pure(derived)
};
assert_eq!(run_i32(&computation, Config { base: 4, factor: 6 }), 10);
assert_eq!(run_i32(&computation, Config { base: 0, factor: 0 }), 0);
assert_eq!(
run_i32(
&computation,
Config {
base: -1,
factor: 1
}
),
0
);
assert_eq!(
run_i32(
&computation,
Config {
base: 100,
factor: -3
}
),
97
);
}
#[test]
fn reader_mdo_ask_derives_string_label() {
let computation: ConfigReader<String> = mdo! {
ReaderKind;
cfg <- ask_env();
let label = format!("base={} factor={}", cfg.base, cfg.factor);
ReaderKind::pure(label)
};
let Identity(result) = (computation.run_reader_t)(Config { base: 7, factor: 3 });
assert_eq!(result, "base=7 factor=3");
}
#[test]
fn reader_mdo_ask_combined_with_field_reader() {
let computation: ConfigReader<i32> = mdo! {
ReaderKind;
cfg <- ask_env();
extra <- ReaderT::new(|cfg: Config| Identity(cfg.factor * 10));
ReaderKind::pure(cfg.base + extra)
};
assert_eq!(run_i32(&computation, Config { base: 3, factor: 5 }), 53);
assert_eq!(run_i32(&computation, Config { base: 0, factor: 0 }), 0);
}
#[test]
fn reader_mdo_equivalence_matches_hand_written_bind() {
let lhs: ConfigReader<i32> = mdo! {
ReaderKind;
base <- ReaderT::new(|cfg: Config| Identity(cfg.base));
factor <- ReaderT::new(|cfg: Config| Identity(cfg.factor));
ReaderKind::pure(base.wrapping_add(factor))
};
let rhs: ConfigReader<i32> = ReaderKind::bind(
ReaderT::new(|cfg: Config| Identity(cfg.base)),
move |base| {
ReaderKind::bind(
ReaderT::new(|cfg: Config| Identity(cfg.factor)),
move |factor| ReaderKind::pure(base.wrapping_add(factor)),
)
},
);
let envs = [
Config { base: 0, factor: 0 },
Config { base: 1, factor: 2 },
Config {
base: -5,
factor: 10,
},
Config {
base: 42,
factor: -7,
},
Config {
base: i32::MAX,
factor: i32::MIN,
},
];
for env in &envs {
let lhs_result = run_i32(&lhs, env.clone());
let rhs_result = run_i32(&rhs, env.clone());
assert_eq!(
lhs_result, rhs_result,
"mdo! and hand-written bind disagree for env {:?}",
env
);
}
}
#[test]
fn reader_mdo_equivalence_three_step_chain() {
let lhs: ConfigReader<i32> = mdo! {
ReaderKind;
b1 <- ReaderT::new(|cfg: Config| Identity(cfg.base));
b2 <- ReaderT::new(|cfg: Config| Identity(cfg.base));
f <- ReaderT::new(|cfg: Config| Identity(cfg.factor));
ReaderKind::pure(b1.wrapping_add(b2).wrapping_add(f))
};
let rhs: ConfigReader<i32> =
ReaderKind::bind(ReaderT::new(|cfg: Config| Identity(cfg.base)), move |b1| {
ReaderKind::bind(ReaderT::new(|cfg: Config| Identity(cfg.base)), move |b2| {
ReaderKind::bind(ReaderT::new(|cfg: Config| Identity(cfg.factor)), move |f| {
ReaderKind::pure(b1.wrapping_add(b2).wrapping_add(f))
})
})
});
let envs = [
Config { base: 0, factor: 0 },
Config {
base: 10,
factor: 3,
},
Config {
base: -4,
factor: 8,
},
];
for env in &envs {
let lhs_result = run_i32(&lhs, env.clone());
let rhs_result = run_i32(&rhs, env.clone());
assert_eq!(
lhs_result, rhs_result,
"three-step mdo! and hand-written bind disagree for env {:?}",
env
);
}
}
proptest! {
#![proptest_config(ProptestConfig { cases: 256, ..ProptestConfig::default() })]
#[test]
fn reader_mdo_equivalence_prop(base in any::<i32>(), factor in any::<i32>()) {
let env = Config { base, factor };
let lhs: ConfigReader<i32> = mdo! {
ReaderKind;
b <- ReaderT::new(|cfg: Config| Identity(cfg.base));
f <- ReaderT::new(|cfg: Config| Identity(cfg.factor));
ReaderKind::pure(b.wrapping_mul(f).wrapping_add(b).wrapping_add(f))
};
let rhs: ConfigReader<i32> = ReaderKind::bind(
ReaderT::new(|cfg: Config| Identity(cfg.base)),
move |b| {
ReaderKind::bind(
ReaderT::new(|cfg: Config| Identity(cfg.factor)),
move |f| ReaderKind::pure(b.wrapping_mul(f).wrapping_add(b).wrapping_add(f)),
)
},
);
let lhs_result = run_i32(&lhs, env.clone());
let rhs_result = run_i32(&rhs, env.clone());
prop_assert_eq!(lhs_result, rhs_result);
}
}
#[test]
fn reader_mdo_three_bindings_all_see_same_env() {
let computation: ConfigReader<i32> = mdo! {
ReaderKind;
b1 <- ReaderT::new(|cfg: Config| Identity(cfg.base));
b2 <- ReaderT::new(|cfg: Config| Identity(cfg.base));
f <- ReaderT::new(|cfg: Config| Identity(cfg.factor));
ReaderKind::pure(b1 + b2 + f)
};
assert_eq!(
run_i32(
&computation,
Config {
base: 10,
factor: 3
}
),
23
);
assert_eq!(run_i32(&computation, Config { base: 0, factor: 0 }), 0);
assert_eq!(
run_i32(
&computation,
Config {
base: -1,
factor: 5
}
),
3
);
}
#[test]
fn reader_mdo_four_bindings_correct_at_depth() {
let computation: ConfigReader<i32> = mdo! {
ReaderKind;
a <- ReaderT::new(|cfg: Config| Identity(cfg.base));
b <- ReaderT::new(|cfg: Config| Identity(cfg.factor));
c <- ReaderT::new(|cfg: Config| Identity(cfg.base * 2));
d <- ReaderT::new(|cfg: Config| Identity(cfg.factor * 2));
ReaderKind::pure(a + b + c + d)
};
let cfg = Config { base: 5, factor: 3 };
assert_eq!(run_i32(&computation, cfg), 24);
}
#[test]
fn reader_mdo_local_doubles_factor_for_inner_computation() {
let read_factor: ConfigReader<i32> = ReaderT::new(|cfg: Config| Identity(cfg.factor));
let doubled_factor: ConfigReader<i32> =
<ReaderKind as MonadReader<Config, i32, IdentityKind>>::local(
|mut cfg: Config| {
cfg.factor *= 2;
cfg
},
read_factor,
);
let computation: ConfigReader<i32> = mdo! {
ReaderKind;
original <- ReaderT::new(|cfg: Config| Identity(cfg.factor));
modified <- doubled_factor;
ReaderKind::pure(original + modified)
};
let cfg = Config { base: 0, factor: 5 };
assert_eq!(run_i32(&computation, cfg), 15);
}
#[test]
fn reader_mdo_local_only_affects_its_inner_computation() {
let inflated_base: ConfigReader<i32> =
<ReaderKind as MonadReader<Config, i32, IdentityKind>>::local(
|mut cfg: Config| {
cfg.base *= 100;
cfg
},
ReaderT::new(|cfg: Config| Identity(cfg.base)),
);
let computation: ConfigReader<i32> = mdo! {
ReaderKind;
(original_base, factor) <- ReaderT::new(|cfg: Config| Identity((cfg.base, cfg.factor)));
inflated <- inflated_base;
ReaderKind::pure(original_base + factor + inflated)
};
let cfg = Config { base: 2, factor: 3 };
let Identity(result) = (computation.run_reader_t)(cfg);
assert_eq!(result, 205);
}
#[test]
fn reader_mdo_local_negated_base_combined_with_original() {
let negated_base: ConfigReader<i32> =
<ReaderKind as MonadReader<Config, i32, IdentityKind>>::local(
|mut cfg: Config| {
cfg.base = -cfg.base;
cfg
},
ReaderT::new(|cfg: Config| Identity(cfg.base)),
);
let computation: ConfigReader<i32> = mdo! {
ReaderKind;
pos_base <- ReaderT::new(|cfg: Config| Identity(cfg.base));
neg_base <- negated_base;
ReaderKind::pure(pos_base + neg_base)
};
assert_eq!(run_i32(&computation, Config { base: 7, factor: 0 }), 0);
assert_eq!(run_i32(&computation, Config { base: 0, factor: 0 }), 0);
assert_eq!(
run_i32(
&computation,
Config {
base: -3,
factor: 0
}
),
0
);
}