iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 22
1

寫在前面

F#在2005年就已經發布第一版,2007年時從研究室移出變成產品部
是Dotnet平台上唯一一款函數導向(Functional Programming)的語言

什麼是函數導向?

跟物件導向就像是物件導向的另一面(不是對面的那種面,比較像是魔術方塊的另外一面)
物件導向的語言強調程式是由一堆物件構築,而函式導向則認為程式由一堆函式所構築,並且強調變數的不可變性
另外一項特點則是函式可以當作一個另一個函式的輸入值或輸出值

那這麼寫有什麼好處嗎?

這麼做的好處是用於數學計算,由於數學面的計算大多都是由各種計算組成
因此相較於將他們轉成物件導向的程式,轉成函數導向的程式會相對輕鬆
所以F#的主要用途會在數學及科學領域

但是F#也不是完全的函數導向語言,你也可以在F#裡面使用物件導向的寫法
讓程式變得更具有彈性,也比其他純函數語言更為易學

同時由於F#生長於Dotnet平台,因此你也可以將需要使用函數語言的方法寫在F#,而其餘的部份寫在C#
這樣也能看到微軟發展Dotnet平台的好處,不必要求一個語言面面俱到,而是將不同的需求使用不同的語言寫成

現在,讓我們開始吧(你可以使用docker或是之前玩C#時安裝的dotnet core)

程式碼

F#跟C#一樣可以使用dotnet的指令建立專案,只是dotnet預設的語言是C#
因此如果要使用F#來開發需要做特別的指令

dotnet new console -lang "F#" -n convert

建立專案

-lang可以指定語言

然後在你的Program.fs貼上以下的程式碼吧

open System

let returnAE input:string = 
    if input<10 then (string input)
    else
        match input with
        | 10 -> "A"
        | 11 -> "B"
        | 12 -> "C"
        | 13 -> "D"
        | 14 -> "E"
        | 15 -> "F"
        |_ -> ""

let t2h input:string =
    let mutable output = ""
    let num = (float input)
    for i = 0 to 15 do
        for j = 0 to 15 do
            for k = 0 to 15 do
                 if Math.Pow(16.0,2.0)*(float i)+Math.Pow(16.0,1.0)*(float j)+Math.Pow(16.0,0.0)*(float k) = num then output <- (returnAE i)+(returnAE j)+(returnAE k)
    output

let AEreturn input =
    match input with
    | 'A' ->  10
    | 'B' ->  11
    | 'C' ->  12
    | 'D' ->  13
    | 'E' ->  14
    | 'F' ->  15
    | _ -> (int input - int '0') //注意char轉型int

let h2t (input:string):int = 
    let mutable output = 0
    let mutable i = 0
    while i < input.Length do
        output <- output + (AEreturn input.[i])*int(Math.Pow(16.0,(float (input.Length-i-1))))
        i <- i+1
    output

[<EntryPoint>]
let main argv =
    printfn "Tell me what you want to do:"
    printfn "(1)T to H (2)H to T"
    let choose = Console.ReadLine(); 

    match choose with
    | "1" -> 
        printfn "Please enter the number:"
        let input = Console.ReadLine(); 
        printfn "%s" (t2h input)
    | "2" ->
        printfn "Please enter the number:"
        let input = Console.ReadLine(); 
        printfn "%A" (h2t input)
    | _ -> 
        printfn "Wrong selection"
    0

執行方式跟C#一樣使用

dotnet run

F#行尾不需要;
方法之類的也不需要{}

程式進入點

雖然F#接近直譯式語言,不管是寫法上還是結構上
但還是有需要註明程式進入點的地方
這個註明方式就是

[<EntryPoint>]

這個註明方式有別種特殊用法,詳細可以看這裡

往下看看主程式在做什麼吧

let main argv =
    printfn "Tell me what you want to do:"
    printfn "(1)T to H (2)H to T"
    let choose = Console.ReadLine(); 

    match choose with
    | "1" -> 
        printfn "Please enter the number:"
        let input = Console.ReadLine(); 
        printfn "%s" (t2h input)
    | "2" ->
        printfn "Please enter the number:"
        let input = Console.ReadLine(); 
        printfn "%A" (h2t input)
    | _ -> 
        printfn "Wrong selection"
    0

跟C++一樣,主程式具有回傳值
在F#裡面回傳值不需要return,只需要放在最後一行,他自己就會回傳了
這裡如果程式沒出錯的話我們就回傳0
開頭的argv則是程式執行時帶入的輸入值,我們這邊不會用到,但是可以去查一下怎麼用

輸入及輸出

printfn "Tell me what you want to do:"
printfn "(1)T to H (2)H to T"
let choose = Console.ReadLine(); 

輸出採用printfn,不用(),但還是需要用""讓F#知道裡面是字串而不是變數
輸入則採用dotnet平台提供的Console.ReadLine(),所以跟C#一樣

模式比對

在F#中沒有傳統的switch,而是使用模式比對match做代替
語法長這樣

match 輸入的參數 with
    | 模式1 -> 
        符合模式1則執行
    | 模式2 ->
        符合模式2則執行
    | _ -> 
        若都沒有符合則執行

看起來跟傳統的switch沒什麼兩樣嘛

理由在於我們只需要進行值的比對,模式比對讓你可以使用下面的方式來寫

let foundX input =
    match input with
    | ("X", "X") -> printfn "Found two X"
    | (var1, var2) & ("X", _) -> printfn "First value is X"
    | (var1, var2)  & (_, "X") -> printfn "Second value is X"
    | _ -> printfn "None X here"

相對於傳統switch這種寫法更具有比對的特性
詳細的用法可以看這邊

比對完成後將執行後續的程式

printfn "Please enter the number:"
let input = Console.ReadLine(); 
printfn "%s" (t2h input)

前面兩行同樣再問一次問題,並且取得要轉換的值
最後一行的printfn則是將後面的值格式化後輸出
結構長這樣

printfn "結構語句" (要填入填充字元的變數)

使用的填充字元為%s而不是C#的{0}

注意後面的方法使用方式

t2h input

使用方法時填入的參數不需要使用(),而是直接放在後面

方法

我們往上一點點看到h2t這個方法吧

let h2t (input:string):int = 
    let mutable output = 0
    let mutable i = 0
    while i < input.Length do
        //printfn "%A" (AEreturn input.[i])
        output <- output + (AEreturn input.[i])*int(Math.Pow(16.0,(float (input.Length-i-1))))
        i <- i+1
    output

這種寫法比較接近於傳統意義上的方法寫法,先來看看結構吧

let 方法名稱 (輸入值:輸入值型別):回傳值型別 =
    方法本體
    回傳值 //最後一行

F#的方法本體並不需要使用{}將他們包起來,而是使用tab確認方法的範圍

這裡的方法宣告跟變數宣告都是使用let這個關鍵字
方法名稱為h2t,輸入值的變數名稱為input,型別為string
而回傳值型別為int,回傳可以不用return這個關鍵字,直接把要回傳的值放在最後一行即可

變數宣告

F#是一個具有自動推斷型別能力的強型別語言
因此你可以使用let讓F#自動推斷變數的型別
要注意的是F#的變數預設是沒辦法改變值的,因此如果你需要累加就要使用mutable這個關鍵字

迴圈 while

F#的while內的條件可以不使用()包起來(如果你需要當然也是可以)

結構為

while 條件 do

同樣不需要使用{}

內部的程式碼

output <- output + (AEreturn input.[i])*int(Math.Pow(16.0,(float (input.Length-i-1))))

並沒有如往常使用

output = output + ~~~

的原因為=在非宣告時其實視為一般常見的比較運算子==
理由是因為F#的變數預設為不可變動,因此也沒有必要留下 = 讓你做變數值變更
雖然編譯時還是會通過,但是意思已經跟原本要表達的意思完全不同了

如果我們需要對可變動的值做變更,就需要<-

陣列

F#的陣列使用

input.[i]

作為取值的方式

而不是如同往常使用

input[i]

自動return的match

我們往上看到用來將十六進位轉成十進位的AEreturn吧

let AEreturn input =
    match input with
    | 'A' ->  10
    | 'B' ->  11
    | 'C' ->  12
    | 'D' ->  13
    | 'E' ->  14
    | 'F' ->  15
    | _ -> (int input - int '0') //注意char轉型int

這裡為什麼沒有return?

這就是函數語言跟物件語言最不同的地方了
函數語言專注於計算的過程跟結果,而不是回傳了什麼

就像是數學函式

f(x) = x + 1

你會關心f(x)會回傳了什麼給你嗎?
不會,因為你只關心你自己因為你只需要專注於算了什麼跟算出的結果
因此在F#會這樣寫

let f x = x + 1

這樣是不是跟傳統的函式寫法很像?

f是方法名稱,x 則是輸入值,x+1則是方法的函式
(所以不是看成 x = x +1 喔, 應該是 f = x +1, x為輸入值)
重點在於x+1這件事情上面
也因此F#不限訂你一定要為輸出值加上型別(注意x後面並沒有:int)

好,說了這麼多我們回來看這個方法的方法本體

match input with
| 'A' ->  10
| 'B' ->  11
| 'C' ->  12
| 'D' ->  13
| 'E' ->  14
| 'F' ->  15
| _ -> (int input - int '0') //注意char轉型int

這裡雖然我們把input拿去比對,之後進行後續的動作
但是我們換個角度想,如果輸入值為'A'這個方法會如何執行?

let AEreturn input =
    // match input with
    | 'A' ->  10 //直接進到這裡
    // | 'B' ->  11
    // | 'C' ->  12
    // | 'D' ->  13
    // | 'E' ->  14
    // | 'F' ->  15
    // | _ -> (int input - int '0') //注意char轉型int

也就是

let AEreturn input = 10 //不是input=10, 而是AEreturn=10 輸入值為input

看懂了嗎?

沒看懂也沒關係,畢竟我們以前都寫物件導向現在寫函式導向會比較難懂
所以

別試著理解他,感受他

因此相對應的returnAE也是類似的寫法

let returnAE input:string = 
    if input<10 then (string input)
    else
        match input with
        | 10 -> "A"
        | 11 -> "B"
        | 12 -> "C"
        | 13 -> "D"
        | 14 -> "E"
        | 15 -> "F"
        |_ -> ""

最後我們看回AEreturn的這段

| _ -> (int input - int '0') //注意char轉型int

比對沒有結果的值將會使用我們之前在C++做過的將字元內的byte相減的作法,
最終得到轉型的結果(注意不是轉型,只是從字元算出相對應的數字)

註解

裡面的//則是單行註解,跟C#相同
多行註解則是

(*
    註解
    註解
    註解
*)

跟C#稍微有點不同

迴圈for

我們看到最後一個沒講過的方法這吧

let t2h input:string =
    let mutable output = ""
    let num = (float input)
    for i = 0 to 15 do
        for j = 0 to 15 do
            for k = 0 to 15 do
                 if Math.Pow(16.0,2.0)*(float i)+Math.Pow(16.0,1.0)*(float j)+Math.Pow(16.0,0.0)*(float k) = num then output <- (returnAE i)+(returnAE j)+(returnAE k)
    output

for的寫法相當簡單易懂

for 變數=起始值to結束值 do

不需要使用()將迴圈值包住,也不用{}將執行內容給包住

邏輯if

我們來看看超級長的這段程式碼吧

if Math.Pow(16.0,2.0)*(float i)+Math.Pow(16.0,1.0)*(float j)+Math.Pow(16.0,0.0)*(float k) = num then output <- (returnAE i)+(returnAE j)+(returnAE k)

我們將他拆解成兩部份,條件句及執行句
條件句長這樣

Math.Pow(16.0,2.0)*(float i)+Math.Pow(16.0,1.0)*(float j)+Math.Pow(16.0,0.0)*(float k) = num

注意到了嗎?
如同我們剛才說的由於變數的不可變動性,我們不需要保留=給變數的指定值,因此這邊的=反而是我們往常見到的==

裡面的Math.Pow是dotnet平台上的寫法,F#你其實可以這樣寫

16.0**2.0*(float i)

計算的數值需要加上.0是因為要讓F#自動判定成float型別,才能進行冪次的計算

執行句的

then output <- (returnAE i)+(returnAE j)+(returnAE k)

則表示將後面算出的三個字串相加後丟入output這個變數裡頭
還記得方法開頭時宣告output為mutable吧

最終,我們將output放在方法的最後一行當作回傳值,完成這個方法

以上就是F#的基本語法拉

我們來複習一下吧

  • 基本結構
    • 使用[]來當作程式進入點的標注
    • 不需要行尾加上;
    • 不需要使用()及{}
  • 印出/讀取
    • printfn來格式化並輸出
    • 輸入則使用dotnet共用的方式Console.ReadLine();
  • 方法的結構
    • 雖然是靜態型別,但是不必提前告知回傳值的型別
    • 使用方法時也不需要()來帶入參數,直接將參數放在後面即可
  • 邏輯控制
    • 使用模式比對替代switch,不需要break; 而且有特殊用法
    • if 使用=而非==作為關係運算子
  • 迴圈控制
    • for 結構為i = 0 to 15 do
    • while 記得加上do
  • 型別
    • 使用強型別系統,必要時需要轉型
  • 冪次計算
    • 輸入值的型別必須是float64
    • 可以使用dotnet平台的Math.Pow()也可以使用**

小結

F#在台灣使用的人真的相當少,情況大概只比Raku好一點
排名甚至在TOBIE上排不進前50(2020.08.30)
而且又是FP(Functional Programming)的概念,因此觀念的部份可能會有寫錯的地方,請不吝糾正

不過寫起來相當舒適,如果用數學函式的方式來看這門語言會覺得相當易寫
或許對於非學界來說用途並不在於寫程式,而是用來當作C#的函式庫來使用
算是MicroSoft正在下的一盤大棋

最後兩門語言我們來介紹google也在下的棋吧
先介紹排名跟F#一樣比較後面的kotlin


上一篇
git 新版本有問題? 那就回到上個版本
下一篇
Kotlin google為了防背刺而留的一手
系列文
你會十五種程式語言?不,我會十五種HelloWorld.為了避免這種狀況,因此寫了這篇:淺入淺出十五種程式語言30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
skycover
iT邦新手 4 級 ‧ 2020-09-22 22:16:28

如果有任何寫不清楚或是觀念沒有很明白的話請留言告知我
會盡快補上

如果有任何寫錯的地方也麻煩留言告知我
會盡快修正

感謝各位

我要留言

立即登入留言