module 與 function

February 24, 2022

在〈OpenSCAD CheatSheet〉最後談到了模組與函式,在 OpenSCAD 中,模組用來繪製 2D/3D 圖形,函式用來計算,從函數式角度來看,模組有副作用,函式無副作用。

沒有魔法數字

在談到 OpenSCAD 的模組或函式前,先來看一個重要的觀念,就以〈OpenSCAD CheatSheet〉的範例來說好了:

translate([-5, -5, -5])
linear_extrude(10) 
    text("春", font = "標楷體");

嗯?這範例有什麼問題?10 是什麼?-5 又是什麼?最好為數字取個具意義的名稱!例如:

height = 10;
offset_for_center = -height / 2;

translate([offset_for_center, offset_for_center, offset_for_center])
linear_extrude(height) 
    text("春", font = "標楷體");

這樣在呼叫函式或模組時,看起來會清楚的多。不過也不是只有用數字,若有助於可讀性,值都可以取個具意義的名稱,像是 "春""標楷體" 也可以給予名稱:

height = 10;
offset_for_center = -height / 2;
word = "春";
font = "標楷體";

translate([offset_for_center, offset_for_center, offset_for_center])
linear_extrude(height) 
    text(word, font = font);

在撰寫程式的過程中,每隔一段時間,記得整理一下程式碼,做這類為值取名稱的動作,這可以讓程式碼的可讀性提升,也容易發現可抽取出來重複使用的模組或函式。

定義模組

在發現繪製圖形的程式碼出現重複流程,或者已達階段性目標,封裝為模組就會非常方便,例如,若想將以上程式碼封裝為 chinese_word 模組,第一步可以這麼寫:

height = 10;
offset_for_center = -height / 2;
word = "春";
font = "標楷體";

module chinese_word() {
    translate([offset_for_center, offset_for_center, offset_for_center])
    linear_extrude(height) 
        text(word, font = font);
}

chinese_word();

在 OpenSCAD 使用 module 建立模組,雖然名為模組,不過這邊的模組,不像其他語言中用來管理名稱的模組,OpenSCAD 的模組只是用來定義一個可重複使用的繪圖功能。

上面這個範例,只是使用了 module 區域來包住先前的程式碼而已,這當然不是最後的成果,目的只是要你在下一步,將 translatelinear_extrudetext 使用到的變數,放到 module 的參數列而已,可以的話,順便在參數的順序安排上花一點心思:

height = 10;
offset_for_center = -height / 2;
word = "春";
font = "標楷體";

module chinese_word(word, font, height, offset_for_center) {
    translate([offset_for_center, offset_for_center, offset_for_center])
    linear_extrude(height) 
        text(word, font = font);
}

chinese_word(word, font, height, offset_for_center);

單看 chinese_word(word, font, height, offset_for_center) 這句,是不是清楚多了?進一步地,offset_for_center 好像可以更有彈性一些,若想讓使用者自行決定要不要置中,可以這麼修改:

height = 10;
word = "春";
font = "標楷體";

module chinese_word(word, font, height, center = true) {
    offset_for_center = center ? -height / 2 : 0;
    translate([offset_for_center, offset_for_center, offset_for_center])
    linear_extrude(height) 
        text(word, font = font);
}

chinese_word(word, font, height, center = false);

這邊看到了 center = true,這是預設參數,如果沒有提供 center 參數,那麼就是 true;OpenSCAD 的模組或函式呼叫時,若給的引數個數可以多於參數個數,會發生警訊,如果少於參數個數,沒有被指定引數的參數會是 undef,有時你會需要判斷參數是不是有指定值,以做出相對應的預設動作,這時可以使用 is_undef 來判斷參數值是不是 undef

模組中用了 ?: 三元運算子,? 前如果是 true,就傳回 : 前的值,否則傳回 : 後的值。

接下來如果想將 chinese_word,繞 X 軸旋轉 90 度,只要寫…

rotate([90, 0, 0]) 
    chinese_word(word, font, height, center = false);

這樣的程式碼,是不是比層層縮排要來得容易閱讀,具有彈性的多了?!現在來回顧一下,〈Hello, OpenSCAD!〉的範例:

my_text = "Hello, OpenSCAD!";
step_angle = 30;
radius = 30;
height = 5;

len_of_my_text = len(my_text);

for(i = [0:len_of_my_text]) {
    rotate(step_angle * i) 
    translate([radius, 0, i * 5]) 
    linear_extrude(height) 
        text(my_text[len_of_my_text - i]);
} 

應該看得懂程式碼在做什麼了,自行試著將之封裝為模組吧!

複雜的模型,往往是由許多基礎模型建構而來,就像其他類型的程式中,複雜的任務演算,往往是由許多子任務演算完成;每當覺得模型達到子目標(也就是那種…嗯…接下來可以做下一步了的時候),可以將之建立為模組,久而久之,就會累積起自己的常用模組。

定義函式

OpenSCAD 的模組會有輸出,也就是改變螢幕的狀態,或者是在控制台(console)輸出文字,就函數式的角度而言,可以說模組是有**副作用(Side effect)**的操作。

OpenSCAD 的函式,則是像數學函式的概念,就像是 f(x) = x + 1,你給它 x 為 1,一定是傳回 2,不會有副作用。

簡單來說,如果你有一個數學運算,像是想使用畢氏定理求斜邊長好了,就可以使用 OpenSCAD 的函式:

length_side1 = 10;
length_side2 = 20;

