about
3/07/2022
Type Erase Structure

Design Bite - Type Erase Structure

line-counter with trait-coupled packages

"You can't depend on your eyes when your imagination is out of focus."
- Mark Twain

1.0 Introduction

This DesignBite sequence was inspired by BuildOn project TextFinder. As that project is designed and implemented, a number of design decisions are made, consciously or unconsciously. Each of these pages addresses one answer to questions about fundamental decisions of structure. To make discussion pragmatic and concrete, we implement a program that evaluates the number of lines in text files. Processing is quite simple so it allows us to see how each structure alternative is implemented. We consider both package structure and logical structure, e.g., functions and structs used to order design and implementation. In this TypeErase Structure page, code is implemented in a set of packages Executive, Input, Compute, and Output and their structs. That provides all of the organization for processing.

2. Application Structure - TypeErase

This structure is modular with a data flow structure. It differs from the previous dataflow structure in that:
  • Its Input and Compute parts are parameterized on the types to which they send output.
  • Each parameterized part depends only on the trait used by its downstream part. That trait is definded in the caller's package.
  • That means that all non-Executive parts are immune to build breakage when other parts change.
  • Its dependencies are opposite of the data flow structure. Each part depends on its upstream package where the trait it uses is defined. For that reason it is often said to implement the Dependency Inversion Principle.
Figure 5. Type Erasure Type Structure

Type Erasure Structure

Type erasure changes dependency relationships. In Figure 5, Input holds a "type erased" instance of the compute block. Input defines an interface it will call, Compute, that ComputeImpl implements. The Executive creates an instance of Input<ComputeImpl> but Input, internally, just uses the Compute interface, Compute. That means that both Input and ComputeImpl depend on Compute. Since Input defines Compute, the Compute package now depends on the Input package - the opposite of the basic data flow structure. Because of that, we say this design uses dependency inversion. Now, Input can be instantiated with any type that implements Comp. It does not depend on any of the downstream implementation details, and so can be reusable.

Pros:

  1. Same as data flow
  2. The Input, Compute, and Output parts are now decoupled, each depending only on the interfaces it defines and the interfaces of upstream components.

Cons:

  1. The building process becomes more complex. Each component needs to use a factory function to create its downstream component (to maintain type ignorance).

