程式日常-指令式與宣告式程式語言的差異

分不清指令式與宣告式程式語言?

Lack of direction, not lack of time, is the problem. We all have twenty-four hour days.
― Zig Ziglar

今天要來和各位聊聊指令式(Imperative)和宣告式(Declarative)程式語言,聽起來會有點像講古喔!但,仔細想一想,學習一門外語,最難的並不是會使用那門語言,而是語言背後的文化才是真正難以摸透的。我想,程式語言也一樣!因為它也是「語言」!我相信在了解程式語言的歷史後,對於自己在寫的程式會更有深入的理解,而不是在瞬息萬變的世界下走馬看花。

  • 指令式程式語言(Imperative Programming)
    • 低階指令式
    • 高階指令式
  • 宣告式程式語言(Declarative Programming)
    • 控制流程(Control flow)
    • 資料庫語言(Database Programming)
    • 函式語言(Functional Programming)
  • 結語

指令式程式語言(Imperative Programming)

Imperative programming is a programming paradigm that uses statements that change a program's state.
我們來仔細思考一下這一句擷取自Wiki頁面的敘述。
指令式程式語言是使用陳述式語法去改變程式狀態的一種程式範式(典範)。嗯…相當的文謅謅,可不可以用更直覺的方式來理解指令式程式語言? 沒問題,我們馬上看下面的程式碼:

指令式程式語言有分低階與高階

低階指令式程式語言(Low-Level)

廣義上來說,低階與高階是相對的概念,以現今來看,就像是C++相對於Python語言就能夠劃分成低階與高階。但狹義上來說,我們劃分低階與高階指令式,可以用組合語言與高階語言來劃分。我們看看下方的例子:

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
33
34
35
36
ITER EQU 10           ; number of iterations
OVERHEAD EQU 15 ; 15 for Pentium, 17 for Pentium MMX

RDTSC MACRO ; define RDTSC instruction
DB 0FH,31H
ENDM

.DATA

ALIGN 4
COUNTER DD 0 ; loop counter
TICS DD 0 ; temporary storage of clock
RESULTLIST DD ITER DUP (0) ; list of test results

.CODE
BEGIN: MOV [COUNTER],0 ; reset loop counter

TESTLOOP: ; test loop
;**************** Do any initializations here: ******
FINIT
;**************** End of initializations *************

RDTSC ; read clock counter
MOV [TICS],EAX ; save count
CLD ; non-pairable filler

REPT 8
NOP ; eight NOP's to avoid shadowing effect
ENDM

;**************** Put instructions to test here: ************************
FLDPI
FSQRT
RCR EBX,10
FSTP ST
;********************* End of instructions to test ************************

以上的程式碼是組合語言,在我們的定義上來說就是低階的程式語言。現在前後端工程師比較少碰到組合語言,基本上都是處理硬體、韌體時比較有機會接觸到底層的程式語言,也就是低階指令式語言,大部分的情況都是為了使硬體效能最佳化,才會去撰寫組合語言。不過小弟對於組合語言也是一知半解,如果有誤,還請多多指正!

高階指令式程式語言(High-Level)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>

bool isPalindrome(int x)
{
long reverse = 0;
int compare = x;

if(x < 0)
{
return false;
}
while(x != 0)
{
int popin = x % 10;
x = x / 10;
reverse = reverse*10 + popin;
}
if(reverse == compare)
return true;
else
return false;
}

上面的程式要確認某一個數字是不是回文(Palindrome),舉例來說:12321。上述就是指令式程式語言的一種。可以看到說我們必須一步一步地去規劃程式的每一個步驟。首先,我們建立另外一個新的數值,利用取餘(Mod %)的方式,去反向依序填入x的值,最後在經過數值比對去核對數字是否一樣,若一樣則回傳True,相反則回傳False。這就是現今比較常看到的高階指令式程式語言。

指令式程式語言基本上的概念就像上述所說,可以劃分為低階與高階。特別注意的是,在C語言後出現的C+、C++以及Java都屬於OOP(Object-Oriented Programmin)的程式語言,但它們本質上依舊屬於指令式程式語言,只是又更上一層樓變得更為高階。其實所謂的指令式沒有想像中的難理解,我們日常在料理的過程,本質上一樣是指令式程式語言。倒不如說,指令式程式語言是從現實生活中,逐步發展出來才是正確的理解方式,有點做什麼(What to)的意味。


