F# 函数式编程之 - 一个例子

Author: 102419@gmail.com
Created at: 2020-12-08

经过本系列前面几篇文章对 F# 的介绍,是时候来一个比较小巧的例子了。

将罗马数字转换成普通的十进制数字,完整代码如下:


    module Roman =
        type Digit = I | V | X | L | C | D | M
        type Numeral = Numeral of Digit list
    
        let digitToInt =
            function
            | I -> 1
            | V -> 5
            | X -> 10
            | L -> 50
            | C -> 100
            | D -> 500
            | M -> 1000
    
        let rec digitsToInt =
            function
            | [] -> 0
            | x::y::tail when x < y ->
                (digitToInt y - digitToInt x) + digitsToInt tail
            | digit::tail ->
                digitToInt digit + digitsToInt tail
    
        let print digits = digits |> digitsToInt |> printfn "%A"
    

非常优雅,非常简洁、清晰,可读性强,易扩展易维护,没有变量,不用管理状态,函数没有副作用,不容易出错,而且类型安全,可进行静态类型分析。

上面是一个模块,可以这样使用它:


    open type Roman.Digit
    
    Roman.print [I;I;I;I] // 4
    Roman.print [I;V]     // 4
    Roman.print [V;I]     // 6
    Roman.print [I;X]     // 9
    
    [M;C;M;L;X;X;I;X] |> Roman.print // 1979
    [M;C;M;X;L;I;V] |> Roman.print   // 1944
    

本文介绍了一个比较完整的例子,它像一个小点心,希望你也能和我一样初尝 F# 函数式编程的美味。

更新,补充

上面的例子,我说它易扩展易维护,下面我们就对它进行一次小小的扩展试试。

也许你已经发现,上面的例子,输入的参数是一个数组(列表),而不是一个字符串,这让输入很不方便。下面,我们让它能接受字符串。


    module Roman =
        type Digit = I | V | X | L | C | D | M
        type Numeral = Numeral of Digit list
    
        let digitToInt =
            function
            | I -> 1
            | V -> 5
            | X -> 10
            | L -> 50
            | C -> 100
            | D -> 500
            | M -> 1000
    
        // Digit list -> int
        let rec digitsToInt =
            function
            | [] -> 0
            | x::y::tail when x < y ->
                (digitToInt y - digitToInt x) + digitsToInt tail
            | digit::tail ->
                digitToInt digit + digitsToInt tail
    
        // Numeral -> int
        // 注意,这里对 Numeral 进行了 unpacking, 即从一个 Numeral 里拆出一个 digits 来。
        let toInt (Numeral digits) = digitsToInt digits
    
        type ParsedChar =
            | Good of Digit
            | Bad of char
    
        let parseChar =
            function
            | 'I' -> Good I
            | 'V' -> Good V
            | 'X' -> Good X
            | 'L' -> Good L
            | 'C' -> Good C
            | 'D' -> Good D
            | 'M' -> Good M
            | ch -> Bad ch
    
        // string -> ParsedChar list
        let toDigitList (s:string) =
            s.ToCharArray()
            |> List.ofArray
            |> List.map parseChar
    
        // string -> Numeral
        let toNumeral s =
            toDigitList s
            |> List.choose (
                function
                | Good digit -> Some digit
                | Bad ch ->
                    eprintfn "%c is not a valid character" ch
                    None
                )
            |> Numeral
    
        let print s =
            s |> toNumeral |> toInt |> printfn "%A"
    

可见,原来的代码几乎全部照原样保留,直接添加处理字符串的代码即可,然后新增的函数可以非常轻松地调用原有的函数。我们可以这样使用它:


    open type Roman.Digit
    
    Roman.print "IIII"
    Roman.print "IV"
    Roman.print "VI"
    Roman.print "IX"
    
    "MCMLXXIX" |> Roman.print
    "MCMXLIV" |> Roman.print
    
    "" |> Roman.print
    "IIKKMM" |> Roman.print
    

这个例子还可以继续扩充/改造,因为现在它还不能处理错误的罗马数字,有兴趣的同学请看原文 https://fsharpforfunandprofit.com/posts/roman-numerals/ 我在这里就不补充完整了。

←← →→