接口(OOP)的语义约定是否比功能签名(FP)更具信息意义?


16

有人说,如果您将SOLID原理付诸实践,那么您最终会从事函数式编程。我同意本文的观点,但是我认为从接口/对象到函数/闭包的转换会丢失一些语义,并且我想知道函数式编程如何减轻这种损失。

从文章:

此外,如果您严格地应用接口隔离原理(ISP),您将了解您应该比角色头接口更喜欢角色接口。

如果您继续将设计推向越来越小的界面,那么最终您将获得最终的角色界面:使用单一方法的界面。这在我身上经常发生。这是一个例子:

public interface IMessageQuery
{
    string Read(int id);
}

如果我依赖IMessageQuery,则隐式合同的一部分是调用Read(id)将搜索并返回具有给定ID的消息。

将此与对其等效功能签名的依赖进行比较 int -> string。没有任何其他提示,此功能可能很简单ToString()。如果您使用A实施IMessageQuery.Read(int id)ToString()我可能会指责您故意颠覆性!

那么,函数式程序员可以做什么来保留命名接口的语义?例如,创建具有单个成员的记录类型是常规做法吗?

type MessageQuery = {
    Read: int -> string
}

5
OOP接口更像FP 类型类,而不是单个函数。
9000

2
您可能拥有太多好处,“严格”应用ISP最终以每个接口只能使用一种方法太过分了。
gbjbaanb 2015年

3
@gbjbaanb实际上,我的大多数接口只有一个具有许多实现的方法。您使用SOLID原理的次数越多,您就会看到更多的好处。但这不是这个问题的话题
AlexFoxGill 2015年

1
@jk .:好吧,在Haskell中,它是类型类,在OCaml中,您可以使用模块或函子,在Clojure中,您可以使用协议。无论如何,通常都不会将接口类比限制为单个函数。
9000

2
Without any additional clues...也许这就是为什么文件是合同的一部分
SJuan76

Answers:


8

正如Telastyn所说,比较函数的静态定义:

public string Read(int id) { /*...*/ }

let read (id:int) = //...

从OOP到FP,您并没有真正失去任何东西。

但是,这只是故事的一部分,因为函数和接口不仅在其静态定义中被引用。他们也经过了。假设我们MessageQuery被另一段代码a读取MessageProcessor。然后我们有:

public void ProcessMessage(int messageId, IMessageQuery messageReader) { /*...*/ }

现在我们无法直接看到方法名称IMessageQuery.Read或其参数int id,但是我们可以通过IDE轻松到达那里。更笼统地说,事实上,我们正在通过IMessageQuery函数而不是从int到字符串的方法传递任何接口,这意味着我们要保留id与该函数关联的参数名称元数据。

另一方面,对于我们的功能版本,我们有:

let read (id:int) (messageReader : int -> string) = // ...

那么我们保留和丢失了什么?好的,我们仍然有参数name messageReader,这可能使类型名称(等效于IMessageQuery)成为不必要的。但是现在我们id在函数中丢失了参数名称。


有两种主要解决方法:

  • 首先,通过阅读该签名,您已经可以很好地猜测会发生什么情况。通过使函数简短,简单且具有凝聚力并使用良好的命名,您可以更轻松地了解或查找此信息。一旦我们阅读了实际的函数本身,它将变得更加简单。

  • 其次,它被认为是许多功能语言中的惯用设计,以创建小的类型来包装基元。在这种情况下,情况正好相反- 我们可以使用类型名称替换参数名称,而不是使用参数名称(IMessageQueryto messageReader)替换类型名称。例如,int可以包装为一个名为Id

    type Id = Id of int
    

    现在我们的read签名变为:

    let read (id:int) (messageReader : Id -> string) = // ...
    

    和我们以前一样丰富。

    附带说明一下,这也为我们提供了OOP中的一些编译器保护。而OOP版本保证我们专门花了IMessageQuery而不是任何旧的int -> string功能,在这里我们有,我们正在做一个类似(但不同)的保护Id -> string,而不是任何旧的int -> string


