Skip to Content

Converting F# to Rust: Remaining expressions and functions

Converting F# to Rust: Remaining expressions and functions

I am back with the final post on converting an existing F# app to Rust. The discriminated union is a crucial piece, and the last remaining part that will be difficult is the function that converts my data structure into a set of files and paths to create. Since the data structure lends itself to recursion, that is the essence of this last function. Before we dive into that function, let’s show the other pieces. I will use the heading to describe what is happening, and both code snippets are below. If you see anything, you would like to ask about don’t hesitate reach out to me.

Generic functions used in the snippets below

F#

let toTuple a b = (a,b)

Rust

fn convert_strings_to_files(file_names: &[&str]) -> Vec<Folder> {
    file_names
        .into_iter()
        .map(|name| Folder::File(name.to_string()))
        .collect()
}

Creating the envs folder

F#

let envsFolder = 
    ["dev.tfvars"; "qa.tfvars"; "prod.tfvars"]
    |> List.map File
    |> toTuple "envs"
    |> Folder

Rust

fn generate_envs_folder() -> Folder {
    let env_files = convert_strings_to_files(&["dev.tfvars", "qa.tfvars", "prod.tfvars"]);
    Folder::Folder(String::from("envs"), env_files)
}

Creating the service folder

F#

let service serviceName =
    [File "main.tf"; File "variables.tf"; File "output.tfvars"; envsFolder]
    |> toTuple serviceName
    |> Folder

Rust

fn generate_service_folder(name: &str) -> Folder {
    let envs_folder = generate_envs_folder();
    let mut files = convert_strings_to_files(&["main.tf", "variables.tf", "output.tf"]);
    files.push(envs_folder);
    Folder::Folder(name.to_string(), files)
}

Creating the infrastructure folder

F#

let infrastructureFolder serviceFolder = Folder("infrastructure", [serviceFolder])

Rust

fn generate_infrastructure_folder(app_name: &str) -> Folder {
    let service_folder = generate_service_folder(app_name);
    Folder::Folder("infrastructure".to_string(), vec![service_folder])
}

The Piece De Resistance

This function is the most exciting piece of the whole app outside of the data structure. The function converts the data structure that was built and generate the full paths from the structure. Since the data structure is recursive, this is a recursive function.

Here it is in F#:

let rec folderToString folder =
    match folder with
    | File fileName -> [fileName]
    | Folder (name, []) -> [name + "/"]
    | Folder (name, otherFolders) ->
        otherFolders |> List.map folderToString |> List.collect (fun s -> s |> List.map (fun x -> name + "/" + x))

The function in Rust ended up with a list inside of a list. After about an hour fighting with the compiler, I realized that I needed to use a flat map instead.

Here is the working Rust version:

fn generate_paths(filesystem: Folder) -> Vec<PathBuf> {
    let file_paths = match filesystem {
        Folder::File(x) => vec![PathBuf::from(x)],
        Folder::Folder(folder, folders) => folders
            .into_iter()
            .flat_map(|path| generate_paths(path))
            .collect::<Vec<PathBuf>>()
            .into_iter()
            .map(|path| PathBuf::from(&folder).join(path))
            .collect(),
    };
    file_paths
}

The IO bits

F#

let createFile (fileName: string) = 
    Directory.CreateDirectory(Path.GetDirectoryName(fileName)) |> ignore
    File.Create(fileName).Close()

Rust

fn create_path(path: PathBuf) -> std::io::Result<()> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?
    }
    File::create(path)?;
    Ok(())
}

The Main function, the entrypoint

Nothing extraordinary here.

F#

[<EntryPoint>]
let main argv =
    match Array.tryHead argv with
    | Some serviceName -> serviceName |> service |> infrastructureFolder |> folderToString |> List.iter createFile
    | None -> printfn "No service name provided!"
    0 // return an integer exit code

Rust

fn main() -> std::io::Result<()> {
    let filesystem = generate_infrastructure_folder("myapp");
    let paths = generate_paths(filesystem);
    for path in paths {
        create_path(path)?;
    }

    Ok(())
}

All of it together

Here is the complete application in both languages.

F#

open System.IO

type Folder = File of string | Folder of string * Folder list

let toTuple a b = (a,b)

let envsFolder = 
    ["dev.tfvars"; "qa.tfvars"; "prod.tfvars"]
    |> List.map File
    |> toTuple "envs"
    |> Folder

let service serviceName =
    [File "main.tf"; File "variables.tf"; File "output.tfvars"; envsFolder]
    |> toTuple serviceName
    |> Folder

let infrastructureFolder serviceFolder = Folder("infrastructure", [serviceFolder])

// type FolderPrinter = FolderPrinter of (Folder -> string list)


let rec folderToString folder =
    match folder with
    | File fileName -> [fileName]
    | Folder (name, []) -> [name + "/"]
    | Folder (name, otherFolders) ->
        otherFolders |> List.map folderToString |> List.collect (fun s -> s |> List.map (fun x -> name + "/" + x))

let createFile (fileName: string) =
    Directory.CreateDirectory(Path.GetDirectoryName(fileName)) |> ignore
    File.Create(fileName).Close()


[<EntryPoint>]
let main argv =
    match Array.tryHead argv with
    | Some serviceName -> serviceName |> service |> infrastructureFolder |> folderToString |> List.iter createFile
    | None -> printfn "No service name provided!"
    0 // return an integer exit code

Rust

use std::fs;
use std::fs::File;
use std::path::PathBuf;

enum Folder {
    File(String),
    Folder(String, Vec<Folder>),
}

fn convert_strings_to_files(file_names: &[&str]) -> Vec<Folder> {
    file_names
        .into_iter()
        .map(|name| Folder::File(name.to_string()))
        .collect()
}

fn generate_envs_folder() -> Folder {
    let env_files = convert_strings_to_files(&["dev.tfvars", "qa.tfvars", "prod.tfvars"]);
    Folder::Folder(String::from("envs"), env_files)
}

fn generate_service_folder(name: &str) -> Folder {
    let envs_folder = generate_envs_folder();
    let mut files = convert_strings_to_files(&["main.tf", "variables.tf", "output.tf"]);
    files.push(envs_folder);
    Folder::Folder(name.to_string(), files)
}

fn generate_infrastructure_folder(app_name: &str) -> Folder {
    let service_folder = generate_service_folder(app_name);
    Folder::Folder("infrastructure".to_string(), vec![service_folder])
}

fn generate_paths(filesystem: Folder) -> Vec<PathBuf> {
    let file_paths = match filesystem {
        Folder::File(x) => vec![PathBuf::from(x)],
        Folder::Folder(folder, folders) => folders
            .into_iter()
            .flat_map(|path| generate_paths(path))
            .collect::<Vec<PathBuf>>()
            .into_iter()
            .map(|path| PathBuf::from(&folder).join(path))
            .collect(),
    };
    file_paths
}

fn create_path(path: PathBuf) -> std::io::Result<()> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?
    }
    File::create(path)?;
    Ok(())
}

fn main() -> std::io::Result<()> {
    let filesystem = generate_infrastructure_folder("myapp");
    let paths = generate_paths(filesystem);
    for path in paths {
        create_path(path)?;
    }

    Ok(())
}

Conclusion

I have more things to add to this app, and I will blog about those features as I add them. I will also announce the name of this app and do a release shortly. I hope you are finding this helpful and exciting. I am enjoying Rust.

Thanks for reading,

Jamie

If you enjoy the content, then consider buying me a coffee.