1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
//! This module includes Rust's implementation of *traversable* types.
//!
//! [Traverse](https://typelevel.org/cats/typeclasses/traverse.html) is a *type class* which
//! threats an effect through a collection (or another wrapping context) and "turns the effect
//! inside out".
//!
//! Rust implements pattern via combination of `map` (to apply the effect) followed by `collect`
//! which is available for [std::iter::FromIterator] instances (in Scala Cats this is referred to
//! as `sequence`).
use std::fs::File;
use std::io::prelude::*;
use std::io::{BufReader, Result};
use std::path::Path;
/// Traverse a collection of input values and apply an effect to item that may fail (take the
/// first character from each string slice, resulting in an [`Option<char>`](Option)).
///
/// Here the *magic* is the sequencing of these effects via `collect`. This is possible
/// because [std::iter::FromIterator] is implemented for the `Option` type, allowing for
/// the following transformations:
/// 1. Applying an effect on each value of `Vec<&str>`, turning it into an
/// `Iterator<Item = Option<char>>`
/// 2. Turning these effects "inside out" and collecting the items into `Option<Vec<char>>`
///
/// This efectively realizes [Traverse](https://typelevel.org/cats/typeclasses/traverse.html),
/// in this case for a [Vec] which is a 'Functor' (has a `map`) and an [Option] which is
/// 'Applicative Monad' (by the realization in Rust std lib).
pub fn collect_initials(names: Vec<&str>) -> Option<Vec<char>> {
names.into_iter().map(first).collect()
}
fn first(input: &str) -> Option<char> {
input.chars().next()
}
/// Traversing a [Result] works analogously to an [Option] since a result is basically an option
/// where the `None` case is some more specific type.
///
/// Other languages typically implement a general `.traverse(f)` method, so it might seem that
/// `collect` is not as expressive (it's equivalent to `.sequence`). However, `.traverse(f)` is
/// equivalent to `.map(f).sequence`. See the reference to *Typelevel Cats* for more.
///
/// Note that [std::io::Result] is just ordinary result with io error `Result<T, std::io::Error>`.
pub fn read_files<P: AsRef<Path>>(paths: &[P]) -> Result<Vec<String>> {
paths.iter().map(read_file).collect()
}
fn read_file<P: AsRef<Path>>(path: P) -> Result<String> {
let file = File::open(path)?;
let mut buf_reader = BufReader::new(file);
let mut contents = String::new();
buf_reader.read_to_string(&mut contents)?;
Result::Ok(contents)
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::*;
use std::borrow::Borrow;
use std::env;
use std::path::PathBuf;
struct TempFile(PathBuf);
impl Drop for TempFile {
fn drop(&mut self) {
std::fs::remove_file(&self.0).unwrap_or(());
}
}
impl AsRef<Path> for TempFile {
fn as_ref(&self) -> &Path {
&self.0
}
}
// Based on https://github.com/la10736/rstest/blob/master/notes.md
#[fixture]
fn temp_file(#[default("test")] name: &str, #[default("")] text: &str) -> TempFile {
let mut path = env::temp_dir();
path.push(name);
File::create(&path)
.and_then(|mut fd| fd.write(text.as_bytes()))
.expect("Failed to create temp file");
TempFile(path)
}
#[rstest]
fn traverse_options() {
let success = collect_initials(vec!["Alice", "Bob", "Charlie"]);
assert_eq!(success, Some(vec!['A', 'B', 'C']));
// Second example demonstrates how the effects are sequenced. If the traversal finds single
// value to be `None`, the whole result is `None`, otherwise all values are valid and can be
// extracted and the result is `Some`.
let failure = collect_initials(vec!["Martin", ""]);
assert_eq!(failure, None);
}
#[rstest]
fn traverse_results(
#[from(temp_file)]
#[with("test1", "some text")]
tmp1: TempFile,
#[from(temp_file)]
#[with("test2", "other text")]
tmp2: TempFile,
) {
let success =
read_files(&[tmp1.borrow(), tmp2.borrow()]).expect("This case should return Ok");
assert_eq!(
success,
vec!["some text".to_string(), "other text".to_string()]
);
let non_existing = TempFile(PathBuf::from("non_existing_file"));
let failure = read_files(&[tmp1, tmp2, non_existing]);
assert!(failure.is_err());
}
#[rstest]
fn build_non_linear_structure() {
use std::collections::BinaryHeap;
// As mentioned above, `collect` works for any implementation of the `FromIterator` trait.
// This allows us to transform sequential data (an `Iterator`) into any data structure -
// event non-linear one such as `BinaryHeap` (`BinaryHeap` implements `FromIterator`).
let heap = vec![1, -2, 5, 4].into_iter().collect::<BinaryHeap<i32>>();
assert_eq!(heap.into_iter().collect::<Vec<_>>(), vec![5, 4, 1, -2]);
}
}
