如何在F#中编写Fizzbuzz代码

25

我目前正在学习 F#,并尝试了一个非常简单的 FizzBuzz 示例。

这是我的初始尝试:

for x in 1..100 do 
    if x % 3 = 0 && x % 5 = 0 then printfn "FizzBuzz"  
    elif x % 3 = 0 then printfn "Fizz"
    elif x % 5 = 0 then printfn "Buzz"
    else printfn "%d" x

使用F#解决这个问题,有哪些更加优雅/简单/更好的解决方案(并解释原因)?

注意:FizzBuzz问题是从1到100遍历每个数字,如果是3的倍数则打印Fizz,如果是5的倍数则打印Buzz,如果既是3的倍数又是5的倍数则打印FizzBuzz。否则,只需显示该数字。

谢谢:)


4
看到提供的答案,我对这段代码的难以阅读的程度感到印象深刻。你自己的代码显然是最好的。 - Robert Jeppesen
http://rosettacode.org/wiki/FizzBuzz - Ruben Bartelink
@RobertJeppesen 在这方面,我最喜欢的始终是https://gist.github.com/jaysonrowe/1592432#gistcomment-790724 - msanford
11个回答

60

我认为你已经有了“最好”的解决方案。

如果你想展示更多的功能性/F#特性,你可以做如下操作。

[1..100] 
|> Seq.map (function
    | x when x%5=0 && x%3=0 -> "FizzBuzz"
    | x when x%3=0 -> "Fizz"
    | x when x%5=0 -> "Buzz"
    | x -> string x)
|> Seq.iter (printfn "%s")

使用列表、序列、映射、迭代器、模式和部分应用。

[1..100]    // I am the list of numbers 1-100.  
            // F# has immutable singly-linked lists.
            // List literals use square brackets.

|>          // I am the pipeline operator.  
            // "x |> f" is just another way to write "f x".
            // It is a common idiom to "pipe" data through
            // a bunch of transformative functions.

   Seq.map  // "Seq" means "sequence", in F# such sequences
            // are just another name for IEnumerable<T>.
            // "map" is a function in the "Seq" module that
            // applies a function to every element of a 
            // sequence, returning a new sequence of results.

           (function    // The function keyword is one way to
                        // write a lambda, it means the same
                        // thing as "fun z -> match z with".
                        // "fun" starts a lambda.
                        // "match expr with" starts a pattern
                        // match, that then has |cases.

    | x when x%5=0 && x%3=0 
            // I'm a pattern.  The pattern is "x", which is 
            // just an identifier pattern that matches any
            // value and binds the name (x) to that value.
            // The "when" clause is a guard - the pattern
            // will only match if the guard predicate is true.

                            -> "FizzBuzz"
                // After each pattern is "-> expr" which is 
                // the thing evaluated if the pattern matches.
                // If this pattern matches, we return that 
                // string literal "FizzBuzz".

    | x when x%3=0 -> "Fizz"
            // Patterns are evaluated in order, just like
            // if...elif...elif...else, which is why we did 
            // the 'divisble-by-both' check first.

    | x when x%5=0 -> "Buzz"
    | x -> string x)
            // "string" is a function that converts its argument
            // to a string.  F# is statically-typed, so all the 
            // patterns have to evaluate to the same type, so the
            // return value of the map call can be e.g. an
            // IEnumerable<string> (aka seq<string>).

|>          // Another pipeline; pipe the prior sequence into...

   Seq.iter // iter applies a function to every element of a 
            // sequence, but the function should return "unit"
            // (like "void"), and iter itself returns unit.
            // Whereas sequences are lazy, "iter" will "force"
            // the sequence since it needs to apply the function
            // to each element only for its effects.

            (printfn "%s")
            // F# has type-safe printing; printfn "%s" expr
            // requires expr to have type string.  Usual kind of
            // %d for integers, etc.  Here we have partially 
            // applied printfn, it's a function still expecting 
            // the string, so this is a one-argument function 
            // that is appropriate to hand to iter.  Hurrah!

4
你的答复中包含了很好的教育性评论,Brian! - Luc C
我的第一次尝试基本上看起来像这样,但我忍不住添加了一个“进入”运算符(let (%>) a b = a % b = 0)。 - Jordan Gray

27

