从函数式迁移到面向对象的问题
我习惯于使用函数式编程(主要是 Haskell),并且我从 OO(scala)开始。
我在翻译代码时遇到了麻烦。例如,这是我对 B 树的 Haskell 定义:
data BTree a =
Leaf
|Node2 (BTree a) a (BTree a)
|Node3 (BTree a) a (BTree a) a (BTree a)
deriving (Eq,Read,Show)
它非常简单。我的树是空的,或者它有一个值并且是两棵树的父亲,或者它是 3 个子树的父亲。
OO 上有什么?我不知道。我只是不知道如何才能以理智的方式做到这一点。
I'm used to work with functional programming (mainly Haskell) and I'm beginning with OO (scala).
I'm having troubles on translating my code. For instance, that's my Haskell definition of a B-tree:
data BTree a =
Leaf
|Node2 (BTree a) a (BTree a)
|Node3 (BTree a) a (BTree a) a (BTree a)
deriving (Eq,Read,Show)
It's quite simple. My tree is empty, or it has a value and is a father of two trees or it is a father of 3 sub trees.
What is it on OO? I have no clue. I just can't figure out how can I do it in a sane way.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
这里有一些很好的答案,但我认为他们都错过了展示您所缺少的观点的机会。因此,您已经展示了这一点:
并询问如何以面向对象的方式实现这一点。所以,这就是:
最重要的事情
所以,事情是这样的:当你谈到面向对象时,你有一个 BTree、一棵手指树或任何其他树结构不相关< /em>。事实上,它应该隐藏。相关的是您可以用它做什么。
您在执行此操作时遇到困难,因为您正在从不应该的方向处理问题。
不那么重要的事情
现在,您实际上提供了一个实现,但 BTree 的细节完全隐藏了。您只能使用
Tree
定义的方法。这是理想的面向对象架构:客户端依赖于接口,数据结构是隐藏的。
Some good answers here, but I think they all miss the opportunity to show the point you are missing. So, you have shown this:
And asked how do you implement this in an object-oriented fashion. So, here it is:
Most important thing
So, here's the deal: when you speak of object orientation, that you have a BTree, a finger tree, or whatever other tree structure is irrelevant. In fact, it should be hidden. What is relevant is what you can do with it.
You are having trouble doing this because you are approaching the problem from precisely the direction you shouldn't.
The not-so-important thing
Now here you actually offer an implementation, but the details of the BTree are completely hidden. You only get to use the methods that
Tree
has defined.This is the ideal object oriented architecture: clients depend on interfaces, data structure is hidden.
这是从函数式思维方式转向面向对象的第一步:对象更像函数而不是数据。这样想它们;现在,您拥有的是不透明的抽象行为,而不是作用于透明的结构化数据的函数。
从“好吧,这是我的数据结构,现在我......”的角度思考它是倒退的。
尝试这样的事情:
首先弄清楚可以使用 B 树执行哪些基本操作(不要忘记诸如
show
和fmap
此处)并基于这些设计类。对于像树这样的总和类型,将基类留空并使用子类来实现数据构造函数的不同变体会更容易。作为 OO 中的经验法则,任何地方您必须做出某种选择来彻底改变后续行为,请强烈考虑使用子类型多态性来区分情况。
除非必要,否则尽量不要担心内部表示,并且不要让表示细节泄漏到类之外。拥有一堆返回原始类型的 GetFoo() 方法是做错事的标志。
最后:请记住您使用的是 Scala。它是一种混合语言是有原因的;并非所有事情都以 OO 风格进行是有意义的。翻阅《设计模式》一书,您会发现其中一半内容是关于针对缺失的语言功能的巴洛克式的、高维护性的解决方法。
Here's a first step for getting to OO from a functional mindset: Objects are more like functions than like data. Think of them as such; instead of functions acting on transparent, structured data, now you have opaque globs of abstract behavior.
Thinking of it in terms of "okay, here is the structure of my data, now I..." is going about it backwards.
Try something like this:
Start by figuring out what are the fundamental actions that can be done with your B-tree (don't forget things like
show
andfmap
here) and design the class based on those.For a sum type like your tree, it can be easier to leave the base class empty and use subclasses for different variations on the data constructors. As a rule of thumb in OO, anywhere you have to make some sort of choice that drastically changes the subsequent behavior, strongly consider using subtype polymorphism to distinguish cases.
Try not to worry about the internal representation until you have to, and don't let representation details leak out of the class. Having a bunch of GetFoo() methods that return primitive types is a sign of Doing It Wrong.
And finally: Remember that you're using Scala. It's a hybrid language for a reason; not everything makes sense to do in OO style. Flip through the Design Patterns book and you'll find that half of it is about baroque, high-maintenance workarounds for missing language features.
既然你的标签列表中有 Scala,那么在 Scala 中是如何完成的:
你有一个基本特征(在 Haskell 中是类型),并从所有 Haskell 构造函数派生为
case
类。这样您也可以在 Scala 模式匹配中使用它们。在 Java(自 1.5 起)、C++ 和 C# 等语言中,您可以使用相同类型的模板来帮助实现类型安全。它们基本上像 Haskell 中的类型变量一样工作。
此示例使用 Scala,但对于其他 OO 语言,您可以采用类似的方式进行操作:创建一个抽象基类并将数据的构造函数转换为类/对象。
Since you have Scala in your tag-list, here is how it would be done in Scala:
You have a base trait (in Haskell the type), and derived from that all the Haskell constructors as
case
classes. That way you can use them in Scala pattern matching as well.In languages like Java (since 1.5), C++ and C# you have the same kind of templates to help type safety. They basically work like the type variables in Haskell.
This example is in Scala, but for other OO languages you would do it in a similar way: Create an abstract base class and turn the constructors of your data into classes/objects.
定义“理智”。实际上,这很酷:这是我第一次看到有人在从函数式转向面向对象而不是其他方式时遇到困难。
事实上,你将会有更多的事情要做;OO;这就是功能性良好的原因之一。您遇到的是功能性具有优势的特定情况。
在面向对象的语言中(这并不意味着是任何特定的语言,只是伪代码),您将需要一个节点类
,然后您可以使用方法来添加节点作为子节点,这样您就可以做一些事情与它。
然后使用 Node 创建一个 BTree 类来执行插入、平衡等操作。
Define "sane". Actually, this is cool: it's the first time I've seen someone having trouble going from functional to OO rather than the other way.
The truth is that you're going to have more stuff to do it OO; that's one reason functional is nice. You're hitting a specific case in which functional has advantages.
In an OO language (this isn't meant to be any specific language, just pseudocode) you're going to need a class for node
and then you have methods to, for example, add a node as a child so you can do things with it.
Then you create a BTree class using Node to do insertion, balancing, and so on.
好的,休克疗法时间到了:Java。您的类型 BTree 成为类层次结构的顶部。没有类型类,您可以重写 equals 和 toString 方法(不过,Read 没有等效方法)。然后将所有函数作为方法放入对象中(通常是 BTree 中的抽象版本,子类中的具体版本)。因为为每个 Leaf 使用一个新实例是浪费的,所以我们欺骗了重用静态字段(使用匿名类初始化)(我们再次欺骗了泛型类型,因为 Java 没有“底层”)就像 Scala 的 Nothing)。当然,这只是一个非常粗略的草图,没有错误处理等。是的,它变得非常冗长,所以如果你可以使用 Scala 来代替,那就很高兴了......
OK, time for the shock therapy: Java. Your type BTree becomes the top of the class hierarchy. No typeclasses, you overwrite the equals and toString methods instead (no equivalent for Read, though). Then you put all functions inside the objects, as methods (often an abstract version in BTree, and concrete versions in the sub-classes). As it would be wasteful to use a new instance for every Leaf, we cheat an reuse a static field (wich is initialized using an anonymous class) instead (where we cheat again by leaving off the generic type, because Java has no "bottom" like Scala's Nothing). Of course this is only a very crude sketch without error handling etc. And yes, it gets really verbose, so be happy if you can use Scala instead ...
我不懂Scala,但我懂Java。
在Java中,对象通常会对特定的事物进行建模,例如:汽车或B树等。
对象将存储有关该事物的数据(信息)。对象还将具有可以针对数据完成的行为(例如:打开车门),这通常会改变事物的状态(更改数据)。行为(方法)也可能只是告诉我们有关对象状态的信息,但不会更改状态。此外,内部数据的确切形式通常是隐藏的(通过良好的实践)。
现在,在任何时间点,对象都将具有精确状态。
因此,如果我们考虑一个二叉树对象。我们可能有一个看起来完全像这样的二叉树(包含整数):
因此在任何时间点,它都会有一定数量的具有特定值的节点,并以特定方式连接。
现在我们需要决定如何存储有关二叉树的信息。这将在每个二叉树对象内部完成。
所以我们知道每个二叉树将由一定数量的节点组成。
因此,我们需要某种方法来存储有关节点的信息。现在,我们需要存储节点所持有的值以及它们拥有的左/右子节点。因为它们包含不止一条信息,所以我们需要将它们存储为对象。
因此,每个节点对象都需要有一个值变量,一个变量告诉我们它的左子节点是什么(如果有),一个变量告诉我们它的右子节点。
因此,对于包含整数值的节点,我们可以这样:
现在并非所有节点都有左或右子节点(或任何子节点)。缺少左子节点由值为“null”的左变量表示,而不是引用实际节点。
上面的代码表示一般的节点,不包含特定的状态信息,但是我们创建的特定节点将具有“value”、“left”和“right”变量的特定值。
现在我们知道二叉树是由许多节点组成的。它从根节点开始。
然后根节点将只包含有关其下面的节点(其子节点)的信息。
但我们还想赋予二叉树(即对象)一些行为,以便我们可以用它们做一些有趣的事情。毕竟,为什么我们甚至想要一棵二叉树——这样我们就可以用它做一些有用的事情!
首先,我们需要一个“构造函数”,以便我们可以创建一个二叉树对象,并将其状态设置为一些初始值。所以我们会让我们的二叉树开始为空。我们通过将“root”变量设为 null 来表示这一点。这仅仅意味着我们甚至没有根!所以它是空的。我们以与方法(属于类/对象的函数)相同的形式编写构造函数,只是我们给它赋予与类本身相同的名称:
我们可能想给我们的二叉树对象一些行为/方法,所以我们实际上可以构建我们想要的任何类型的二叉树。只能制作空树而不改变它们可能没有用!
因此,我们可以创建 addLeftChild(Node addFrom, int value) 和 addRightChild(Node addFrom, int value) 方法。 addLeftChild 将创建一个具有给定值(并且没有子节点)的新节点,并使其成为 addFrom 给定节点的左子节点,但前提是“addFrom”节点还没有左子节点。
我们可能还有一个添加新根节点的方法,addRoot(int value),因此我们可以在第一次创建二叉树时添加根节点。
您可能还会有删除节点的方法(行为)。您可能有方法在树中搜索值/节点,或者为您提供有关树的信息(例如:深度、节点数)。
然后我们可能会编写一些代码来实际创建二叉树对象,以某种方式与它们交互,例如:
这将为我们提供这个称为 ourBT(只是根节点)的二叉树,
然后我们可能会:
这将留下我们的处于这种状态的二叉树:
然后我们可以:
这将使我们的二叉树处于这种状态:
然后在构建我们的二叉树之后,我们也许可以用它做一些其他有趣的事情(例如:搜索,删除)
这可能不是'这是最好的例子,但希望我能让您深入了解如何以 OO 风格编写自定义数据结构。
I don't know Scala, but I know Java.
In Java, and object, will often model a specific thing, eg: a car, or a B-tree etc.
An object will store data (information) about this thing. An object will also have behaviour that can be done with respect to the data (eg: open the car's door) which will often change the state of the thing (change the data). The behaviour (method) might also just tells us information about the object's state and not change the state though. Also, the exact form of the internal data is usually hidden (by good practice).
Now at any point in time an object will have an exact state.
So if we think about a binary tree object. we might have a binary tree (containing integers) that looks exactly like this:
So at any point in time it's going to have a certain number of nodes with certain values, attached in certain ways.
Now we need to decide how to store information about our binary tree. This will be done internally in each binary tree object.
So we know each binary tree is going to be made up some number of nodes.
So we'll need some way to store information about nodes. Now nodes we need to store what value they hold as well as what left/right children they have. Because they contain more than one piece of information we'll need to store these as objects.
So each node object will need to have variable for the value, a variable which tells us what its left child is (if any), and one for it's right child.
So for a node containing integer values we could go:
Now not all nodes will have a left or right child (or any children at all). A lack of left child is represented by the left variable having value 'null' rather than referring to an actual node.
The above code represents a node in general without containing specific state information, but a particular node that we've created will have a particular values for the 'value', 'left' and 'right' variables.
Now we know a binary tree is made up of a number of nodes. It starts with a root node.
And then the root node will just contain the information about what nodes are below it (its children).
But we'll also want to give our binary tree's (ie. the objects) some behaviour so we can do some interesting things with them. After all, why do we even want a binary tree anyway -- so we can do something useful with it!
First we need a "constructor" so we can make a binary tree object, and set it's state to some initial values. So we'll just make our binary trees start off being empty. We represent this by having 'root' variable null. This just means we don't even have a root! So it's empty. We write a constructor in the same form as a method (a function which belongs to a class/object) except we give it the same name as the class itself:
We'll probably want to give our binary tree objects some behaviour/methods so that we can actually build up whatever kind of binary tree that we want. Only being able to make empty trees and not change them probably won't be useful!
So we might make an addLeftChild(Node addFrom, int value), and addRightChild(Node addFrom, int value), methods. The addLeftChild will make a new node having the given value (and no children), and make it the left child of the node given by addFrom, but only if the 'addFrom' node doesn't already have a left child.
We might also have a method to add a new root node, addRoot(int value), so we can add a root node when we first make a binary tree.
You would probably also have methods (behaviour) to remove nodes. You might have methods to search the tree for values/nodes, or to give you information about the tree (eg: depth, number of nodes).
Then we might write some code to actually make binary tree objects, interact with them in some way, eg:
this would give us this so far as this binary tree called ourBT (just a root node)
then we might go:
which would leave our binary tree in this state:
Then we could go:
which would leave our binary tree in this state:
Then after building up our binary tree we could maybe then do some other interesting stuff with it (eg: searching, removing)
This probably isn't the best example, but hopefully I've given you a bit of insight into how custom data structures are written in OO style.