<-- Home |--rust |--rfe

RFE 004: 工程师(粗)Rust入门之值编程范式应用

i

值编程范式

在前面的文章中,我们介绍了Rust的变量使用方式,强调了变量名的重复使用和不可变性。这种方式实际上体现了一种编程范式,称为值编程范式(Value Programming Paradigm)。在这种范式下,程序主要关注值的计算和传递,而不是变量的状态变化。

实际上,我们还差不多掌握了只编程范式所需要的全部工具。

  • let value = if condition { value1 } else { value2 };:条件表达式,根据条件选择值。
  • let value = match expression { pattern1 => value1, pattern2 => value2, _ => default_value };:模式匹配,根据表达式的值选择对应的值。
  • 数组:存储一组同类型的值。
  • 循环:for循环用于遍历数组等集合,或者for i in 0..n用于重复执行代码块。
  • 函数调用:通过传递值作为参数,获取返回值,支持递归调用。

实际上,我们还差一个就是元组(Tuple),元组允许我们将多个不同类型的值组合在一起,形成一个新的值。这是因为,如果我们把函数当作纯函数来看待的话,函数的输入和输出都是值,元组可以帮助我们处理多个输出的情形。

新的知识

元组

元组是一种将多个值组合在一起的数据结构。元组中的值可以是不同类型的,这使得元组非常灵活。元组的大小在编译时确定,不能动态改变。

1let point: (f64, f64, f64) = (1.0, 2.0, 3.0);
2
3let JulianDate: (u64, f64) = (2451545, 0.5); // 2000-01-01 12:00:00 TT
4let (jd_day, jd_fraction) = JulianDate; // 解构元组
5
6println!("Julian Date: {} + {}", jd_day, jd_fraction);

这对于不引入更高级的数据结构(结构体、枚举等)来说,是一个非常方便的工具。

函数与递归

我们可以再来看一下函数调用。函数是程序的基本组成部分,通过函数调用,我们可以将复杂的问题分解为更小、更易处理的子问题。当我们把函数看作纯函数时,函数的输入和输出都是值,这使得函数调用非常适合值编程范式。

对于工程师而言,通过值编程范式,我们可以更容易地理解和分析程序的行为。因为值是不可变的,我们不需要担心变量状态的变化,这使得程序更容易推理和验证。

如果我们没有for循环,我们就只能通过递归函数来实现重复计算。递归函数是指在函数内部调用自身的函数。递归函数通常需要一个基准情况(base case)来终止递归调用。看起来很高端,但是其实还是很凡人的……有时候一个简单的loopwhile循环还是很好用的(需要可变变量的支持)。

一个简单的例子

月相计算

我们来实现一个非常简单的程序,查询月亮的相位。我们从1900年1月0日(1900-01-00)开始计算,到指定日期的月龄(从新月开始计算的天数)。这个计算是天文学中一个经典的问题,涉及到日期转换和周期计算。

首先,我们只针对4个简单的月相。

 1/// Return a human-readable name for a moon phase index.
 2///
 3/// The `nph` parameter is an index in the range 0..=3 that denotes the
 4/// canonical quarter phases used by the algorithms in this module:
 5/// - 0: New Moon
 6/// - 1: First Quarter
 7/// - 2: Full Moon
 8/// - 3: Last Quarter
 9///
10/// Any other value yields "Unknown Phase".
11///
12/// # Examples
13///
14/// ```rust
15/// assert_eq!(crate::calendar::moon_phase(2), "Full Moon");
16/// ```
17pub fn moon_phase(nph: i32) -> &'static str {
18    match nph {
19        0 => "New Moon",
20        1 => "First Quarter",
21        2 => "Full Moon",
22        3 => "Last Quarter",
23        _ => "Unknown Phase",
24    }
25}

Rust的match表达式使得条件判断非常简洁。

  1. 新月(New Moon):月亮完全不可见,位于地球和太阳之间。
  2. 上弦月(First Quarter):月亮的一半可见,右侧亮起。
  3. 满月(Full Moon):月亮完全可见,位于地球的
  4. 下弦月(Last Quarter):月亮的一半可见,左侧亮起。

