0%

The story about return value and error handling

這故事說起來還真不簡單,在寫Code的時候,往往與這兩樣東西牽扯不清。
例如以下這段Code

1
2
3
FILE *fp = fopen( "input.txt", "r");
if (fp == NULL)
printf (" err %d \n", errno);

fp代表著回傳的正常值,errno表示萬一錯誤的時候,可以供判斷的錯誤依據。
不過就算是return value跟error code,也有很長故事可以說。
$ f(x)=\frac{100}{\frac{100}{x}+1} $當我們的範例,來討論各種處理return value跟error handleing

Global state

在設計API的時候,就完全沒考慮過Error這件事,如同上面的errno那樣,我們可以仿照使用一個global variable。
可能的作法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int _errno;
const int invalid_value = -1;
int g(int x) {
if (x == 0) {
_errno = -1;
return invalid_value;
}
return 100 / x + 1;
}
int f(int x)
{
int v1 = g(x);
if (v1 == invalid_value)
return v1;
if (v1 == 0) {
_errno = -2;
return invalid_value;
}
return 100 / v1;
}

這作法不難懂,不過有幾個很重大的缺點
– 需要對Return Value定義一個特蘇的值,代表這個值是無效的,而這個Return Value有可能跟值域的某個數值碰撞。例如上例的f(-50),你不知道他是錯誤還是有效值。
– global variable表示任何人都有機會更動到,因此在Code的Maintain跟Protect上難很多
– Error很容易被忽略,因為沒有強制性,可能在實做內部,或是外部使用,都有可能因為Code的修改而跳過Error Code處理

因此有了改良版的方法

Return Error and Value simultaneously

上面方法的改良版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int g(int x, int *retvalue) {
if (x == 0)
return -1;
*retvalue = 100 / x + 1;
return 0;
}
int f(int x, int *retvalue)
{
int err = g(x, retvalue);
if (err != 0)
return err;
if (*retvalue == 0)
return -2;
*retvalue = 100 / (*retvalue);
return 0;
}

這邊的寫法類似於COM的作法,把return value放在最後一個參數,而errorcode當作return值傳回。
也可以像golang那樣傳回多個回傳質的方法,大同小異

1
2
3
4
f, err := os.Open("filename.ext")
if err != nil {
log.Fatal(err)
}

這方是解決了上述錢兩個問題,不過還是無法解決第三個問題

Exception-Based solution

越來越多程式語言都加入當標準配備了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int g(int x) {
if (x == 0)
throw std::exception("cannot be zero in g");
return 100 / x + 1;
}
int f(int x)
{
int v1 = g(x);
if (v1 == 0)
throw std::exception("cannot be zero in f");
return 100 / v1;
}
try {
f(0);
}
catch (std::exception e) {
}

比起上述兩種,exception的優點
– 不需要特異定義一堆內部的Error Code Value,並且Code的可讀性比上面兩者都強。可以專住在Logic上的Code。
而缺點是:
– 效能問題,雖然這問題越來越不重要,不過使用Exception的方案通常比Error code慢。
– Boundary issue,當跨越兩個Shared library的程式碼互動食,這方案完全派不上用場。還是要走回上面的老路。

Railway Oriented Programming

錢幾個作法都是遇到問題就往上拋,而ROP反其道而行,將錯誤往後傳,最後統一處理。

這例子改用ustㄉ做示範範例 因為C++诶有ML系Language Pattern Matching和Abstract Data Type,要模擬這個不如直接換個語言寫寫看當練習。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
enum Result {
Value(i32),
Error(&'static str)
}
fn div(x: Result, y: Result) -> Result {
match y {
Result::Value(0) => Result::Error("Cannot divide 0"),
Result::Value(y_) => {
match x {
Result::Value(x_) => Result::Value(x_ / y_),
_ => x
}
},
_ => y
}
}
fn add1(x: Result) -> Result {
match x {
Result::Value(v) => Result::Value(v + 1),
_ => x
}
}
fn f(x: i32) -> Result {
return div(Result::Value(100), add1(div(Result::Value(100), Result::Value(x))));
}
fn main() {
let value = f(25);
match value {
Result::Value(v) => println!("The result value is {}", v),
Result::Error(str) => println!("Error happen, {}", str)
}
}

座個事情很簡單,如果Result是有效的值就繼續往下做,不然就直接往後傳。

Enhance Monad solution for Railway Oriented Programming

網路上有很多Monad的介紹,於是東施校憑寫了一個山寨版的。關鍵在於所謂的bind函數,這裡就不多介紹了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
trait Monad {
fn unit(x: i32) -> Self;
fn bind(&self, f: fn(i32) -> Self) -> Self;
}
enum Result {
Value(i32),
Error(&'static str)
}
impl Monad for Result {
fn unit(x: i32) -> Result {
Result::Value(x)
}
fn bind(&self, f: fn(i32) -> Self) -> Self {
match *self {
Result::Value(x) => f(x),
Result::Error(str) => Result::Error(str)
}
}
}
fn div100(x: i32) -> Result {
match x {
0 => Result::Error("Cannot divide 0"),
_ => Result::Value(100 / x)
}
}
fn add1(x: i32) -> Result {
Result::Value(x + 1)
}
fn f(x: i32) -> Result {
Result::unit(x).bind(div100).bind(add1).bind(div100)
}
~

Reference

C++ Exceptions: Pros and Cons
http://fsharpforfunandprofit.com/rop/