TypeErase Code Repository
Executive::main.rs ///////////////////////////////////////////////////////////// // TypeErasureDataFlowStructure::Executive::main.rs // // - Executive creates and uses all lower level parts // // Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 // ///////////////////////////////////////////////////////////// /* Note: Executive only creates Input instance. The rest of the pipeline self installs, e.g., Input creates Compute, and Compute creates Output. */ use input::*; use compute::*; use output::*; fn main() { let putln = || println!(); print!( "\n -- TypeErasureDataFlowStructure::Executive --\n" ); let mut lines = 0; type Comp = ComputeImpl<OutputImpl>; let mut inp = Input::<Comp>::new(); let name = "./src/main.rs"; lines += inp.do_input(name); putln(); let name = "../Input/src/lib.rs"; lines += inp.do_input(name); let name = "../Input/examples/test1.rs"; lines += inp.do_input(name); putln(); let name = "../Compute/src/lib.rs"; lines += inp.do_input(name); let name = "../Compute/examples/test1.rs"; lines += inp.do_input(name); putln(); let name = "../Output/src/lib.rs"; lines += inp.do_input(name); let name = "../Output/examples/test1.rs"; lines += inp.do_input(name); putln(); print!("\n total lines: {}", lines); print!("\n\n That's all Folks!\n\n"); } Output -- TypeErasureDataFlowStructure::Executive -- file "./src/main.rs" has 52 lines of code file "../Input/src/lib.rs" has 58 lines of code file "../Input/examples/test1.rs" has 30 lines of code file "../Compute/src/lib.rs" has 61 lines of code file "../Compute/examples/test1.rs" has 39 lines of code file "../Output/src/lib.rs" has 28 lines of code file "../Output/examples/test1.rs" has 16 lines of code total lines: 284 That's all Folks! cargo.toml [package] name = "executive" version = "0.1.0" authors = ["James W. Fawcett"] edition = "2018" # See more keys ... [dependencies] input = { path = "../Input" } compute = { path = "../Compute" } output = { path = "../Output" } Input::lib.rs ///////////////////////////////////////////////////////////// // TypeErasureDataFlowStructure::Input::lib.rs // // - Attempts to return line count from file // // Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 // ///////////////////////////////////////////////////////////// /* Note: - Input owns and instantiates Compute. - It attempts to open file and pass to Compute for processing. - Returns line count if successful */ use std::fs::*; mod file_utilities; use file_utilities::open_file_for_read; pub trait Compute { fn new() -> Self; fn do_compute(&mut self, name: &str, file:File); fn lines(&self) -> usize; } #[derive(Debug)] pub struct Input<T: Compute> { name: String, compute: T } impl<T: Compute> Input<T> { pub fn new() -> Input<T> { Input { name: String::new(), compute: T::new() } } pub fn do_input(&mut self, name: &str) -> usize { let mut lines: usize = 0; self.name = name.to_string(); let rslt = open_file_for_read(name); if let Ok(file) = rslt { self.compute.do_compute(name, file); lines = self.compute.lines(); } else { print!("\n can't open file {:?}", name); } lines } } #[cfg(test)] mod tests { #[test] fn it_works() { assert_eq!(2 + 2, 4); } } file_utilities module ///////////////////////////////////////////////////////////// // FactoredStructure::Input::file_utilities.rs // // - Input attempts to open named file and return File // // Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 // ///////////////////////////////////////////////////////////// /* This code may be useful for other programs so it is factored into a module here. */ #![allow(dead_code)] use std::fs::*; use std::io::{Read, Error, ErrorKind}; pub fn open_file_for_read(file_name:&str) ->Result<File, std::io::Error> { let rfile = OpenOptions::new() .read(true) .open(file_name); rfile } pub fn read_file_to_string(f:&mut File) -> Result<String, std::io::Error> { let mut contents = String::new(); let bytes_rslt = f.read_to_string(&mut contents); if bytes_rslt.is_ok() { Ok(contents) } else { Err(Error::new(ErrorKind::Other, "read error")) } } test1.rs ///////////////////////////////////////////////////////////// // TypeErasureDataFlowStructure::Input::test1.rs // // - Attempts to return line count from file // // Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 // ///////////////////////////////////////////////////////////// use input::*; use std::fs::*; struct MockCompute {} impl Compute for MockCompute { fn new() -> MockCompute { MockCompute{} } fn do_compute(&mut self, name: &str, _file:File) { print!("\n {} doing mock computation", name); } fn lines(&self) -> usize { 0 } } fn main() { print!("\n -- input::test1 --\n"); let mut inp = Input::<MockCompute>::new(); let name = "./src/lib.rs"; let lines = inp.do_input(name); print!("\n received {} lines from compute", lines); print!("\n\n That's all Folks!\n\n"); } Test Output -- input::test1 -- ./src/lib.rs doing mock computation received 0 lines from compute That's all Folks! cargo.toml [package] name = "input" version = "0.1.0" authors = ["James W. Fawcett"] edition = "2018" # See more keys ... [dependencies] Compute::lib.rs ///////////////////////////////////////////////////////////// // TypeErasureDataFlowStructure::Compute::lib.rs // // - Attempts to read opened file to string, count lines // // Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 // ///////////////////////////////////////////////////////////// /* Note: - creates instance of Output - attempts to read file to string and count its lines - sends results to Output */ use input::{Compute}; use std::fs::*; pub trait Output { fn new() -> Self; fn do_output(&self, name: &str, lines: usize); } mod file_utilities; use file_utilities::read_file_to_string; #[derive(Debug)] pub struct ComputeImpl<Out: Output> { lines: usize, out: Out } impl<Out:Output> Compute for ComputeImpl<Out> { fn new() -> ComputeImpl<Out> { ComputeImpl { lines: 0, out: Out::new() } } fn do_compute(&mut self, name: &str, mut file:File) { let rslt = read_file_to_string(&mut file); if let Ok(contents) = rslt { self.lines = 1; for ch in contents.chars() { if ch == '\n' { self.lines += 1; } } self.out.do_output(name, self.lines); } else { print!("\n could not read {:?}", name); } } fn lines(&self) -> usize { self.lines } } #[cfg(test)] mod tests { #[test] fn it_works() { assert_eq!(2 + 2, 4); } } Module file utilities Module copied from input/src test1.rs ///////////////////////////////////////////////////////////// // TypeErasureDataFlowStructure::Compute::test1.rs // // - Attempts to read opened file to string, count lines // // Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 // ///////////////////////////////////////////////////////////// use compute::*; use input::Compute; use std::fs::*; use std::io::*; struct MockOutput {} impl Output for MockOutput { fn new() -> MockOutput { MockOutput{} } fn do_output(&self, name: &str, lines: usize) { print!("\n sending {} lines to {:?}", lines, name); } } fn open_file_for_read(file_name:&str) -> Result<File> { let rfile = OpenOptions::new() .read(true) .open(file_name); rfile } fn main() { print!("\n -- compute::test1 --\n"); let name = "./src/lib.rs"; let rslt = open_file_for_read(name); if let Ok(file) = rslt { let mut compute = ComputeImpl::<MockOutput>::new(); let _ = compute.do_compute(name, file); } print!("\n\n That's all Folks!\n\n"); } Output -- compute::test1 -- sending 61 lines to "./src/lib.rs" That's all Folks! cargo.toml [package] name = "compute" version = "0.1.0" authors = ["James W. Fawcett"] edition = "2018" # See more keys ... [dependencies] input = { path = "../Input" } Output::lib.rs ///////////////////////////////////////////////////////////// // TypeErasureDataFlowStructure::Output::lib.rs // // - Sends results to console // // Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 // ///////////////////////////////////////////////////////////// use compute::Output; #[derive(Debug)] pub struct OutputImpl {} impl Output for OutputImpl { fn new() -> OutputImpl { OutputImpl {} } fn do_output(&self, name: &str, lines: usize) { print!( "\n file {:?} has {} lines of code", name, lines ); } } #[cfg(test)] mod tests { #[test] fn it_works() { assert_eq!(2 + 2, 4); } } test1.rs ///////////////////////////////////////////////////////////// // TypeErasureDataFlowStructure::Output::test1.rs // // - Sends results to console // // Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 // ///////////////////////////////////////////////////////////// use output::*; use compute::Output; fn main() { print!("\n -- test Output --\n"); let out = OutputImpl::new(); out.do_output("SomeFile.rs", 3); print!("\n That's all Folks!\n\n"); } Test Output -- test Output -- file "SomeFile.rs" has 3 lines of code That's all Folks! cargo.toml [package] name = "output" version = "0.1.0" authors = ["James W. Fawcett"] edition = "2018" # See more keys ... [dependencies] compute = { path = "../Compute" }

3. Epilogue

The fourh design alternatives considered here:
  1. Monolithic Structure
  2. Factored Structure
  3. DataFlow Structure
  4. TypeErase Structure
  5. PlugIn Structure
are progressively more flexible, eventually resulting in reusable components, but also increasingly complex. Where you settle in these alternatives is determined by design context. Is this a one-of-a-kind project that you want to finish quickly or is it heading for production code that will be maintained by more than one developer?
  Next Prev Pages Sections About Keys