当然,我们的输入还包括从1900-01-00开始的第几次某月相。

 1/// Compute an approximate Julian day and fractional day offset for a lunar
 2/// phase.
 3///
 4/// This implements a classic lunar phase approximation. Parameters:
 5/// - `n`: lunation index (count of lunations since a reference epoch)
 6/// - `nph`: phase index (0 = New, 1 = First Quarter, 2 = Full, 3 = Last Quarter)
 7///
 8/// Returns a tuple `(jd, frac)` where `jd` is the integral Julian day for the
 9/// phase and `frac` is the fractional day offset (in days) into that day.
10///
11/// The algorithm follows common astronomical approximations and is suitable
12/// for general-purpose calendrical calculations (not for high-precision
13/// ephemeris work).
14pub fn flmoon(n: i64, nph: i32) -> (i64, f64) {
15    const RAD: f64 = std::f64::consts::PI / 180.0;
16    let n = n as f64;
17    let nph = nph as f64;
18    let c = n + nph / 4.0;
19    let t = c / 1236.85;
20    let t2 = t * t;
21    let _as = 359.2242 + 29.105356 * c;
22    let _am = 306.0253 + 385.816918 * c + 0.010730 * t2;
23    let jd = 2415020.0 + 28.0 * n + 7.0 * nph;
24    let xtra = 0.75933 + 1.53058868 * c + (1.178e-4 - 1.55e-7 * t) * t2;
25    let xtra = xtra
26        + if nph == 0.0 || nph == 2.0 {
27            (0.1734 - 3.93e-4 * t) * (_as * RAD).sin() - 0.4068 * (_am * RAD).sin()
28        } else if nph == 1.0 || nph == 3.0 {
29            (0.1721 - 4.0e-4 * t) * (_as * RAD).sin() - 0.6280 * (_am * RAD).sin()
30        } else {
31            panic!("bad nph {}", nph)
32        };
33
34    let i = if xtra > 0.0 {
35        xtra.floor() as i64
36    } else {
37        (xtra - 1.0).ceil() as i64
38    };
39
40    let jd = (jd + i as f64) as i64;
41    let frac = xtra - i as f64;
42
43    (jd, frac)
44}

这个计算的过程非常直接,我们依然采用了值编程范式的方式来实现。所有的计算都是基于值的传递和返回,没有任何变量状态的变化。其中稍微复杂的就是判断的部分,Rust的if表达式同样是一个值,可以直接赋值给变量。

flowchart TD
    A[第几次某月相] --> B[计算Julian Date]
    B --> C[从Julian Date转换到日历日期]

接下来就是Julian Date和日历日期的转换了。首先是日历日到Julian Date的转换:

 1/// Compute the Julian Day Number for a given Gregorian (or Julian) date.
 2///
 3/// Arguments:
 4/// - `month`: 1-based month (1 = January)
 5/// - `day`: day of the month
 6/// - `year`: the year (note: there is no year zero; negative years are BCE)
 7///
 8/// The function follows the common algorithm that handles the Gregorian
 9/// calendar reform (October 1582). Returns the integer Julian day number.
10pub fn julday(month: i32, day: i32, year: i32) -> i64 {
11    const IGREG: i32 = 15 + 31 * (10 + 12 * 1582);
12    let jy = if year == 0 {
13        panic!("julday: there is no year zero");
14    } else if year < 0 {
15        year + 1
16    } else {
17        year
18    };
19
20    let jy = if month > 2 { jy } else { jy - 1 };
21
22    let jm = if month > 2 { month + 1 } else { month + 13 };
23
24    let jd = (365.25 * jy as f64).floor() + (30.6001 * jm as f64).floor() + day as f64 + 1720995.0;
25    let jd = jd as i64;
26
27    if day + 31 * (month + 12 * jy) >= IGREG {
28        let ja = (0.01 * jy as f64).floor() as i64;
29        jd + 2 - ja + (0.25 * ja as f64).floor() as i64
30    } else {
31        jd
32    }
33}

