【问题标题】:What's the functional programming's answer for overriding?函数式编程的覆盖答案是什么?
【发布时间】:2014-05-12 11:11:47
【问题描述】:

我对来自 OOP 和 C# 背景的函数式编程和 F# 都很陌生,我注意到函数式编程方法通常是静态的,并且根据类型在模块中(我可以理解为什么)。

例如,要使用list,您可以使用List 模块中的函数,使用option 可以使用Option 模块等。

但是,当我接受基类的参数,而派生类有更合适的实现时会发生什么?

在 OOP 中,将调用适当的实现,这是派生的实现,即使对象静态地属于基类,因为该成员已被覆盖

在函数式编程中会怎样?

如果您将基类型作为参数(应该如此),则您只能使用函数来处理它,就像您只能在 OOP 中调用基的成员一样。
唯一不同的是,在 OOP 中,被调用的成员是合适的。

【问题讨论】:

  • 我觉得这可能更适合programmers.stackexchange.com
  • 通过根据基类和派生类来构建问题,您已经将面向对象注入其中。因此,任何解决方案要么违反纯函数式编程,要么在上下文中简单地采用标准的面向对象原则。 (注意:OCaml 有像 ++. 这样的运算符来处理整数和浮点数。)
  • @Shredderroy 是的,这是真的,我明白你的意思。但是我该如何处理现有的类层次结构?它已经存在于 F# 中,例如集合中。例如,Seq 模块并不总是合适且快速的选择,不是吗?
  • 你说的模块,完全没有关系。它们只是碰巧有具有相同名称和相似语义的函数。至于现有的类层次结构,您仍然可以在 F# 中继续使用普通的 OOP,如果您愿意的话。然而,我有时会加倍努力,实际上以更代数数据类型的方式重写一些类,并使用对它们进行操作的函数。我发现,添加类的灵活性通常不如添加函数的能力。
  • @DanielFabian 哦,好吧,我明白了。那么如何采用一个采用序列并返回第 1000 个元素的方法呢?我怎么写呢?我可以使用Seq.nth,这将导致 O(1000),但是我有可以使用 O(1) 和 Array.get 执行相同操作的数组。如前所述,OOP 做到了这一点,但是功能性呢?问题是它已经存在于 F# 中,所以甚至在我开始之前,我就已经在这个 OOP 领域了。

标签: c# oop f# functional-programming overriding


【解决方案1】:

你描述的覆盖场景,有一个基类绑定到子类型Polymorphism。在您的情况下,您可以拥有一个通用接口,例如IEnumerable。 这种多态性在 OOP 中很常见,但在 FP 中并不常见,它更多地依赖于其他两种类型的多态性:Ad-Hoc 和 Parametric。

这方面的一个例子是类型类,在 Haskell 中,你可以让函数接受类型 T 的任何参数,只要类型 T 是类型类 C 的实例,但你的函数可以以通用方式实现>您仍然可以通过添加仅适用于该特定实例 T 的函数来“覆盖”该通用定义,当使用该特定类型调用此类函数时,通用实现将被覆盖,请注意这将在编译时而不是编译时知道-时间。

一个简单的例子是 Functors 和 Monads,fmap 函数可以在函数 bindreturn 方面以任何 Monad 的通用方式实现,但您可以添加 @987654329 的特定实现@ 到list 和/或option,比通用的更高效。

我不是 100% 确定,但我认为在 C# 中您可以在 Linq 中进行这种优化,因为您可以为 SelectMany 定义更多重载,这相当于 bind

不幸的是,在 F# 中没有这样的内置机制,尽管有一些方法可以对此进行编码(请参阅 FsControl 中的默认方法),但 F# 不仅仅是功能性的,它是多范式并且存在于 .NET 世界中(一个 OOP 世界),因此您可以覆盖 SubTyping。

话虽如此,值得一提的是,这更多是一种技术观点,但在大多数情况下,在设计方面与 OOP 中的覆盖不是一对一的,因为这比 SubTyping 更通用。我的意思是,如果您将 OOP 设计迁移到 FP 并将类层次结构更改为假设联合,那么您的泛型方法很可能会在 match (带有下划线的那个)中结束 | _ -> ) 以及特定情况下的覆盖。

更新

以下是您在 cmets 中的问题的答案:

type MyType = 
    static member Nth (x:seq<_>) = fun n -> printfn "General case"; Seq.nth n x 
    static member Nth (x:_ [])   = fun n -> printfn "Override for []"; x.[n]

let bigSeq = seq {1..10000000}
let bigLst = [ 1..10000000 ]
let bigArr = [|1..10000000|]

MyType.Nth bigSeq 9000000
// General case
// Real: 00:00:00.217, CPU: 00:00:00.218, GC gen0: 0, gen1: 0, gen2: 0