宣告式程式語言(Declarative Programming)

同樣地,我們來看看Wiki上面如何定義宣告式程式語言。
A style of building the structure and elements of computer programs—that expresses the logic of a computation without describing its control flow.
一種不需描述控制流程即可表達運算邏輯的電腦架構與元素風格。嗯…翻譯的似乎不是很到位,不過反覆思考這段話,什麼是不需描述控制流程(Control flow))的架構? 我們先來看看何謂控制流程(Control flow):

控制流程(Control flow)

控制流程可以用一種方式去理解,那就是「順序」。當程式碼的擺放位置不同時,執行過程就會不同,這就是控制流程的概念。
我們來簡單舉個JS的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const world = 'Hello';
switch (world) {
case 'Hello':
console.log('Hello world!');
break;
case 'hello':
console.log('World hello!);
break;
case 'Hi':
console.log('Hi, world!');
break;
default:
console.log(`Sorry, we are out of ${world}.`);
}

上述程式碼就是控制流程的概念,可以看到如果改動case的順序,程式流向就會改變。我們暫且討論到這邊,因為重點還是在宣告式程式語言。
宣告式語言基本上就是由指令式語言逐步發展過來,因為它的最大區別在於避免副作用(Side Effect)。一般來說,撰寫程式免不了會出現副作用,白話一點就是改了這邊的程式碼,壞了那邊的程式碼的概念,因為副作用的其中一種可能性便是改變程式執行的順序,而導致某些程式片段無法成功讀取定義好的函數、物件、陣列等等。

資料庫語言(Database Programming)

最容易理解也不會令人困惑的非屬於資料庫語言,如:MySQL、PostgreSQL等等的資料庫語言。
同樣舉例來看:
SELECT name FROM tw.male ORDER BY name
這就是宣告式程式語言的一種,因為我們並不知道SELECT以及FROM還有ORDER BY的內部程式碼,就算我們更改name以及tw.male的欄位,都不會對整體程式造成任何副作用,頂多回報格式不符合或是查無資料的訊息。

函式語言(Functional Programming)

函式語言同樣是宣告式程式語言,你正拿來寫網站的JS就是其中的一名。可是你會說寫JS可能會有副作用啊? 沒錯,確實有可能產生,但那只是符合宣告式定義的一種。因為副作用不是必要條件,而是充分條件。換句話說,沒有副作用一定是宣告式程式語言,但有副作用也有可能是宣告式程式語言。直接來看看JS的範例吧!

1
2
3
4
const filterNums = (...args) => {
return args.filter(el => el > 5);
}
console.log(filterNums(1,3,5,7,9,11,13,15,17));

這段程式碼很眼熟對吧! 因為在ES6快速入門那篇文章也有提到。我們仔細看一下回傳過後的那段程式碼return args.filter(el => el > 5);這段程式碼讀來沒有任何問題,但是仔細想想filter是什麼? 它是一個函數,可是函數就不會有副作用嗎? 不一定,但至少在這裡它確實不會有副作用,因為不管傳入什麼樣的陣列,都不會影響filter()本身。


結語

今天概略講過指令式和宣告式程式語言的差別,因為在Udemy上課程時,講師剛好提到React是宣告式程式語言,就很好奇到底什麼是宣告式程式語言。從React是宣告式語言這一點來看,我們可以知道HTML、CSS、JS都可以歸類為宣告式語言,甚至是XML,畢竟JSX就是JS-XML形式的程式碼,具有標記語言的性質。

這一篇希望對能了解這兩個概念的讀者有點幫助,一開始查資料時,覺得內容蠻多而且還有點乏味。不過仔細去品味程式語言的整個發展,就像發現新世界一樣,一切都是環環相扣。礙於篇幅,還有許多沒有提到,如:DSL(Domain-Specific Language)就沒有提到,但是加上DSL其實就會變得不單純。

那今天就先這樣囉! 我們下回見!

組合語言程式片段來源: https://www.csie.ntu.edu.tw/~b5506058/optimize.html