function length_hypotenuse(length_side1, length_side1) = 
    sqrt(pow(length_side1, 2) + pow(length_side2, 2));
    
echo(length_hypotenuse(length_side1, length_side1));  // ECHO: 22.3607

建立函式使用 function,當然不只有能建數學公式,只要是給一些引數,傳回一個值的演算,都可以使用 function 來定義;另外,對於 pow 函式,也可以使用 ^ 運算子來取代。例如:

function length_hypotenuse(length_side1, length_side1) = 
    (length_side1 ^ 2 + length_side2 ^ 2) ^ 0.5;

若需要變數來代表計算結果,可以使用 let,例如,定義計算兩點間距離的函式:

function length_between(point1, point2) = 
    let(
        x = point1.x - point2.x, 
        y = point1.y - point2.y,
        z = point1.z - point2.z
    )
    (x ^ 2 + y ^ 2 + z ^ 2) ^ 0.5;
    
echo(length_between([1, 2, 3], [3, 2, 1])); // ECHO: 2.82843

let 可以使用的地方很多,並不限於函式,像是 ?: 或者 list 表示式 等,若需要變數來代表計算結果,都可以適當地使用。

在 OpenSCAD 裡,[1, 2, 3, 4, 5] 可用來建立一個 list,可以透過索引來存取元素,索引由 0 開始;由於 list 也常用來表示點或向量,例如用 [10, 20, 30] 表示點座標,如果有個 p = [10, 20, 30],雖然可以使用 p[0]p[1]p[2] 來取得 x、y、z,不過 OpenSCAD 可以直接透過 p.xp.yp.z 這樣的 dot 表示法。

另一點要記得的是,if/else 在 OpenSCAD 是陳述句(statement),與 for 相同,使用它們意謂著有副作用;在 function 若想有 if/else 的效果,可以使用 ?: 三元運算,例如定義一個 max 函式:

function max(a, b) = a >= b ? a : b;

function 中要有巢狀的 if/else 效果,也是組合多個 ?: 運算。例如:

function compare(a, b) = a > b ? 1 : (a < b ? -1 : 0);

不過這樣有點難以閱讀,我個人都是排版為以下風格,比較容易閱讀:

function compare(a, b) = 
    a > b ?  1 : 
    a < b ? -1 : 
             0; // default value

需要重複性計算的演算怎麼辦?使用遞迴!

「遞迴只應天上有,凡人只能用迴圈」是嗎?其實只要任務單一,每次遞迴只專注當次子任務的分解,不管前後任務之狀態,遞迴並不困難!

方才的範例有看到 len 函式,可以用來計算文字的長度,假設現在沒有 len 函式可以用,可以自行定義,記得,只要看當次任務該解什麼就好了,那麼,計算文字長度的當次任務是什麼,不就是「看看有沒有字元,有就在計數上加 1」:

function length(lt, count = 0) = 
    is_undef(lt[count]) ? count : length(lt, count + 1);

echo(length("TEST"));  // ECHO: 4

OpenSCAD 的字串可以透過索引來取得各個字元,就 length 的函式本體來說,就是將 count 當成索引,取 lt 中的字元,也就是「看看有沒有字元」,如果超出索引就是 undef,雖然可以使用 == 來比較 undef,不過建議使用 is_undef 函式,比較具有可讀性,若不是 undefcount + 1,也就是「有就在計數上加 1」,下次計數就是下一次呼叫 length 的事了。

echo 與 assert

在程式運行過程中,有時會想知道某些計算的值是否正確,可以使用 echo 模組來指定的值輸出,例如:

echo(1, 2, 3);         // ECHO: 1, 2, 3

x = 10;  
echo(x);               // ECHO: 10
echo(str("x = ", x));  // ECHO: "x = 10"
echo(x = x);           // ECHO: x = 10

第四個 echo 是個特別的用法,= 左邊會被當成文字顯示,= 右邊取 x 真正的值,這會比第三個 echo 先用 str 組裝字串後顯示的方式便利。

echo 也可以被安插在運算式或函式呼叫之前,這時會將指定的訊息顯示出來,運算式或函式呼叫的結果則直接傳遞,例如:

p1 = [10, 20];
p2 = [30, 40];

length = let(v = p2 - p1) echo(v) sqrt(v * v); // ECHO: v = [20, 20]
echo(length); // ECHO: 28.2843

如果 echo 後面沒有運算式或函式呼叫,那麼 echo 執行結果 undef

foo = echo("XD"); // ECHO: "XD"
echo(foo);        // ECHO: undef

有時在程式執行過程中,你想斷言必須處於某種狀態,這時可以使用 assert,例如:

// 經過一番運算…
foo = expr;

assert(foo != 0, "foo 必須不為 0");

assert 的第一個引數若為 true,什麼事都不會發生,若為 false 就會發生 ERROR: Assertion 並中止程式,如果有指定第二個引數,就會在引發 ERROR: Assertion 時一併顯示。

echo 也可以被安插在運算式或函式呼叫之前,如果斷言成功,運算式或函式呼叫的結果直接傳遞,例如:

p1 = [10, 20];
p2 = [30, 40];

length = let(v = p2 - p1) assert(v != [0, 0]) sqrt(v * v); 
echo(length); // ECHO: 28.2843

如果 assert 後面沒有運算式或函式呼叫,那麼 assert 執行結果 undef

分享到 LinkedIn 分享到 Facebook 分享到 Twitter