【问题标题】:summing elements from a user defined datatype对用户定义的数据类型中的元素求和
【发布时间】:2019-02-07 13:56:27
【问题描述】:

在介绍了 f# 中的预定义数据类型(即列表)以及如何对列表或序列的元素求和后,我试图了解如何使用用户定义的数据类型。假设我创建了一个数据类型,称之为 list1:

type list1 = 
    A
  | B of int * list1

地点:

  • A 代表空列表
  • B 通过在另一个列表前面添加一个 int 来构建一个新列表

所以 1,2,3,4,将用 list1 值表示:

B(1, B(2, B(3, B(4, A))))

从 wikibook 中我了解到,使用列表我可以通过以下方式总结元素:

let List.sum [1; 2; 3; 4]

但是如何对用户定义的数据类型的元素求和呢?任何提示将不胜感激。

编辑:我可以利用匹配运算符:

let rec sumit (l: ilist) : int = 
  match l with
  | (B(x1, A)) -> x1
  | (B(x1, B(x2, A))) -> (x1+x2)

sumit (B(3, B(4, A)))

我明白了:

val it : int = 7

我怎样才能做到,如果我有超过 2 个整数,它仍然对元素求和(即 (B(3, B(4, B(5, A)))) 得到 12?

【问题讨论】:

    标签: f# functional-programming f#-interactive


    【解决方案1】:

    解决此类问题的一个很好的通用方法是以单词形式或伪代码形式写出您的算法,然后在您弄清楚您的算法后,将其转换为 F#。在这种情况下,您想对列表求和,看起来像这样:

    找出算法的第一步是仔细定义问题的规范。我想要一个算法来总结我的自定义列表类型。 究竟是什么意思?或者,更具体地说,这对于我的自定义列表类型可以具有的两种不同类型的值(A 和 B)究竟意味着什么?好吧,让我们一次看一个。如果一个列表是 A 类型,那么它代表一个空列表,所以我需要决定一个空列表的总和应该是多少。空列表之和最合理的值是 0,因此规则是“如果列表是 A 类型,则和为 0”。现在,如果列表是 B 类型,那么该列表的总和是什么意思?嗯,B 类型列表的总和将是它的 int 值加上子列表的总和。

    所以现在我们对list1 可以拥有的两种类型中的每一种都有一个“求和”规则。如果A,总和为0。如果B,总和是(值+子列表的总和)。而且这条规则几乎可以逐字翻译成 F# 代码!

    let rec sum (lst : list1) =
        match lst with
        | A -> 0
        | B (value, sublist) -> value + sum sublist
    

    关于这段代码,我想注意几点。首先,您以前可能见过或可能没见过的一件事(因为您似乎是 F# 初学者)是 rec 关键字。在编写递归函数时这是必需的:由于 F# 解析器如何实现的内部细节,如果函数要调用自身,则必须在声明函数的名称和参数时提前声明。其次,这不是编写sum 函数的最佳方法,因为它实际上不是尾递归的,这意味着如果你尝试对一个真的求和,它可能会抛出 StackOverflowException,真的长名单。在您学习 F# 的这一点上,您可能暂时不应该担心这一点,但最终您将学习一种将非尾递归函数转换为尾递归函数的有用技术。它涉及添加一个通常称为“累加器”的额外参数(有时简称为acc),并且上述sum 函数的正确尾递归版本将如下所示:

    let sum (lst : list1) =
        let rec tailRecursiveSum (acc : int) (lst : list1) =
            match lst with
            | A -> acc
            | B (value, sublist) -> tailRecursiveSum (acc + value) sublist
        tailRecursiveSum 0 lst
    

    如果您已经能够理解这一点,那就太好了!如果您还没有到那个时候,请将此答案添加为书签,并在您研究尾递归后回到它,因为这种技术(使用内部函数将非尾递归函数转换为尾递归函数)函数和累加器参数)是一个非常有价值的函数,在 F# 编程中有各种应用。

    【讨论】:

    • 太好了,感谢您的现场解释。您是否建议处理可能的错误| _ -> failwith "error message"?
    • 在这种特定情况下,不,我不这样做,因为匹配表达式涵盖了您的数据类型的所有可能情况。因此,如果您添加了第三种情况 C,您会在匹配表达式中收到一个编译器警告,上面写着“可能未处理的情况 C”,并且您会知道您需要修复您的 sum 函数。如果你有一个像| _ -> 这样的包罗万象的匹配表达式,那么编译器不会警告你C 可能未处理,你会在运行时而不是在编译时发现。这是一件坏事™,因为运行时的错误可能会传递到生产代码中,而编译时错误会被捕获。
    • 使用笼统的| _ -> failwith "error message" 情况是个好主意的一种情况是,如果您有一个您知道的匹配表达式并不能涵盖所有可能的情况。例如,您可能正在检查只能是“A”、“B”或“C”的用户输入。你可以写成match input with | "A" -> handleA() | "B" -> handleB() | "C" -> handleC() | _ -> failwith "Input should be A, B or C"。在这种情况下,catchall 子句是个好主意。
    【解决方案2】:

    除了尾递归之外,泛型编程对于函数式学习器来说可能是一个重要的概念。如果它只能保存整数值,为什么还要麻烦创建自定义数据类型?

    列表的所有元素的总和可以抽象为对列表的所有元素重复应用加法运算符和一个以初始状态为初始状态的累加器。这可以概括为功能折叠:

    type 'a list1 = A | B of 'a * 'a list1
    let fold folder (state : 'State) list =
        let rec loop s = function
        | A -> s
        | B(x : 'T, xs) -> loop (folder s x) xs
        loop state list 
    // val fold :
    //   folder:('State -> 'T -> 'State) -> state:'State -> list:'T list1 -> 'State
    B(1, B(2, B(3, B(4, A))))
    |> fold (+) 0
    // val it : int = 10
    

    同时使sum 函数泛型需要一个称为静态解析类型参数的小黑魔法。签名并不漂亮,它本质上告诉您它希望类型上的 (+) 运算符能够成功编译。

    let inline sum xs = fold (+) Unchecked.defaultof<_> xs
    // val inline sum :
    //   xs: ^a list1 ->  ^b
    //     when ( ^b or  ^a) : (static member ( + ) :  ^b *  ^a ->  ^b)
    B(1, B(2, B(3, B(4, A))))
    |> sum
    // val it : int = 10
    

    【讨论】:

      猜你喜欢
      • 2021-10-31
      • 2019-10-06
      • 2021-06-05
      • 1970-01-01
      • 2021-12-04
      • 2018-12-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多