我的例子只是在“ssp”发布的代码基础上做了一点改进。它使用参数化活动模式(将除数作为参数传入)。以下是更深入的解释:

下面定义了一个活动模式,我们稍后可以在match表达式中使用它来测试值i是否可被值divisor整除。当我们写:

match 9 with
| DivisibleBy 3 -> ...

这意味着值'9'将作为i传递给以下函数,值3将作为divisor传递。名称(|DivisibleBy|_|)是一种特殊的语法,表示我们正在声明一个活动模式(名称可以出现在match中,并且位于->左侧)。|_|部分表示该模式可能失败(例如,当值不能被divisor整除时,我们的示例会失败)。

let (|DivisibleBy|_|) divisor i = 

  // If the value is divisible, then we return 'Some()' which
  // represents that the active pattern succeeds - the '()' notation
  // means that we don't return any value from the pattern (if we
  // returned for example 'Some(i/divisor)' the use would be:
  //     match 6 with 
  //     | DivisibleBy 3 res -> .. (res would be asigned value 2)
  // None means that pattern failed and that the next clause should 
  // be tried (by the match expression)
  if i % divisor = 0 then Some () else None 

现在我们可以遍历所有数字并使用match(或使用Seq.iter或其他技术,如其他答案中所示)将它们与模式(我们的活动模式)进行匹配:

for i in 1..100 do
  match i with
  // & allows us to run more than one pattern on the argument 'i'
  // so this calls 'DivisibleBy 3 i' and 'DivisibleBy 5 i' and it
  // succeeds (and runs the body) only if both of them return 'Some()'
  | DivisibleBy 3 & DivisibleBy 5 -> printfn "FizzBuzz"
  | DivisibleBy 3 -> printfn "Fizz" 
  | DivisibleBy 5 -> printfn "Buzz" 
  | _ -> printfn "%d" i

关于 F# active patterns 的更多信息,这是一个 MSDN 文档链接。我认为如果您删除所有注释,代码会比原始版本稍微易读一些。它展示了一些非常有用的技巧:-),但在您的情况下,任务相对简单...


+1 我喜欢活动模式“DivisibleBy”的灵活性,可以处理其他除数,并且代码易读。我记下了这一点 :-). - OnesimusUnbound
这个回复终于教会了我活动模式。谢谢,朋友! - Stachu

12

然而,使用F#风格的一个解决方案(即使用活动模式):

let (|P3|_|) i = if i % 3 = 0 then Some i else None
let (|P5|_|) i = if i % 5 = 0 then Some i else None

let f = function
  | P3 _ & P5 _ -> printfn "FizzBuzz"
  | P3 _        -> printfn "Fizz"
  | P5 _        -> printfn "Buzz"
  | x           -> printfn "%d" x

Seq.iter f {1..100}
//or
for i in 1..100 do f i

1
我真的很喜欢这个,但我会使用一个单参数化的 (|DivisibleBy|_|) 活动模式,并让它返回 Some(),这样你就可以跳过匹配中的所有下划线。 - kvb
谢谢@ssp。如果您有机会,能否解释一下每行代码的含义?我对|P3|_|和函数| P3 | P3 _...不是很熟悉。谢谢 :) - Russell
@Russel,Tomas已经解释了它们是如何工作的。这里有一个简短的补充说明:我们可以用任意数量的参数(f.e. (|A|B|_) a b c d e)定义AP,当我们在模式匹配分支中使用它时,我们可以通过那些参数对我们的AP进行参数化,除了最后一个参数外。也就是说,它看起来像我们使用普通函数作为模式(f.e. |A a1 b2 c3 d4 e5 -> ...)。F#将我们的模式与上次定义匹配变量时使用的值绑定在一起调用。(f.e. match 5 with A a b c d e -> ...将被转换为A a1 b2 c3 d4 5 -> ...)并且如果成功,F#将最后一个参数绑定到由AP返回的选项值。 - ssp

11

再提供一种可能的解答,这里是另一种不需要模式匹配的方法。它利用了 Fizz + Buzz = FizzBuzz 这个事实,因此你实际上不需要测试所有三种情况,你只需要看它是否能被 3 整除(然后打印“Fizz”),并且看它是否能被 5 整除(然后打印“Buzz”),最后打印一个新行:

for i in 1..100 do
    for divisor, str in [ (3, "Fizz"); (5, "Buzz") ] do
        if i % divisor = 0 then printf "%s" str
    printfn ""

嵌套的 for 循环在第一次迭代中将3和“Fizz”分配给 divisorstr,然后在第二次迭代中分配第二组值。好处是,如果担心解决方案的可扩展性,您可以轻松添加打印“Jezz”的功能,当该值可被7整除时 :-)。


2
我认为这并不完全有效,因为对于既不能被3整除也不能被5整除的数字,它不会打印任何内容,而在这种情况下应该打印数字本身。 - kvb
@kvb:好观点...我想我可以使用类似 let l = [ for ... do if ... then yield printf str ] 的东西来处理嵌套的 for 循环,然后测试 l <> [](意味着没有找到除数,我们需要打印原始数字)。或者使用 fold,并指定一个布尔状态来说明是否已经打印了某些内容。 - Tomas Petricek

8
这里还有一个例子:
let fizzy num =     
   match num%3, num%5 with      
      | 0,0 -> "fizzbuzz"
      | 0,_ -> "fizz"
      | _,0 -> "buzz"
      | _,_ -> num.ToString()

[1..100]
  |> List.map fizzy
  |> List.iter (fun (s:string) -> printfn "%s" s)

1
+1 我也打算把这个作为我的答案发布。人们经常忘记你可以在元组上进行匹配。没有任何 when-guard 的废话。 - Richiban

7
我认为这篇答案更易读,经过编辑受到其他人的启发。
let FizzBuzz n =
    match n%3,n%5 with
    | 0,0 -> "FizzBuzz"
    | 0,_ -> "Fizz"
    | _,0 -> "Buzz"
    | _,_ -> string n

[1..100]
|> Seq.map (fun n -> FizzBuzz n)
|> Seq.iter (printfn "%s")

这是我的最爱,简洁而简单。 - 9 Guy

1

这是我的版本:

//initialize array a with values from 1 to 100
let a = Array.init 100 (fun x -> x + 1)

//iterate over array and match *indexes* x
Array.iter (fun x ->
    match x with
        | _ when x % 15 = 0 -> printfn "FizzBuzz"
        | _ when x % 5 = 0 -> printfn "Buzz"
        | _ when x % 3 = 0 -> printfn "Fizz"
        | _ -> printfn "%d" x
) a

这是我在 F# 中的第一个程序。

它并不完美,但我认为像我一样刚开始学习 F# 的人可以很快地理解这里发生了什么。

然而,我想知道在上面的模式匹配中,匹配任何 _ 和匹配 x 本身之间有什么区别?


谢谢@Grzegorz,这也是一个好的解决方案。:) 不好意思,我不确定标准中_或x之间的区别。我知道_将匹配任何值,尽管x也会(因为x始终等于x,我想)。 - Russell

1

我找不到一个可行的解决方案,而不需要测试i % 15 = 0。 我一直觉得不测试这个是这个“愚蠢”的任务的一部分。 请注意,由于这是我在该语言中的第一个程序,因此可能不是典型的F#。

for n in 1..100 do 
  let s = seq { 
    if n % 3 = 0 then yield "Fizz"
    if n % 5 = 0 then yield "Buzz" } 
  if Seq.isEmpty s then printf "%d"n
  printfn "%s"(s |> String.concat "")

0
我不喜欢所有这些重复的字符串,这是我自己的:
open System
let ar = [| "Fizz"; "Buzz"; |]
[1..100] |> List.map (fun i ->
    match i % 3 = 0, i % 5 = 0 with
        | true, false ->  ar.[0]
        | false, true ->  ar.[1] 
        | true, true ->  ar |> String.Concat
        | _ -> string i
    |> printf "%s\n"
)
|> ignore

0

这里是一个尝试将模数检查因素化的方法

let DivisibleBy x y = y % x = 0
[ 1 .. 100 ]
|> List.map (function
    | x when DivisibleBy (3 * 5) x -> "fizzbuzz"
    | x when DivisibleBy 3 x -> "fizz"
    | x when DivisibleBy 5 x -> "buzz"
    | x -> string x)
|> List.iter (fun x -> printfn "%s" x)

这是我如何完善它的方法


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接