MyType.Nth bigArr 9000000
// Override for []
// Real: 00:00:00.001, CPU: 00:00:00.000, GC gen0: 0, gen1: 0, gen2: 0

MyType.Nth bigLst 9000000
// General case
// Real: 00:00:00.080, CPU: 00:00:00.078, GC gen0: 0, gen1: 0, gen2: 0

但没有 [] 的覆盖:

MyType.Nth bigArr 9000000
// General case
// Real: 00:00:00.052, CPU: 00:00:00.046, GC gen0: 0, gen1: 0, gen2: 0

这就是覆盖在临时多态(重载)中的工作原理。

通过使用 .NET 重载方法,您可以覆盖,但您可能会争辩说,由于重载已在调用站点解决,因此您无法在该调用之上定义另一个泛型函数。

p>

现在假设我有一个类型类Collection,它表示可以向上转换为seq 的所有类型,还有一个函数nth,如果我在F# 中有类型类,我将能够编写如下内容:

// General implementation relaying in IEnumerable<_>
let nth n (x:Collection<_>) = Seq.nth n

// Specific implementation for arrays
let nth n (x:_ []) = x.[n]

不幸的是,这不会编译,因为 F# 中的重载仅适用于方法,而不适用于函数,而且目前 F# 中没有类型类,但正如我提到的,有一些解决方法,FsControl 我可以写 @987654324 @ 它实际上会编译并允许我运行相同的测试。

在同样的场景下,你将如何在 OOP 中编写代码,因为你无法访问 Seq 和 Array 的源代码?在这种情况下,使用子类型多态性您将无法覆盖任何内容。

【讨论】:

  • 能否请您参考一下我在 cmets 中向 Daniel 提出的问题?我问:那么采用一个采用序列并返回第 1000 个元素的方法怎么样?我怎么写呢?我可以使用Seq.nth,这将导致 O(1000),但是我有可以使用 O(1) 和 Array.get 执行相同操作的数组。如前所述,OOP 做到了这一点,但是功能性呢?问题是它已经存在于 F# 中,所以甚至在我开始之前,我就已经在这个 OOP 领域了。
  • @Ken 查看更新中的答案。
【解决方案2】:

您不一定会以这种方式对其进行建模。考虑一下;有区别的联合对应于整个类层次结构,而不仅仅是一个类。因此,您在可区分联合上定义的任何函数都已经对应于基类包括所有覆盖上的方法。情况就是这样,因为可区分联合对数据案例的扩展是关闭的,但对添加新功能是开放的。

通常在 OOP 中,类为添加功能而关闭,但为添加新数据案例而开放。

更接近于可区分联合上的 FP 样式函数的是类似访问者模式的 moke。并且模式匹配是在访问者中完成的动态调度的(严格)更强大的版本。

此外,在 F# 中泛型比在 C# 中发挥更大的作用,因此您通常没有任何关于所讨论类型的信息,除了这些信息,这些信息在逻辑上来自您的代码。例如,您经常根据基类编码,而是根据泛型类型'a

编辑:关于编写一个返回第 n 个元素的函数。同样,您通常要么已经知道所有集合类型,例如

type 'a Collection =
    | Array of 'a array
    | List of 'a list
    | Seq of 'a seq

在这种情况下,您可以始终将集合包装到三种情况之一,并且您可以为这两种情况提供特殊实现,您可以做一些有意义的事情,例如ListArray 同时为更简单的情况提供一些默认值。我喜欢将 DU 视为案例定义。通常分支逻辑非常适合这种世界观。所以我会以这样的方式编写一个nth 函数,以便我处理这些情况,我可以有意义地做一些事情。并将我一无所知的情况保留为默认值。

在函数式编程中,您较少关注通过继承的子类型多态性(典型的 OOP 风格,“我不知道在运行时会有什么具体实现”),而是特别关注参数多态性(即泛型)多态性(重载)。

看下面:

[1..10] |> List.sumBy (+)
[1.0..10.0] |> List.sumBy (+)

在这里,您在两种情况下都使用相同的+ 函数。尽管它实际上是一种不同的类型。

当您注意到某段代码在两个不同的地方是相同的时,很有可能在 FP 中,您可以抽象该段代码并使您的代码更通用和可重用。因此,即使像 List.map 这样的函数只是一个具体的函数,它实际上可以在大量的上下文中使用并表现“多态”,通过你提供特定的行为,即一个函数,这是需要的在这种情况下。

OOP 风格的多态性和那些高阶函数之间的区别在于,您可以在需要的地方传入您需要的行为,而不是作为类层次结构的一部分。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-07-07
    • 2010-11-10
    • 2010-09-13
    • 1970-01-01
    • 2018-09-21
    • 1970-01-01
    • 1970-01-01
    • 2010-11-04
    相关资源
    最近更新 更多