然后是Julian Date到日历日期的转换:

 1/// Convert an integral Julian day number to a calendar date (year, month, day).
 2///
 3/// The returned tuple is `(year, month, day)` using the proleptic Gregorian
 4/// calendar for dates on/after the reform and the Julian calendar before it.
 5pub fn caldat(jd: i64) -> (i32, i32, i32) {
 6    const IGREG: i64 = 2299161;
 7    let ja = if jd >= IGREG {
 8        let jalpha = (((jd as f64 - 1867216.25) / 36524.25).floor()) as i64;
 9        jd + 1 + jalpha - (0.25 * jalpha as f64).floor() as i64
10    } else {
11        jd
12    };
13
14    let jb = ja + 1524;
15    let jc = ((6680.0 + ((jb as f64 - 2439870.0) - 122.1) / 365.25).floor()) as i64;
16    let jd = (365.0 * jc as f64 + (0.25 * jc as f64).floor()) as i64;
17    let je = ((jb - jd) as f64 / 30.6001).floor() as i64;
18
19    let day = jb - jd - (30.6001 * je as f64).floor() as i64;
20    let month = if je > 13 { je - 13 } else { je - 1 };
21    let year = if month > 2 { jc - 4716 } else { jc - 4715 };
22
23    (year as i32, month as i32, day as i32)
24}

最后是一个计算中得到的一天的小数值转化成时分秒的函数:

 1/// Convert a fractional day (0.0..1.0) into hours, minutes, seconds and the
 2/// fractional part of a second.
 3///
 4/// Returns `(hours, minutes, seconds, fractional_seconds)` where `fractional_seconds`
 5/// is the fractional remainder of the second (between 0 and 1).
 6pub fn time_of_day(frac: f64) -> (i32, i32, i32, f64) {
 7    let total_seconds = frac * 86400.0;
 8    let seconds = total_seconds.floor() as i32;
 9    let factor_seconds = total_seconds - (seconds as f64);
10    let hours = seconds / 3600;
11    let minutes = (seconds % 3600) / 60;
12    let seconds = seconds % 60;
13    (hours, minutes, seconds, factor_seconds)
14}

实际上,Rust中还可以很方便的提供测试功能。

测试功能

Rust支持两种测试功能,一种是文档测试,就是把测试算例直接写在文档注释中;另一种是单元测试,把测试代码写在专门的测试模块中。

 1/// Return a human-readable name for a moon phase index.
 2///
 3/// The `nph` parameter is an index in the range 0..=3 that denotes the
 4/// canonical quarter phases used by the algorithms in this module:
 5/// - 0: New Moon
 6/// - 1: First Quarter
 7/// - 2: Full Moon
 8/// - 3: Last Quarter
 9///
10/// Any other value yields "Unknown Phase".
11///
12/// # Examples
13///
14/// ```rust
15/// assert_eq!(crate::calendar::moon_phase(2), "Full Moon");
16/// ```

文档中的测试代码可以直接运行,验证函数的正确性。而单元测试模块中的测试代码则可以包含更复杂的测试逻辑。

 1#[cfg(test)]
 2mod tests {
 3    use super::*;
 4
 5    #[test]
 6    fn test_moon_phase() {
 7        assert_eq!(moon_phase(0), "New Moon");
 8        assert_eq!(moon_phase(1), "First Quarter");
 9        assert_eq!(moon_phase(2), "Full Moon");
10        assert_eq!(moon_phase(3), "Last Quarter");
11        assert_eq!(moon_phase(4), "Unknown Phase");
12    }
13
14    #[test]
15    fn test_flmoon() {
16        let (jd, frac) = flmoon(0, 0);
17        assert_eq!(jd, 2415021);
18        assert!((frac - 0.08598468).abs() < 1e-5);
19    }
20
21    #[test]
22    fn test_julday() {
23        let jd = julday(1, 1, 2000);
24        assert_eq!(jd, 2451545);
25    }
26
27    #[test]
28    fn test_caldat() {
29        let (year, month, day) = caldat(2451545);
30        assert_eq!((year, month, day), (2000, 1, 1));
31    }
32
33    #[test]
34    fn test_time_of_day() {
35        let (h, m, s, fs) = time_of_day(0.5);
36        assert_eq!((h, m, s), (12, 0, 0));
37        assert!((fs - 0.0).abs() < 1e-5);
38    }
39}

