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)来终止递归调用。看起来很高端,但是其实还是很凡人的……有时候一个简单的loop和while循环还是很好用的(需要可变变量的支持)。
一个简单的例子
月相计算
我们来实现一个非常简单的程序,查询月亮的相位。我们从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表达式使得条件判断非常简洁。
- 新月(New Moon):月亮完全不可见,位于地球和太阳之间。
- 上弦月(First Quarter):月亮的一半可见,右侧亮起。
- 满月(Full Moon):月亮完全可见,位于地球的
- 下弦月(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
- 本站总访问量:loading次
- 本站总访客数:loading人
- 可通过邮件联系作者:Email大福
- 也可以访问技术博客:大福是小强
- 也可以在知乎搞抽象:知乎-大福
- Comments, requests, and/or opinions go to: Github Repository