use std::convert::Infallible;
use std::future::Future;
use crate::effect::prelude::*;
use crate::BoxedEffect;
#[derive(Debug, Clone, Copy)]
pub struct IO;
impl IO {
pub fn read<T, R, F, Env>(f: F) -> BoxedEffect<R, Infallible, Env>
where
F: FnOnce(&T) -> R + Send + 'static,
R: Send + 'static,
T: Send + Sync + 'static,
Env: AsRef<T> + Clone + Send + Sync + 'static,
{
from_fn(move |env: &Env| Ok(f(env.as_ref()))).boxed()
}
pub fn write<T, R, F, Env>(f: F) -> BoxedEffect<R, Infallible, Env>
where
F: FnOnce(&T) -> R + Send + 'static,
R: Send + 'static,
T: Send + Sync + 'static,
Env: AsRef<T> + Clone + Send + Sync + 'static,
{
from_fn(move |env: &Env| Ok(f(env.as_ref()))).boxed()
}
pub fn read_async<T, R, F, Fut, Env>(f: F) -> BoxedEffect<R, Infallible, Env>
where
F: FnOnce(&T) -> Fut + Send + 'static,
Fut: Future<Output = R> + Send + 'static,
R: Send + 'static,
T: Send + Sync + 'static,
Env: AsRef<T> + Clone + Send + Sync + 'static,
{
from_async(move |env: &Env| {
let fut = f(env.as_ref());
async move { Ok(fut.await) }
})
.boxed()
}
pub fn write_async<T, R, F, Fut, Env>(f: F) -> BoxedEffect<R, Infallible, Env>
where
F: FnOnce(&T) -> Fut + Send + 'static,
Fut: Future<Output = R> + Send + 'static,
R: Send + 'static,
T: Send + Sync + 'static,
Env: AsRef<T> + Clone + Send + Sync + 'static,
{
from_async(move |env: &Env| {
let fut = f(env.as_ref());
async move { Ok(fut.await) }
})
.boxed()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
#[tokio::test]
async fn test_io_read_simple() {
#[derive(Clone)]
struct Database {
value: i32,
}
impl Database {
fn get_value(&self) -> i32 {
self.value
}
}
#[derive(Clone)]
struct Env {
db: Database,
}
impl AsRef<Database> for Env {
fn as_ref(&self) -> &Database {
&self.db
}
}
let env = Env {
db: Database { value: 42 },
};
let effect = IO::read(|db: &Database| db.get_value());
let result = effect.run(&env).await;
assert_eq!(result, Ok(42));
}
#[tokio::test]
async fn test_io_read_user_data() {
#[derive(Clone, PartialEq, Debug)]
struct User {
id: u64,
name: String,
}
#[derive(Clone)]
struct Database {
users: Vec<User>,
}
impl Database {
fn find_user(&self, id: u64) -> Option<User> {
self.users.iter().find(|u| u.id == id).cloned()
}
}
#[derive(Clone)]
struct Env {
db: Database,
}
impl AsRef<Database> for Env {
fn as_ref(&self) -> &Database {
&self.db
}
}
let env = Env {
db: Database {
users: vec![User {
id: 1,
name: "Alice".to_string(),
}],
},
};
let effect = IO::read(|db: &Database| db.find_user(1));
let result = effect.run(&env).await;
assert_eq!(
result,
Ok(Some(User {
id: 1,
name: "Alice".to_string()
}))
);
}
#[tokio::test]
async fn test_io_write_with_mutex() {
#[derive(Clone)]
struct Logger {
messages: Arc<Mutex<Vec<String>>>,
}
impl Logger {
fn log(&self, msg: String) {
self.messages.lock().unwrap().push(msg);
}
}
#[derive(Clone)]
struct Env {
logger: Logger,
}
impl AsRef<Logger> for Env {
fn as_ref(&self) -> &Logger {
&self.logger
}
}
let env = Env {
logger: Logger {
messages: Arc::new(Mutex::new(Vec::new())),
},
};
let effect = IO::write(|logger: &Logger| {
logger.log("Hello".to_string());
});
effect.run(&env).await.unwrap();
assert_eq!(env.logger.messages.lock().unwrap().len(), 1);
assert_eq!(env.logger.messages.lock().unwrap()[0], "Hello");
}
#[tokio::test]
async fn test_io_read_async() {
use std::future::ready;
#[derive(Clone)]
struct Database {
value: String,
}
#[derive(Clone)]
struct Env {
db: Database,
}
impl AsRef<Database> for Env {
fn as_ref(&self) -> &Database {
&self.db
}
}
let env = Env {
db: Database {
value: "Result of: SELECT * FROM users".to_string(),
},
};
let effect = IO::read_async(|db: &Database| {
let value = db.value.clone();
ready(value)
});
let result = effect.run(&env).await;
assert_eq!(result, Ok("Result of: SELECT * FROM users".to_string()));
}
#[tokio::test]
async fn test_io_write_async() {
use std::future::ready;
#[derive(Clone)]
struct Cache {
data: Arc<Mutex<Vec<String>>>,
}
#[derive(Clone)]
struct Env {
cache: Cache,
}
impl AsRef<Cache> for Env {
fn as_ref(&self) -> &Cache {
&self.cache
}
}
let env = Env {
cache: Cache {
data: Arc::new(Mutex::new(Vec::new())),
},
};
let effect = IO::write_async(|cache: &Cache| {
cache.data.lock().unwrap().push("value".to_string());
ready(())
});
effect.run(&env).await.unwrap();
assert_eq!(env.cache.data.lock().unwrap().len(), 1);
}
#[tokio::test]
async fn test_multiple_services() {
#[derive(Clone)]
struct Database {
data: String,
}
#[derive(Clone)]
struct Cache {
data: String,
}
#[derive(Clone)]
struct Logger {
data: String,
}
#[derive(Clone)]
struct Env {
db: Database,
cache: Cache,
logger: Logger,
}
impl AsRef<Database> for Env {
fn as_ref(&self) -> &Database {
&self.db
}
}
impl AsRef<Cache> for Env {
fn as_ref(&self) -> &Cache {
&self.cache
}
}
impl AsRef<Logger> for Env {
fn as_ref(&self) -> &Logger {
&self.logger
}
}
let env = Env {
db: Database {
data: "db data".to_string(),
},
cache: Cache {
data: "cache data".to_string(),
},
logger: Logger {
data: "logger data".to_string(),
},
};
let db_effect = IO::read(|db: &Database| db.data.clone());
let cache_effect = IO::read(|cache: &Cache| cache.data.clone());
let logger_effect = IO::read(|logger: &Logger| logger.data.clone());
assert_eq!(db_effect.run(&env).await, Ok("db data".to_string()));
assert_eq!(cache_effect.run(&env).await, Ok("cache data".to_string()));
assert_eq!(logger_effect.run(&env).await, Ok("logger data".to_string()));
}
#[tokio::test]
async fn test_composition_with_combinators() {
#[derive(Clone)]
struct Database {
value: i32,
}
#[derive(Clone)]
struct Env {
db: Database,
}
impl AsRef<Database> for Env {
fn as_ref(&self) -> &Database {
&self.db
}
}
let env = Env {
db: Database { value: 10 },
};
let effect = IO::read(|db: &Database| db.value)
.map(|x| x * 2)
.and_then(|x| IO::read(move |db: &Database| x + db.value));
let result = effect.run(&env).await;
assert_eq!(result, Ok(30)); }
#[tokio::test]
async fn test_real_world_composition() {
use std::future::ready;
#[derive(Clone)]
struct Database {
data: HashMap<u64, String>,
}
#[derive(Clone)]
struct Cache {
data: Arc<Mutex<HashMap<u64, String>>>,
}
impl Cache {
fn get(&self, id: u64) -> Option<String> {
self.data.lock().unwrap().get(&id).cloned()
}
fn set(&self, id: u64, value: String) {
self.data.lock().unwrap().insert(id, value);
}
}
#[derive(Clone)]
struct Env {
db: Database,
cache: Cache,
}
impl AsRef<Database> for Env {
fn as_ref(&self) -> &Database {
&self.db
}
}
impl AsRef<Cache> for Env {
fn as_ref(&self) -> &Cache {
&self.cache
}
}
let mut db_data = HashMap::new();
db_data.insert(1, "Alice".to_string());
let env = Env {
db: Database { data: db_data },
cache: Cache {
data: Arc::new(Mutex::new(HashMap::new())),
},
};
let effect = IO::read(move |cache: &Cache| cache.get(1)).and_then(|cached| {
if cached.is_some() {
pure(cached).boxed()
} else {
IO::read_async(|db: &Database| {
let value = db.data.get(&1).cloned();
ready(value)
})
.and_then(move |value| {
if let Some(ref v) = value {
let v = v.clone();
IO::write(move |cache: &Cache| {
cache.set(1, v);
})
.map(move |_| value.clone())
.boxed()
} else {
pure(value).boxed()
}
})
.boxed()
}
});
let result = effect.run(&env).await;
assert_eq!(result, Ok(Some("Alice".to_string())));
let cached = env.cache.get(1);
assert_eq!(cached, Some("Alice".to_string()));
}
}