Functional Programming in Rust 您所在的位置:网站首页 rust! Functional Programming in Rust

Functional Programming in Rust

#Functional Programming in Rust| 来源: 网络整理| 查看: 265

We love Haskell, but we also love learning new languages. In this article, we want to show how to use your Haskell knowledge to write Rust code.

We’ll go through the concepts familiar to most Haskell developers, present a few gotchas, and cover these questions:

To what extent is FP possible in Rust? (On the scale from Java streams to doing Lambda Calculus on paper.) Which Haskell concepts map well to Rust, and which don’t? Are there monads in Rust?

This article is written to help Haskellers grasp common Rust concepts faster. For a more detailed comparison of languages, read our Rust vs. Haskell article.

Does Rust follow FP principles?

Rust Book says: “Rust’s design has taken inspiration from many existing languages and techniques, and one significant influence is functional programming”.

This section reviews if Rust adheres to basic functional programming principles.

Immutability

When we typically think about immutability, we think about values that get assigned only once and variables that never change. But this is only part of the picture.

Variables in Rust are immutable by default. We can make them mutable by using the mut keyword.

let immutable_x: i32 = 3; immutable_x = 1; // ^^^^^^^^^^^^^^^ error: cannot assign twice to immutable variable let mut mutable_x = 3; // 3 mutable_x = 1; // 1 mutable_x += 4; // 5

But we get support from the Rust’s ownership model and borrow checker, which guarantee safety in the presence of mutability. Rust has strict rules about borrowing, which affect what operations the owner of the item can perform. The rules guarantee that only one thing has write access to a variable, and whenever you mutate, you don’t have to worry about breaking any reader of the data.

💡 You know how ST allows local mutation?

For example, we can’t suddenly mutate the collection that we’re iterating over:

let mut ints = vec![1, 2, 3]; for i in &ints { // ^^^^^ immutable borrow occurs here println!("{}", i); ints.push(34); // ^^^^^^^^^^^^^ mutable borrow occurs here } // error[E0502]: cannot borrow `ints` as mutable because it is also borrowed as immutable

There is a clear separation between mutable and immutable code, and mutability is contained.

As a result, there is no major use case for persistent data structures in Rust.

💡 A few crates provide immutable collections, for example, im and rpds.

Purity

Pure functions don’t have side effects. In Rust, we can perform arbitrary side effects anywhere and anytime, so don’t expect referential transparency.

💡 A quick reminder: What is referential transparency?

If an expression is referentially transparent, we can replace it with its value without changing the program’s behavior. For instance, the following snippets should be the same:

let a = (a, a) (, )

This means you have to keep an eye on your Rust code. The code might not behave as expected, and refactoring might break the logic.

let a: i32 = { println!("Boom"); 42 }; let result = a + a; println!("Result is {result}"); // Prints: // Boom // Result is 84

If we try to inline, we get different results:

let result = { println!("Boom"); 42 } + { println!("Boom"); 42 }; println!("Result {result}"); // Prints: // Boom // Boom // Result is 84

💡 Also note that you can’t easily copy-paste code around because the scope might affect allocation management.

Totality

Here’s the twist: Rust is way better at totality than Haskell – for instance, Rust doesn’t allow partial field selectors or non-exhaustive pattern matches, and the standard library has fewer partial functions. You can safely get the head of a list, wrapped in Rust’s variant of the Maybe type!

let empty: Vec = vec![]; println!("The head is {:?}", empty.first()); // Prints: The head is None Idiomatic FP in Rust

Functional Rust is frequently just proper, idiomatic Rust. On top of that, thinking in terms of expressions, by-default immutable variables, iterator combinators, Option/Result, and higher-order functions makes Rust appealing to functional programmers.

ADTs and pattern matching

We use struct and enum to declare data types in Rust.

Product types

In Rust, structs represent product types. They come in two forms:

// A tuple struct has no names: struct SimpleMushroom(String, i32); // Creating a tuple struct: let simple = SimpleMushroom("Oyster".to_string(), 7); // A struct has named fields: #[derive(Debug)] struct Mushroom { name: String, price: i32, } // Creating an ordinary struct: let mushroom = Mushroom { name: "Oyster".to_string(), price: 7, }; println!("It costs {}", mushroom.price); // Prints: "It costs 7"

💡 #[derive(Debug)] is similar to deriving Show.

If we want to update a struct as we do in Haskell, we can use struct update syntax to copy and modify it:

let mushroomflation = Mushroom { price: 12, ..mushroom }; println!("{:?}", mushroomflation); // Prints: Mushroom { name: "Oyster", price: 12 }

But it’s more idiomatic to modify the fields of mutable structs:

let mut mushroom = Mushroom { name: "Porcini".to_string(), price: 75, }; mushroom.price = 100; println!("{:?}", mushroom); // Prints: Mushroom { name: "Porcini", price: 100 } Sum types

Enums represent sum types.

#[derive(Debug)] enum Fungi { Edible { name: String, choice_edible: bool }, Inedible { name: String }, }

Rust doesn’t allow accessing a partial field, such as the ones on an enum.

let king = Fungi::Edible { name: "King Bolete".to_string(), choice_edible: true, }; println!("{}", king.name) // ^^^^ // error[E0609]: no field `name` on type `Fungi` Pattern matching

Instead, we can use pattern matching. For example, we can turn the value into a string.

fn to_string(fungi: Fungi) -> String { match fungi { Fungi::Edible { name, choice_edible } if choice_edible => { format!("{name} is tasty!") } Fungi::Edible { name, .. } => { format!("{name} is edible.") } Fungi::Inedible { name } => { format!("{name} is inedible!") } } } let king = Fungi::Edible { name: "King Bolete".to_string(), choice_edible: true, }; println!("{}", to_string(king)); // King Bolete is tasty!

Note that Rust is stricter than Haskell about non-exhaustive patterns.

fn to_string(fungi: Fungi) -> String { match fungi { // ^^^^^ Fungi::Inedible { name } => { format!("{name} is inedible!") } } } // error[E0004]: non-exhaustive patterns: // pattern `Fungi::Edible { .. }` not covered Option and Result

Option corresponds to Maybe:

// Defined in the standard library enum Option { None, Some(T), } let head = ["Porcini", "Oyster", "Shiitake"].get(0); println!("{:?}", head); // Prints: Some("Porcini")

Result corresponds to Either:

// Defined in the standard library enum Result { Ok(T), Err(E), }

There are no monads and no do-notation. We can use unwrap_or() from Option or unwrap_or() from Result to safely unwrap values.

let item = [].get(0).unwrap_or(&"nothing"); println!("I got {item}"); // Prints: I got nothing let result = Err::("Out of mushrooms error").unwrap_or(0); println!("I got {result}"); // Prints: I got 0

Note that there are also unwrap_or_else() and unwrap_or_default().

We can also use the and_then() method to chain (or bind) multiple effectful functions:

let inventory = vec![ "Shiitake".to_string(), "Porcini".to_string(), "Oyster".to_string(), ]; let optional_head = inventory.first().and_then(|i| i.strip_prefix("Shii")); println!("{:?}", optional_head); // Prints: Some("take")

Also, Rust provides the question mark operator (?) to deal with sequences of Result or Option.

use std::collections::HashMap; fn order_mushrooms(prices: HashMap) -> Option { let donut_price = prices.get("Porcini")?; // early return if None let cake_price = prices.get("Oyster")?; // early return if None Some(donut_price + cake_price) } let prices = HashMap::from([("Porcini", 75), ("Oyster", 7)]); let total_price = order_mushrooms(prices); println!("{:?}", total_price); // Prints: Some(82)

An expression that ends with ? either results in the unwrapped success value or short-circuits on failure.

For example, if looking up one of the mushrooms fails, the whole function fails:

let prices = HashMap::from([("Oyster", 7)]); let total_price = order_mushrooms(prices); println!("{:?}", total_price); // Prints: None Newtypes

We can use a tuple struct with a single field to make an opaque wrapper for a type. Newtypes are a zero-cost abstraction – there’s no runtime overhead.

#[derive(Debug)] struct MushroomName(String);

When we want to hide the internals, we can expose methods for converting to and from a newtype. For example:

impl MushroomName { pub fn new(s: String) -> MushroomName { MushroomName(s) } pub fn as_str(&self) -> &str { &self.0 } } let shiitake = MushroomName::new("Shiitake".to_string()); println!("{:?}", shiitake); // MushroomName("Shiitake") println!("{}", shiitake.as_str()); // Shiitake

We can also use traits (covered in the next section) to add this behavior: FromStr and Display.

💡 If the underlying type is not a string, there are other convenient traits you can use: TryFrom and Into.

Polymorphism

Rust traits are siblings of Haskell typeclasses.

We can implement the Display trait for MushroomName, which gives us the to_string() method and the ability to pretty-print values using println! and format!

use std::fmt; impl fmt::Display for MushroomName { fn fmt(&self, f: &mut fmt::Formatter where Self: 'a; fn add_item(&'a mut self) -> Option


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有