Recursion 递归较少的函数式编程?

Recursion 递归较少的函数式编程?,recursion,f#,Recursion,F#,我目前在使用F#的函数编程方面做得相当不错。然而,当F#/函数式编程社区中似乎有更好的习惯用法时,我倾向于使用递归进行大量编程。因此,本着学习的精神,有没有更好/更惯用的方法来编写下面的函数而不使用递归 let rec convert line = if line.[0..1] = " " then match convert line.[2..] with | (i, subline) -> (i+1, subline) else

我目前在使用F#的函数编程方面做得相当不错。然而,当F#/函数式编程社区中似乎有更好的习惯用法时,我倾向于使用递归进行大量编程。因此,本着学习的精神,有没有更好/更惯用的方法来编写下面的函数而不使用递归

let rec convert line =
    if line.[0..1] = "  " then
        match convert line.[2..] with
        | (i, subline) -> (i+1, subline)
    else
        (0, line)
结果如下:

> convert "asdf";;
val it : int * string = (0, "asdf")
> convert "  asdf";;
val it : int * string = (1, "asdf")
> convert "      asdf";;
val it : int * string = (3, "asdf")

使用尾部递归,它可以写成

let rec convert_ acc line =
    if line.[0..1] <> "  " then
        (acc, line)
    else
        convert_ (acc + 1) line.[2..]
let convert = convert_ 0
让rec转换为acc行=
如果第[0..1]行为“”,则
(行政协调会,行)
其他的
转换(acc+1)行。[2..]
让convert=convert_0

不过,仍在寻找非递归答案。

因为您以非统一的方式遍历字符串,所以在本例中,递归解决方案更合适。为了可读性,我将重写您的尾部递归解决方案,如下所示:

let convert (line: string) =
    let rec loop i line =
        match line.[0..1] with
        | "  " -> loop (i+1) line.[2..]
        | _ -> i, line
    loop 0 line
既然你问了,这里有一个(奇怪的)非递归解决方案:)


递归是用函数式语言编写循环的基本机制,因此,如果您需要迭代字符(就像在示例中所做的那样),那么递归就是您所需要的

如果您想改进代码,那么您可能应该避免使用
行。[2..]
,因为这样做效率很低(字符串不是为这种处理而设计的)。最好将字符串转换为列表,然后进行处理:

let convert (line:string) = 
  let rec loop acc line =
    match line with
    | ' '::' '::rest -> loop (acc + 1) rest
    | _ -> (acc, line)
  loop 0 (List.ofSeq line)
您可以使用标准库中的各种函数以更短的方式实现这一点,但它们通常也是递归的(您只是看不到递归!),因此我认为使用像
Seq.unfold
Seq.fold
这样的函数仍然是递归的(它看起来比您的代码复杂得多)

使用标准库的一种更简洁的方法是使用
TrimLeft
方法(参见注释),或者使用标准F#library函数,执行以下操作:

let convert (line:string) =
  // Count the number of spaces at the beginning
  let spaces = line |> Seq.takeWhile (fun c -> c = ' ') |> Seq.length
  // Divide by two - we want to count & skip two-spaces only
  let count = spaces / 2
  // Get substring starting after all removed two-spaces
  count, line.[(count * 2) ..]
编辑关于字符串与列表处理的性能,问题是切片分配了一个新字符串(因为在.NET平台上字符串就是这样表示的),而切片列表只会更改引用。下面是一个简单的测试:

let rec countList n s = 
  match s with 
  | x::xs -> countList (n + 1) xs
  | _ -> n

let rec countString n (s:string) =
  if s.Length = 0 then n
  else countString (n + 1) (s.[1 ..])


let l = [ for i in 1 .. 10000 -> 'x' ]
let s = new System.String('x', 10000)

#time 
for i in 0 .. 100 do countList 0 l |> ignore    // 0.002 sec (on my machine)
for i in 0 .. 100 do countString 0 s |> ignore  // 5.720 sec (on my machine)

这里有一种编写函数的更快方法——它显式地检查字符,而不是使用字符串切片(正如Tomas所说,这很慢);它也是尾部递归的。最后,它使用一个StringBuilder来创建“过滤”字符串,一旦输入字符串达到合适的长度,它将提供更好的性能(尽管对于非常小的字符串,由于创建StringBuilder的开销,它会稍微慢一点)


你应该明白什么是尾部递归。谢谢@BasileStarynkevitch。我相信我能理解。在这种情况下,我将对变量
I
使用累加器,并将调用转换为尾部调用。不过,我想看看是否有不同的方法。可能使用了某种折叠或展开。懒惰的回答-
fun x->(x.Length-x.TrimStart().Length),x
同意(除以2),@JohnPalmer。但是你知道我的意思:)@JohnPalmer它需要更多的调整:-)Muhammad的版本计算两个空格的数量,并且在从开头删除所有两个空格后返回字符串(不仅仅是
x
)。但是,是的,
TrimStart
是一种.NET方式,它只是稍微复杂一点:)。谢谢。第二个解决方案本质上是我的答案,形式更为精练。首先,这就是我想要的。但有一个问题:你说我以统一的方式遍历字符串,如何遍历?什么类型的问题比先展开后折叠写得更好?使用
unfold
fold
的版本看起来有点可怕:-)使用
Seq.reduce
甚至在新版本中
Seq.last
这是最后一行的功能,可以将
fold
位写得更短无论如何。@MuhammadAlkarouri:No.
Seq.fold
在空序列上不会失败,而
Seq.reduce
Seq.last
则会失败。@TomasPetricek:所以它很好地达到了目的:)。我试图说服大家,在这种情况下,递归解决方案更好。我可能应该看看字符串切片是如何完成的。我很惊讶它没有效率。我的直觉是,不可变字符串的切片应该是一个非常便宜的O(1)操作。有什么理由不这样吗?@MuhammadAlkarouri有关性能的更多信息,请参阅编辑。
let rec countList n s = 
  match s with 
  | x::xs -> countList (n + 1) xs
  | _ -> n

let rec countString n (s:string) =
  if s.Length = 0 then n
  else countString (n + 1) (s.[1 ..])


let l = [ for i in 1 .. 10000 -> 'x' ]
let s = new System.String('x', 10000)

#time 
for i in 0 .. 100 do countList 0 l |> ignore    // 0.002 sec (on my machine)
for i in 0 .. 100 do countString 0 s |> ignore  // 5.720 sec (on my machine)
let convert' str =
    let strLen = String.length str
    let sb = System.Text.StringBuilder strLen

    let rec convertRec (count, idx) =
        match strLen - idx with
        | 0 ->
            count, sb.ToString ()

        | 1 ->
            // Append the last character in the string to the StringBuilder.
            sb.Append str.[idx] |> ignore
            convertRec (count, idx + 1)

        | _ ->
            if str.[idx] = ' ' && str.[idx + 1] = ' ' then
                convertRec (count + 1, idx + 2)
            else
                sb.Append str.[idx] |> ignore
                convertRec (count, idx + 1)

    // Call the internal, recursive implementation.
    convertRec (0, 0)