尾递归映射f#

时间:2015-12-13 07:32:35

标签: f# tail-recursion map-function

我想编写一个尾递归函数,将F#中列表中的所有值乘以2。我知道有很多方法可以做到这一点,但我想知道这是否是一个可行的方法。这纯粹是出于教育目的。我意识到有一个内置功能可以帮我这么做。

 let multiply m =
    let rec innerfunct ax = function
    | [] -> printfn "%A" m
    | (car::cdr) -> (car <- car*2 innerfunct cdr);
  innerfunct m;;



let mutable a = 1::3::4::[]
multiply a

我有两个错误,但我怀疑他们是唯一的问题。

此值在我的第二个匹配条件

时不可变

此表达式是一个函数值,即缺少参数。它的类型是&#39;列表 - &gt;单元。因为我打电话给length a

我对F#相当新,并且意识到我可能没有正确调用该函数,但我无法弄清楚为什么。这对我来说主要是一种学习经验,因此解释比修复代码更重要。语法显然是关闭的,但是我可以通过执行相当于

的操作将* 2映射到列表

car = car*2然后调用列表的cdr上的内部函数。

3 个答案:

答案 0 :(得分:5)

在没有显示中间代码的情况下,我无法轻易解释许多问题,因此我将尝试通过评论重构:

首先,我们沿着可变路径走下去

  1. 由于F#列表是不可变的,原始int也是如此,我们需要一种方法来改变列表中的内容:

    let mutable a = [ref 1; ref 3; ref 4]
    
  2. 摆脱多余的ax并稍微安排一下,我们可以利用这些参考单元格:

    let multiply m =
        let rec innerfunct = function
        | [] -> printfn "%A" m
        | car :: cdr ->
            car := !car*2
            innerfunct cdr
        innerfunct m
    
  3. 我们看到,乘法仅调用其内部函数,因此我们最终得到第一个解决方案:

    let rec multiply m =
        match m with
        | [] -> printfn "%A" m
        | car :: cdr ->
            car := !car*2
            multiply cdr
    

    这实际上只是出于它自己的目的。如果您想要可变性,请使用数组和传统的for循环。

  4. 然后,我们走上不可变的路径

    1. 正如我们在可变世界中所了解到的,第一个错误是由于car不可变。它只是一个不可变列表中的原始int。生活在一个不可改变的世界意味着我们只能从我们的输入中创造出新的东西。我们想要的是构造一个新列表,以car*2为首,然后是递归调用innerfunct的结果。像往常一样,函数的所有分支都需要返回一些相同类型的东西:

      let multiply m =
          let rec innerfunct = function
          | [] ->
              printfn "%A" m
              []
          | car :: cdr -> 
              car*2 :: innerfunct cdr
          innerfunct m
      
    2. 知道m是不可变的,我们可以摆脱printfn。如果需要,我们可以将它放在函数之外,我们可以访问列表的任何地方。它将始终打印相同。

    3. 我们还完成了对列表不可变的引用并获得第二个(中间)解决方案:

      let multiply m =
          let rec innerfunct = function
          | [] -> []
          | car :: cdr -> car*2 :: innerfunct cdr
          innerfunct m
      
      let a = [1; 3; 4]
      printfn "%A" a
      let multiplied = multiply a
      printfn "%A" multiplied
      
    4. 也可以乘以不同的值(该函数毕竟被称为multiply而不是double)。此外,现在innerfunct非常小,我们可以使名称与小范围匹配(范围越小,名称越短):

      let multiply m xs =
          let rec inner = function
          | [] -> []
          | x :: tail -> x*m :: inner tail
          inner xs
      

      请注意,我将因子放在第一位,列表放在最后。这类似于其他List函数,并允许使用部分应用程序创建预定制函数:

      let double = multiply 2
      let doubled = double a
      
    5. 现在剩下的就是让multiply尾递归:

      let multiply m xs =
          let rec inner acc = function
          | [] -> acc
          | x :: tail -> inner (x*m :: acc) tail
          inner [] xs |> List.rev
      
    6. 因此,我们最终(出于教育目的)拥有let multiply' m = List.map ((*) m)

      的硬编码版本

答案 1 :(得分:3)

F#是单程&#39;编译器,因此您可以预期任何编译错误都会在错误下面产生级联效应。如果出现编译错误,请关注该单个错误。虽然您的代码中可能有更多错误(您确实如此),但后续错误可能只是第一个错误的结果。

正如编译器所说,car不可变,所以你可以给它赋值。

在功能编程中,地图可以很容易地实现为递归函数:

// ('a -> 'b) -> 'a list -> 'b list
let rec map f = function
    | [] -> []
    | h::t -> f h :: map f t

然而,这个版本不是尾递归的,因为它在之前以递归方式调用map 它将头部拖到尾部。

您通常可以通过引入内部&#39;来重构尾部递归实现。实现函数,它使用累加器作为结果。这是一种方法:

// ('a -> 'b) -> 'a list -> 'b list
let map' f xs =
    let rec mapImp f acc = function
        | [] -> acc
        | h::t -> mapImp f (acc @ [f h]) t
    mapImp f [] xs

此处,mapImph::t案例中要调用的最后一项操作。

这种实现有点低效,因为它在每次迭代中连接两个列表(acc @ [f h])。根据要映射的列表的大小,可以更有效地使用累加器,然后在最后执行单个反向:

// ('a -> 'b) -> 'a list -> 'b list
let map'' f xs =
    let rec mapImp f acc = function
        | [] -> acc
        | h::t -> mapImp f (f h :: acc) t
    mapImp f [] xs |> List.rev

然而,在任何情况下,完成所有这一切的唯一原因是练习,因为this function is already built-in

在所有情况下,您都可以使用map函数将列表中的所有元素乘以2:

> let mdouble = List.map ((*) 2);;

val mdouble : (int list -> int list)

> mdouble [1..10];;
val it : int list = [2; 4; 6; 8; 10; 12; 14; 16; 18; 20]

但是,通常情况下,我甚至不会明确地定义这样的功能。相反,你使用它内联:

> List.map ((*) 2) [1..10];;
val it : int list = [2; 4; 6; 8; 10; 12; 14; 16; 18; 20]

您可以以相同的方式使用上述所有地图功能。

答案 2 :(得分:0)

您在匹配语句中创建的符号不可变,因此当您与(car::cdr)匹配时,您无法更改其值。

标准功能方式是生成具有计算值的新列表。为此你可以这样写:

let multiplyBy2 = List.map (fun x -> x * 2)
multiplyBy2 [1;2;3;4;5]

这本身不是尾递归(但List.map是)。 如果您确实想要更改列表的值,则可以使用数组。然后你的函数不会产生任何新对象,只需遍历数组:

let multiplyArrayBy2 arr =
    arr
    |> Array.iteri (fun index value -> arr.[index] <- value * 2)

let someArray = [| 1; 2; 3; 4; 5 |]
multiplyArrayBy2 someArray