How to use named closures effectively to simplify function implementation?

The code below does not compile but it demonstrates the general idea of the question. There are many borrowing/lifetime issues that appear (due to capturing/moves etc.) even though if all functions were inlined, the issues would not apply. I assume fn cannot be used inside with the goal to capture?

Should I instead group the state in a struct and write an impl for it?

async fn verify_torrent(torrent: &Torrent, root_dir: &str) -> Vec<u64> {
    async fn open_file(idx: u64) -> Option<File> {
        if idx as usize >= torrent.files.len() {
            return None;
        }
        let file_path = os::join_path(root_dir, &torrent.files[idx as usize].path);
        if !os::file_exists(&file_path) {
            return None;
        }
        Some(io::open(&file_path, fmRead))
    }

    let mut file_idx = 0;
    let mut file = open_file(file_idx).await;
    let mut piece_idx = 0;
    let mut piece_buffer = vec![0; torrent.piece_length as usize];
    let mut read_bytes_count = 0;

    async fn next_file(file: &mut Option<File>) {
        if let Some(f) = file {
            f.close();
        }
        file = open_file(file_idx).await;
    }

    fn has_next_piece() -> bool {
        file_idx as usize < torrent.files.len() && piece_idx < torrent.piece_count
    }

    fn next_piece() {
        piece_idx += 1;
        read_bytes_count = 0;
    }

    fn is_last_piece() -> bool {
        piece_idx == torrent.piece_count - 1
    }

    fn piece_torrent_start() -> u64 {
        piece_idx * torrent.piece_length
    }

    fn piece_torrent_end() -> u64 {
        if is_last_piece() {
            torrent.total_length
        } else {
            piece_torrent_start() + torrent.piece_length
        }
    }

    fn piece_length() -> u64 {
        piece_torrent_end() - piece_torrent_start()
    }

    let file_starts = torrent.files.iter().map(|f| f.length).scan(0, |acc, x| {
        *acc += x;
        *acc
    });

    fn file_torrent_start() -> u64 {
        file_starts[file_idx as usize]
    }

    fn file_torrent_end() -> u64 {
        file_starts[(file_idx + 1) as usize]
    }

    fn valid_hash() -> bool {
        if is_last_piece() {
            piece_buffer.resize(read_bytes_count as usize, 0);
        }
        sha1::digest(&piece_buffer) == torrent.piece_hashes[piece_idx as usize]
    }

    fn file_start_pos() -> u64 {
        if piece_torrent_start() > file_torrent_start() {
            piece_torrent_start() - file_torrent_start()
        } else {
            0
        }
    }

    fn file_end_pos() -> u64 {
        std::cmp::min(piece_torrent_end(), file_torrent_end()) - file_torrent_start()
    }

    fn file_ends_before_piece_starts() -> bool {
        file_torrent_end() <= piece_torrent_start()
    }

    async fn read_bytes_from_file(f: &mut File) -> u64 {
        if f.get_file_pos() != file_start_pos() as i32 {
            f.set_file_pos(file_start_pos() as i32);
        }
        let bytes_read = f.read_bytes(&mut piece_buffer, read_bytes_count as usize, (file_end_pos() - file_start_pos()) as usize).await as u64;
        read_bytes_count += bytes_read;
        bytes_read
    }

    fn not_all_read_from_file(bytes_read: u64) -> bool {
        file_end_pos() - file_start_pos() != bytes_read
    }

    fn piece_loaded() -> bool {
        read_bytes_count == piece_length()
    }

    fn file_exhausted(f: &File) -> bool {
        f.end_of_file()
    }

    fn mark_missing_piece() {
        result.push(piece_idx);
    }

    let mut result = Vec::new();

    while has_next_piece() {
        if file_ends_before_piece_starts() {
            next_file(&mut file).await;
        } else if file.is_none() || not_all_read_from_file(read_bytes_from_file(&mut file.as_mut().unwrap()).await) {
            mark_missing_piece();
            next_piece();
        } else if piece_loaded() {
            if !valid_hash() {
                mark_missing_piece();
            }
            next_piece();
        } else if file_exhausted(&file.as_ref().unwrap()) {
            next_file(&mut file).await;
        }
    }

    if let Some(f) = file {
        f.close();
    }

    result
}

I assume fn cannot be used inside with the goal to capture?

Correct. Function items simply cannot capture anything at all, under any conditions.

You can create a named local closure by closure syntax and let:

let mut i = 0;
let mut f = || { i += 1; i };

But you are likely to encounter trouble with captures (since no two closures can borrow and mutate the same data) and with properly defining the function signatures (closure syntax is limited, especially around async).

Should I instead group the state in a struct and write an impl for it?

With a problem this complex, yes. This way you don't have (as many) borrowing problems because instead of indefinitely borrowing the state variables, the struct is borrowed anew for each method call.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.