主程序

我们的主程序非常简单,本质上也是一种测试。唯一需要注意的就是第一行

1mod calendar;

这一行代码告诉Rust编译器,我们要使用calendar模块中的内容。

 1fn main() {
 2    for n in 0..=100 {
 3        for nph in 0..4 {
 4            let (year, frac) = calendar::flmoon(n, nph);
 5            let moon_phase = calendar::moon_phase(nph);
 6            let (y, m, d) = calendar::caldat(year);
 7            let (h, min, s, ss) = calendar::time_of_day(frac);
 8            print!("{:>4}th {:>15} {:>12} {:>20}", n, moon_phase, year, frac);
 9            println!(
10                " => {:>4}{:>02}{:>02}+{:>02}:{:>02}:{:>02}:{:>12}",
11                y, m, d, h, min, s, ss
12            );
13        }
14    }
15
16    for year in 2025..=2025 {
17        for month in 11..=12 {
18            for day in 1..=31 {
19                let jd = calendar::julday(month, day, year);
20                print!("{:>4}-{:>02}-{:>02} => JD {}", year, month, day, jd);

我们用for循环来构造多个计算任务,逐个打印结果。这个方式基本上能够完成我们日常工程设计的大部分计算任务。本质上,我们目前能够用值和栈内存来完成小规模的计算,大概能跟Fortran差不多快,但是工具链比Fortran现代多了,例如测试的问题,例如模块化的问题。当然,要达到完整地替换Fortran的地步,我们还需要学习更多的内容:例如可以变的变量、传递引用、结构体和枚举、使用堆内存等。

但是,光看到这里,前面的四个文章,已经对Rust中极端无趣的最容易学习的部分进行了介绍。对于工程师而言,已经意思了。其实这个部分,还特别适合给小朋友学习编程。

完整的代码

  1/// Return a human-readable name for a moon phase index.
  2///
  3/// The `nph` parameter is an index in the range 0..=3 that denotes the
  4/// canonical quarter phases used by the algorithms in this module:
  5/// - 0: New Moon
  6/// - 1: First Quarter
  7/// - 2: Full Moon
  8/// - 3: Last Quarter
  9///
 10/// Any other value yields "Unknown Phase".
 11///
 12/// # Examples
 13///
 14/// ```rust
 15/// assert_eq!(crate::calendar::moon_phase(2), "Full Moon");
 16/// ```
 17pub fn moon_phase(nph: i32) -> &'static str {
 18    match nph {
 19        0 => "New Moon",
 20        1 => "First Quarter",
 21        2 => "Full Moon",
 22        3 => "Last Quarter",
 23        _ => "Unknown Phase",
 24    }
 25}
 26
 27/// Compute an approximate Julian day and fractional day offset for a lunar
 28/// phase.
 29///
 30/// This implements a classic lunar phase approximation. Parameters:
 31/// - `n`: lunation index (count of lunations since a reference epoch)
 32/// - `nph`: phase index (0 = New, 1 = First Quarter, 2 = Full, 3 = Last Quarter)
 33///
 34/// Returns a tuple `(jd, frac)` where `jd` is the integral Julian day for the
 35/// phase and `frac` is the fractional day offset (in days) into that day.
 36///
 37/// The algorithm follows common astronomical approximations and is suitable
 38/// for general-purpose calendrical calculations (not for high-precision
 39/// ephemeris work).
 40pub fn flmoon(n: i64, nph: i32) -> (i64, f64) {
 41    const RAD: f64 = std::f64::consts::PI / 180.0;
 42    let n = n as f64;
 43    let nph = nph as f64;
 44    let c = n + nph / 4.0;
 45    let t = c / 1236.85;
 46    let t2 = t * t;
 47    let _as = 359.2242 + 29.105356 * c;
 48    let _am = 306.0253 + 385.816918 * c + 0.010730 * t2;
 49    let jd = 2415020.0 + 28.0 * n + 7.0 * nph;
 50    let xtra = 0.75933 + 1.53058868 * c + (1.178e-4 - 1.55e-7 * t) * t2;
 51    let xtra = xtra
 52        + if nph == 0.0 || nph == 2.0 {
 53            (0.1734 - 3.93e-4 * t) * (_as * RAD).sin() - 0.4068 * (_am * RAD).sin()
 54        } else if nph == 1.0 || nph == 3.0 {
 55            (0.1721 - 4.0e-4 * t) * (_as * RAD).sin() - 0.6280 * (_am * RAD).sin()
 56        } else {
 57            panic!("bad nph {}", nph)
 58        };
 59
 60    let i = if xtra > 0.0 {
 61        xtra.floor() as i64
 62    } else {
 63        (xtra - 1.0).ceil() as i64
 64    };
 65
 66    let jd = (jd + i as f64) as i64;
 67    let frac = xtra - i as f64;
 68
 69    (jd, frac)
 70}
 71
 72/// Compute the Julian Day Number for a given Gregorian (or Julian) date.
 73///
 74/// Arguments:
 75/// - `month`: 1-based month (1 = January)
 76/// - `day`: day of the month
 77/// - `year`: the year (note: there is no year zero; negative years are BCE)
 78///
 79/// The function follows the common algorithm that handles the Gregorian
 80/// calendar reform (October 1582). Returns the integer Julian day number.
 81pub fn julday(month: i32, day: i32, year: i32) -> i64 {
 82    const IGREG: i32 = 15 + 31 * (10 + 12 * 1582);
 83    let jy = if year == 0 {
 84        panic!("julday: there is no year zero");
 85    } else if year < 0 {
 86        year + 1
 87    } else {
 88        year
 89    };
 90
 91    let jy = if month > 2 { jy } else { jy - 1 };
 92
 93    let jm = if month > 2 { month + 1 } else { month + 13 };
 94
 95    let jd = (365.25 * jy as f64).floor() + (30.6001 * jm as f64).floor() + day as f64 + 1720995.0;
 96    let jd = jd as i64;
 97
 98    if day + 31 * (month + 12 * jy) >= IGREG {
 99        let ja = (0.01 * jy as f64).floor() as i64;
100        jd + 2 - ja + (0.25 * ja as f64).floor() as i64
101    } else {
102        jd
103    }
104}
105
106/// Convert an integral Julian day number to a calendar date (year, month, day).
107///
108/// The returned tuple is `(year, month, day)` using the proleptic Gregorian
109/// calendar for dates on/after the reform and the Julian calendar before it.
110pub fn caldat(jd: i64) -> (i32, i32, i32) {
111    const IGREG: i64 = 2299161;
112    let ja = if jd >= IGREG {
113        let jalpha = (((jd as f64 - 1867216.25) / 36524.25).floor()) as i64;
114        jd + 1 + jalpha - (0.25 * jalpha as f64).floor() as i64
115    } else {
116        jd
117    };
118
119    let jb = ja + 1524;
120    let jc = ((6680.0 + ((jb as f64 - 2439870.0) - 122.1) / 365.25).floor()) as i64;
121    let jd = (365.0 * jc as f64 + (0.25 * jc as f64).floor()) as i64;
122    let je = ((jb - jd) as f64 / 30.6001).floor() as i64;
123
124    let day = jb - jd - (30.6001 * je as f64).floor() as i64;
125    let month = if je > 13 { je - 13 } else { je - 1 };
126    let year = if month > 2 { jc - 4716 } else { jc - 4715 };
127
128    (year as i32, month as i32, day as i32)
129}
130
131/// Convert a fractional day (0.0..1.0) into hours, minutes, seconds and the
132/// fractional part of a second.
133///
134/// Returns `(hours, minutes, seconds, fractional_seconds)` where `fractional_seconds`
135/// is the fractional remainder of the second (between 0 and 1).
136pub fn time_of_day(frac: f64) -> (i32, i32, i32, f64) {
137    let total_seconds = frac * 86400.0;
138    let seconds = total_seconds.floor() as i32;
139    let factor_seconds = total_seconds - (seconds as f64);
140    let hours = seconds / 3600;
141    let minutes = (seconds % 3600) / 60;
142    let seconds = seconds % 60;
143    (hours, minutes, seconds, factor_seconds)
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_moon_phase() {
152        assert_eq!(moon_phase(0), "New Moon");
153        assert_eq!(moon_phase(1), "First Quarter");
154        assert_eq!(moon_phase(2), "Full Moon");
155        assert_eq!(moon_phase(3), "Last Quarter");
156        assert_eq!(moon_phase(4), "Unknown Phase");
157    }
158
159    #[test]
160    fn test_flmoon() {
161        let (jd, frac) = flmoon(0, 0);
162        assert_eq!(jd, 2415021);
163        assert!((frac - 0.08598468).abs() < 1e-5);
164    }
165
166    #[test]
167    fn test_julday() {
168        let jd = julday(1, 1, 2000);
169        assert_eq!(jd, 2451545);
170    }
171
172    #[test]
173    fn test_caldat() {
174        let (year, month, day) = caldat(2451545);
175        assert_eq!((year, month, day), (2000, 1, 1));
176    }
177
178    #[test]
179    fn test_time_of_day() {
180        let (h, m, s, fs) = time_of_day(0.5);
181        assert_eq!((h, m, s), (12, 0, 0));
182        assert!((fs - 0.0).abs() < 1e-5);
183    }
184}
 1mod calendar;
 2
 3fn main() {
 4    for n in 0..=100 {
 5        for nph in 0..4 {
 6            let (year, frac) = calendar::flmoon(n, nph);
 7            let moon_phase = calendar::moon_phase(nph);
 8            let (y, m, d) = calendar::caldat(year);
 9            let (h, min, s, ss) = calendar::time_of_day(frac);
10            print!("{:>4}th {:>15} {:>12} {:>20}", n, moon_phase, year, frac);
11            println!(
12                " => {:>4}{:>02}{:>02}+{:>02}:{:>02}:{:>02}:{:>12}",
13                y, m, d, h, min, s, ss
14            );
15        }
16    }
17
18    for year in 2025..=2025 {
19        for month in 11..=12 {
20            for day in 1..=31 {
21                let jd = calendar::julday(month, day, year);
22                print!("{:>4}-{:>02}-{:>02} => JD {}", year, month, day, jd);
23                let (y, m, d) = calendar::caldat(jd);
24                println!("  JD {} => {:>4}-{:>02}-{:>02}", jd, y, m, d);
25            }
26        }
27    }
28
29    let n = 1280 + 23 * 12;
30
31    let (y, f) = calendar::flmoon(n, 2);
32    println!("{}th Full Moon: JD {}, frac {}", n, y, f);
33    let (y, m, d) = calendar::caldat(y);
34    let (h, min, s, ss) = calendar::time_of_day(f);
35    println!(
36        "=> {:>4}-{:>02}-{:>02}+{:>02}:{:>02}:{:>02}:{:>12}",
37        y, m, d, h, min, s, ss
38    );
39
40    let y = calendar::julday(11, 5, 2025);
41    println!("JD of 2025-11-05: {}", y);
42}

结论:仅仅采用值编程,还是可以很容易地实现一些小型的计算任务,我想,小龙应该可以胜任这种任务了。


文章标签

|-->rust |-->engineering |-->tutorial |-->value |-->programming paradigm |-->tuple |-->function |-->recursion


GitHub