What is your thinking/design/implementation process when constructing functional programs?

I'd like to improve my process for constructing functional programs while having conversations with my compiler. I've had the thought for awhile that while functional solutions to problems are often easier to read (once constructed), they tend to be slower for me to write, since I'm not quite functional-fluent (yet), and for that reason, tend to be ridden with type errors.

This is my current thinking process, and design approach to constructing functional programs. I'd like advice on how to think better, and that's a bit open ended. At each iteration, I'm trying to figure out what it is that the next step is going to do. I have a general idea at the beginning, and iterate, comfort the screaming type errors, and eventually arrive at a result.

In this particular case, I'm building a 2D array.

fn main() {
    let now = std::time::Instant::now();
    println!("result: {:?}, time: {:?}", e11(), now.elapsed());
}
// took out all but two lines here
const INPUT: &'static str = r" 
08 02 22 97 38 15 00 40 00 75 04 05 07 78 52 12 50 77 91 08 
01 70 54 71 83 51 54 69 16 92 33 48 61 43 52 01 89 19 67 48
";
// Alternative: let buffer = include_str!("../data/p011_grid.txt");
// or use BufReader
fn e11() {
    INPUT
        .lines()
        .skip(1) // discovered: need to skip the first one, which is empty
        .take(5)
        .for_each(|x| println!("lines test {:?}", x));

    INPUT
        .lines()
        .skip(1)
        .map(|x| {
            x.split(' ')
                .map(|x| x.parse().unwrap())
                // x needs a type, but the type can be pretty much whatever: try String, usize,
                .take(1) // column
                .for_each(|x: usize| println!("{:?} parse test", x))
        }) // needs a collect or for_each goes uncalled
        .collect::<()>();

    INPUT
        .lines()
        .skip(1)
        .map(|x| {
            x.split(' ')
                .map(|x| x.parse().unwrap())
                .collect::<Vec<usize>>();
        })
        .take(2)
        .for_each(|x| println!("vec test {:?}", x)); // Result: (); needs a collect?

    let b = INPUT.lines().skip(1).map(|x| {
        x.split_whitespace()
            .map(|x| x.parse().unwrap())
            .collect::<Vec<usize>>();
        // I have a column vector now
    }); // I think I need a collect here
    println!("b test: {:?}", b);

    let c: Vec<()> = INPUT // not Vec<usize>. Says Iterator<Item=()>. Try Vec<()>. Works. But I don't think that's what I want
        .lines()
        .skip(1)
        .map(|x| {
            x.split_whitespace()
                .map(|x| x.parse().unwrap())
                .collect::<Vec<usize>>();
        })
        .collect(); // what type is c now? xD
    println!("b test: {:?}", b);

    let c: Vec<Vec<usize>> = INPUT // Semicolon fix. Now I can get my 2D vector.
        .lines()
        .skip(1)
        .map(|x| {
            x.split_whitespace()
                .map(|x| x.parse().unwrap())
                .collect::<Vec<usize>>() // ah I think this semicolon was in the way
        })
        .collect();
    println!("c test: {:?}", c);

}

playground

I don't think this is something I can give a good answer to, but I can try. I think of it as a sequence of transformations.

  1. Start by creating the initial collection: This is what lines() does.
  2. Each line should be turned into a Vec<usize>. So we need to use map().
  3. How do you turn a single line into a Vec<usize>? Well you parse each token.

Regarding your semicolon: well you just have to know what it means that you got a () and that should lead you to the last map() and hopefully notice the semicolon.

I will note that you can seamlessly avoid your unwrap() by using that Iterator<Item = Result<T, E>> can be collected into Result<Container<T>, E> any time you can collect a sequence of T into Container<T>.

let c: Result<Vec<Vec<usize>>, _> = INPUT
    .lines()
    .skip(1)
    .map(|x| {
        x.split_whitespace()
            .map(|x| x.parse())
            .collect()
    })
    .collect();
println!("c test: {:?}", c.unwrap());
1 Like

I mean, more generally. Know what tools you have available for making your transformations:

  1. Change each item? That's map()
  2. Exclude some items? That's filter(), skip(), take_while() and friends...
  3. Replace each item with variable numbers of items? That's flat_map (and flatten)
  4. Aggregate items into one? That's sum(), product(), max(), min() and more generally fold().
  5. Produce some collection? That's collect()
  6. Run something per item? That's for_each(), inspect() or a for loop. (or collect::<()> I guess)
  7. Search for things? That's find(), position(), all(), any().

Then think about what sequence of operations does what you want. Additionally think about what techniques you have for creating collections. This is stuff like:

  1. Things such as lines() or split_whitespace().
  2. Functions such as iter() or into_iter().
  3. Combining others e.g. zip().
  4. Things such as once(), repeat(), empty() and successors().

And finally your tools may have features you don't know of. E.g. collect() a sequence of results.

2 Likes

That's a very useful list. I didn't know of inspect or find yet. Moreover, it's useful for detailing a couple of common thoughts that aren't yet natural for me.

Per the first reply, that process seems good.

When trading between &types &mut types and types, and bouncing between different levels of indentation, I tend to get lost. That process seems like a nice macro-level pattern to keep things straight.

My feeling right now is that some combination of

  1. the 3 steps you described,
  2. the process I detailed (iteration steps testing the output of each function I'm chaining), and
  3. writing code with fewer indentations (an updated version of the thing I put up there), and then refactoring to the one line

seem to be the best I've figured for approaching the compiler's errors iteratively.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.