我不太愿意100%地相信这些技术将永远与在界面上提供全部信息一样好,而且内容丰富,但是我想从上述示例中,您可以说,在大多数情况下,我们可以做得很好


1
这是我最喜欢的答案
-FP

10

在执行FP时,我倾向于使用更具体的语义类型。

例如,您对我的方法将变为:

read: MessageId -> Message

与OO(/ java)风格的ThingDoer.doThing()风格相比,它传达了更多的信息


2
+1。此外,您还可以使用Haskell等类型化FP语言将更多属性编码到类型系统中。如果这是一个haskell类型,我会知道它根本不做任何IO(因此可能引用了一些id到消息中的内存映射)。在OOP语言中,我没有该信息-此函数可能会在调用数据库时使数据库振铃。
2015年

1
我不十分清楚为什么这会比Java风格传达更多信息。有什么read: MessageId -> Message告诉你string MessageReader.GetMessage(int messageId)没有呢?
Ben Aaronson

@BenAaronson信噪比更好。如果它是一个OO实例方法,我不知道它在幕后做什么,那么它可能具有任何未表达的依赖关系。正如杰克提到的,当您拥有更强大的类型时,您将拥有更多的保证。
Daenyth 2015年

9

那么,函数式程序员可以做什么来保留命名接口的语义?

使用命名良好的函数。

IMessageQuery::Read: int -> string变得简单ReadMessageQuery: int -> string或类似。

需要注意的关键是,名称只是最广义上的缩写。只有当您和另一个程序员从名称中推断出相同的含义并服从它们时,它们才起作用。因此,您实际上可以使用任何传达隐含行为的名称。OO和函数式编程的名称在稍微不同的位置和形状上略有不同,但是它们的功能是相同的。

接口(OOP)的语义约定是否比功能签名(FP)更具信息意义?

在这个例子中没有。正如我在上面解释的那样,具有单个函数的单个类/接口没有比类似命名的独立函数有意义的多。

一旦在一个类中获得多个功能/字段/属性,就可以推断出有关它们的更多信息,因为您可以看到它们之间的关系。如果这比采用相同/相似参数的独立函数或按名称空间或模块组织的独立函数更具信息性,则存在争议。

就我个人而言,即使在更复杂的示例中,我也不认为OO可以提供更多信息。


2
参数名称呢?
Ben Aaronson

@BenAaronson-他们呢?OO语言和功能语言都允许您指定参数名称,因此很方便。我只是在这里使用类型签名速记来与问题保持一致。用真实的语言看起来像是ReadMessageQuery id = <code to go fetch based on id>
Telastyn

1
好吧,如果函数将接口作为参数,然后在该接口上调用方法,则该接口方法的参数名称很容易获得。如果一个函数取另一个函数作为参数,这是不容易的功能,通过该分配参数名称
本·阿伦森

1
是的,@ BenAaronson,这是函数签名中丢失的信息之一。例如,在中let consumer (messageQuery : int -> string) = messageQuery 5id参数只是一个int。我猜一个论点是您应该传递一个Id,而不是一个int。实际上,这本身将给出一个不错的答案。
AlexFoxGill 2015年

@AlexFoxGill实际上我只是在按照这些思路写东西!
Ben Aaronson

0

我不同意单个功能不能具有“语义契约”。考虑以下法律foldr

foldr f z nil = z
foldr f z (singleton x) = f x z
foldr f z (xn <> ys) = foldr f (foldr f z ys) xn

从什么意义上说这不是语义的还是合同?您无需为“文件夹”定义类型,尤其是因为foldr这些规则是唯一确定的。您确切知道它将要做什么。

如果要使用某种类型的函数,则可以执行相同的操作:

-- The first argument `f` must satisfy for all x, y, z
-- > f x x = true
-- > f x y = true and f y x = true implies x = y
-- > f x y = true and f y z = true implies f x z = true
sort :: forall 'a. (a -> a -> bool.t) -> list.t a -> list.t a;

如果多次需要相同的合同,则只需命名和捕获该类型:

-- Type of functions `f` satisfying, for all x, y, z
-- > f x x = true
-- > f x y = true and f y x = true implies x = y
-- > f x y = true and f y z = true implies f x z = true
type comparison.t 'a = a -> a -> bool.t;

类型检查器不会强制执行您为类型分配的任何语义,因此为每个合同创建新类型只是样板。


1
我认为您的第一点没有解决这个问题-它是一个函数定义,而不是一个签名。当然,通过查看类或函数的实现,您可以分辨出它的作用-问题询问您是否仍可以在抽象级别(接口或函数签名)保留语义
AlexFoxGill

@AlexFoxGill- 虽然它唯一地确定,但它不是函数定义foldr。提示:的定义foldr有两个方程式(为什么?),而上面给出的说明有三个。
乔纳森·

我的意思是文件夹是一个函数而不是函数签名。法律或定义不属于签名的一部分
AlexFoxGill 2015年

-1

几乎所有静态类型的功能语言都有一种别名基本类型的方式,该方式要求您显式声明您的语义意图。其他一些答案也给出了示例。在实践中,经验丰富的功能性需要程序员非常好的理由来使用这些包装类型,因为他们伤害了组合性和可重用性。

例如,假设一个客户想要一个由列表支持的消息查询的实现。在Haskell中,实现可以像这样简单:

messages = ["Message 0", "Message 1", "Message 2"]
messageQuery = (messages !!)

使用newtype Message = Message String它会变得不那么直接,使该实现看起来像:

messages = map Message ["Message 0", "Message 1", "Message 2"]
messageQuery (Id index) = messages !! index

这看起来可能不是什么大不了的事,但你要么做类型转换来回到处,或者在你的代码,一切上面是建立一个边界层Int -> String,那么你将其转换为Id -> Message传递给下一层。假设我想添加国际化,或者全部使用大写格式,或者添加日志记录上下文,等等。这些操作简直很难用组成Int -> String,而烦人的是Id -> Message。并不是在任何情况下都不希望增加类型限制,但是最好还是以烦恼为代价。

您可以使用类型同义词代替包装器(在Haskell type而不是中newtype),这很常见,并且不需要到处都进行转换,但是它不像OOP版本那样提供静态类型保证,只是一点点迷恋。类型包装器通常用于根本不希望客户端操纵值的地方,只需将其存储并传递回去即可。例如,文件句柄。

没有什么可以阻止客户“颠覆性的”。您只是为每个客户创建了一个跳跃循环。一个用于普通单元测试的简单模拟通常需要奇怪的行为,这在生产中是没有意义的。应该编写您的接口,以使它们完全不关心。


1
这听起来更像是对Ben / Daenyth答案的评论-您可以使用语义类型,但是由于不便而不会吗?我没有对你投反对票,但这不是对这个问题的答案。
AlexFoxGill 2015年

-1完全不会损害可组合性或可重用性。如果IdMessage是简单的包装IntString,这是微不足道它们之间的转换。
Michael Shaw

是的,这是微不足道的,但仍然很烦人。我添加了一个示例并描述了类型同义词和包装器的用法之间的区别。
Karl Bielefeldt 2015年

这似乎是一件大事。在小型项目中,无论如何,这些类型的包装器类型可能没有用。在大型项目中,边界自然只占整个代码的一小部分,因此在边界处进行这些类型的转换,然后在其他地方传递包装的类型并不是很繁琐。而且,就像亚历克斯所说的那样,我看不出这是对这个问题的答案。
Ben Aaronson

我解释了如何做到这一点,然后解释了为什么函数式程序员不会这样做。即使不是人们想要的,这也是一个答案。它与大小无关。故意使重用代码变得更加困难的想法在某种程度上是一个OOP概念。创建增加价值的产品类型?每时每刻。创建类型与广泛使用的类型完全一样,只是您只能在一个库中使用它?几乎闻所未闻,除了可能与OOP代码大量交互的FP代码,或者是由最熟悉OOP习惯用法的人编写的。
Karl Bielefeldt 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.