Проблемы при переходе с функционала на ОО - PullRequest
14 голосов
/ 03 декабря 2010

Я привык работать с функциональным программированием (в основном, с 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-х поддеревьев.Я понятия не имею.Я просто не могу понять, как я могу сделать это в здравом уме.

Ответы [ 6 ]

18 голосов
/ 04 декабря 2010

Некоторые хорошие ответы здесь, но я думаю, что они все упускают возможность показать, что вы упускаете.Итак, вы показали это:

data BTree a = 
    Leaf
    |Node2 (BTree a) a (BTree a)
    |Node3 (BTree a) a (BTree a) a (BTree a)
    deriving (Eq,Read,Show)

И спросили, как вы реализуете это объектно-ориентированным способом.Итак, вот оно:

Самая важная вещь

trait Tree[A] {
  // not required because it is inherited from AnyRef
  // def equals(other: Any): Boolean

  // not required because it is inherited from AnyRef
  // def toString: String

  // does not belong in the object
  // def fromString(input: String): Tree[A]

  // Stuff that is missing but is needed
  def isEmpty: Boolean
  def value: Option[A]
  def subtrees: Seq[Tree[A]]
  def iterator: Iterator[A]
  def depthFirstIterator: Iterator[A]
  def breadthFirstIterator: Iterator[A]
}

Итак, вот в чем дело: когда вы говорите об ориентации объекта, у вас есть BTree,дерево пальца или любая другая древовидная структура не имеет значения .На самом деле он должен быть скрыт .Что важно, так это то, что вы можете сделать с ним.

У вас проблемы с этим, потому что вы подходите к проблеме именно в том направлении, в котором вы не должны. Не столь важная вещь

sealed abstract class BTree[A] extends Tree[A]
object BTree {
  def apply[A](input: String): BTree[A] = { /* factory */ null.asInstanceOf[BTree[A]] }
  private case object Leaf extends BTree[Nothing] {
    // method implementation
  }
  private case class Node2[A](value: A, private one: BTree[A], private two: BTree[A]) extends BTree[A] {
    // method implementation
  }
  private case class Node3[A](value: A, private one: BTree[A], private two: BTree[A], private three: BTree[A]) extends BTree[A] {
    // method implementation
  }
}

Теперь здесь вы фактически предлагаете реализацию, но детали BTree полностью скрыты.Вы можете использовать только методы, определенные Tree.

Это идеальная объектно-ориентированная архитектура: клиенты зависят от интерфейсов, структура данных скрыта.

17 голосов
/ 03 декабря 2010

Вот первый шаг для перехода к ОО из функционального мышления: Объекты больше похожи на функции, чем на данные. Думайте о них как о таковых;вместо функций, действующих на прозрачные структурированные данные, теперь у вас есть непрозрачные глобусы абстрактного поведения.

Думая об этом в терминах «хорошо, вот структура моих данных, теперь я ...», говоря об этом в обратном направлении.

Попробуйте что-то вроде этого:

  • Начните с выяснения основных действий, которые можно выполнить с вашим B-деревом (не забывайте о таких вещах, как show и fmap здесь), и разработайте класс, основанный на них.

  • Для типа суммы, такого как ваше дерево, может быть проще оставить базовый класс пустым и использовать подклассы для различных вариантов конструкторов данных.Как правило, в ОО, где бы вам ни приходилось делать какой-то выбор, который радикально меняет последующее поведение, настоятельно рекомендуется использовать полиморфизм подтипа для различения случаев.

  • Постарайтесь не беспокоиться овнутреннее представление, пока вы не должны, и не позволяйте деталям представления вытекать из класса.Наличие множества методов GetFoo (), которые возвращают примитивные типы, является признаком Doing It Wrong.

И наконец: помните, что вы используете Scala.Это гибридный язык по причине;Не все имеет смысл делать в ОО стиле.Пролистайте книгу «Шаблоны проектирования», и вы обнаружите, что половина из них посвящена обходным маневрам в стиле барокко для отсутствующих языковых функций.

12 голосов
/ 03 декабря 2010

Поскольку у вас есть Scala в вашем списке тегов, вот как это будет сделано в Scala:

У вас есть базовая черта (в типе Haskell), и из нее получены все конструкторы Haskell какcase классы.Таким образом, вы также можете использовать их при сопоставлении с шаблоном Scala.

sealed trait Tree[+A]

case object Leaf extends Tree[Any]
case class Node2[+A](a: A,  t1: Tree[A], t2: Tree[A]) extends Tree[A]
case class Node3[+A](a: A, b: A,  t1: Tree[A], t2: Tree[A], t2: Tree[A]) extends Tree[A]

В таких языках, как Java (начиная с 1.5), C ++ и C #, у вас есть один и тот же тип шаблонов для обеспечения безопасности типов.Они в основном работают как переменные типа в Haskell.

Этот пример в Scala, но для других ОО-языков вы бы сделали это аналогичным образом: создайте абстрактный базовый класс и превратите конструкторы ваших данных в классы/objects.

3 голосов
/ 04 декабря 2010

ОК, время для шоковой терапии: Ява. Ваш тип BTree становится вершиной иерархии классов. Нет классов типов, вместо этого вы перезаписываете методы equals и toString (хотя для Read нет эквивалента). Затем вы помещаете все функции внутри объектов как методы (часто абстрактная версия в BTree и конкретные версии в подклассах). Поскольку было бы расточительно использовать новый экземпляр для каждого Листа, вместо этого мы обманываем и повторно используем статическое поле (которое инициализируется с помощью анонимного класса) (где мы снова читируем, исключая универсальный тип, потому что у Java нет «дна» как у Скалы Ничто). Конечно, это только очень грубый набросок без обработки ошибок и т. Д. И да, он становится действительно многословным, поэтому будьте счастливы, если вместо этого вы сможете использовать Scala ...

public abstract class BTree<A> {
   public static final BTree LEAF = new BTree {
      //concrete implementations of the methods 
      public boolean isEmpty(){ return true; } 
      public String toString() { return ""; }
   }
   public abstract boolean isEmpty(); 
   //concrete methods that are the same for all sub-classes
   //abstract methods that are different
}

public class Node2<A> {
   private BTree<A> left; 
   private BTree<A> right; 
   private A a;

   public Node2(BTree<A> left, A a, BTree<A> right) {
      this.left = left;
      this.a = a;
      this.right = right;  
   }

   public String toString() {
      return "(" + left +  a + right +  ")";
   }

   public boolean equals(Object o) {
      if (o instanceof Node2) {
          Node2<A> n2 = (Node2<A>) n2;
          return a.equals(n2.a) && left.equals(n2.left) && right.equals(n2.right);
      }
      return false;    
   }

   public boolean isEmpty(){ return false; } 
   //other concrete methods
}  

//same for Node3
3 голосов
/ 03 декабря 2010

Определить «вменяемый». На самом деле, это круто: я впервые увидел, что у кого-то возникают проблемы с переходом от функционального к ОО, а не наоборот.

Правда в том, что у тебя будет больше вещей, чтобы сделать это ОО; это одна из причин функциональности это хорошо. Вы попали в конкретный случай, в котором функционал имеет свои преимущества.

В языке OO (это не какой-то конкретный язык, просто псевдокод) вам понадобится класс для узла

class Node
  children : array of Node;
end

и затем у вас есть методы, например, добавить узел в качестве дочернего, чтобы вы могли что-то с ним делать.

Затем вы создаете класс BTree, используя Node для вставки, балансировки и т. Д.

1 голос
/ 11 декабря 2010

Я не знаю Scala, но я знаю Java.

В Java и объектах часто моделируется конкретная вещь, например: автомобиль или B-дерево и т. Д.

Объект будет хранить данные (информацию) об этой вещи.Объект также будет иметь поведение, которое может быть выполнено в отношении данных (например, открыть дверь автомобиля), что часто меняет состояние вещи (изменяет данные).Поведение (метод) может также просто сообщать нам информацию о состоянии объекта и не изменять его состояние.Кроме того, точная форма внутренних данных обычно скрыта (по хорошей практике).

Теперь в любой момент времени объект будет иметь точное состояние.

Так что, если мы думаем о бинарном объекте дерева.у нас может быть двоичное дерево (содержащее целые числа), которое выглядит точно так:

      4
    /   \
   2     1
 /      / \
1     3    1

Так что в любой момент времени в нем будет определенное количество узлов с определенными значениями, прикрепленными определенным образом.

Теперь нам нужно решить, как хранить информацию о нашем двоичном дереве.Это будет сделано внутри каждого двоичного дерева.

Итак, мы знаем, что каждое двоичное дерево будет состоять из некоторого числа узлов.

Итак, нам понадобится какой-то способ хранения информации.об узлах.Теперь узлам нужно хранить то, что они содержат, а также то, какие у них левые / правые потомки.Поскольку они содержат более одной части информации, нам нужно хранить их как объекты.

Таким образом, каждый объект узла должен иметь переменную для значения, переменную, которая сообщает нам, каков его левый потомок (если есть), и одну для его правого потомка.

Так что дляузел, содержащий целочисленные значения, мы могли бы перейти:

class Node {
  int value;
  Node left;
  Node right;

}

Теперь не у всех узлов будет левый или правый дочерний элемент (или вообще никаких дочерних элементов).Отсутствие левого потомка представлено левой переменной, имеющей значение 'null', а не ссылающейся на фактический узел.

Приведенный выше код представляет узел в целом без конкретной информации о состоянии, но конкретный узел, который мыСозданный файл будет иметь определенные значения для переменных «value», «left» и «right».

Теперь мы знаем, что двоичное дерево состоит из нескольких узлов.Начинается с корневого узла.И тогда корневой узел будет просто содержать информацию о том, какие узлы находятся под ним (его дочерние элементы).

class BinaryTree {
    Node root;


}

Но мы также хотим дать нашему бинарному дереву (то есть объектам) некоторое поведение, чтобы мы могли делать с ним некоторые интересные вещи.В конце концов, зачем нам вообще нужно двоичное дерево - так что мы можем сделать с ним что-то полезное!

Сначала нам нужен «конструктор», чтобы мы могли создать объект двоичного дерева и установить его состояние внекоторые начальные значения.Поэтому мы просто сделаем наши двоичные деревья пустыми.Мы представляем это, имея корневую переменную null.Это просто означает, что у нас даже нет рута!Так что пусто.Мы пишем конструктор в той же форме, что и метод (функция, которая принадлежит классу / объекту), за исключением того, что мы даем ему то же имя, что и сам класс:

class BinaryTree {
    Node root;

    BinaryTree() {
       root = null; // make it so that newly made objects start off being empty
   }
}

Мы, вероятно, захотим датьнаше двоичное дерево возбуждает некоторое поведение / методы, так что мы на самом деле можем создать любой вид двоичного дерева, которое мы хотим.Только способность создавать пустые деревья и не менять их, вероятно, не будет полезна!

Таким образом, мы можем создать методы addLeftChild (Node addFrom, int value) и addRightChild (Node addFrom, int value).AddLeftChild создаст новый узел с заданным значением (и без дочерних элементов) и сделает его левым дочерним узлом, заданным addFrom, но только в том случае, если у узла addFrom еще нет левого дочернего элемента.

class BinaryTree {
    Node root;

    BinaryTree() {
       root = null; // make it so that newly made objects start off being empty
   }

   Node addLeftChild(Node addFrom, int value) {
            // change the binary tree's state somehow to achieve this
   }

   Node addRightChild(Node addFrom, int value) {
            // change the binary tree's state somehow to achieve this
   }

}

У нас также может быть метод для добавления нового корневого узла, addRoot (int value), чтобы мы могли добавить корневой узел при первом создании двоичного дерева.Возможно, у вас также есть методы (поведение) для удаления узлов.У вас могут быть методы поиска в дереве значений / узлов или предоставления вам информации о дереве (например, глубина, количество узлов).

Тогда мы могли бы написать некоторый код, чтобы фактически создавать объекты двоичного дерева, взаимодействовать с ними каким-либо образом, например:

// this is some main method,etc
BinaryTree ourBT = new BinaryTree(); // make an new binary tree
                                    // remember these start off empty

 Node rootNode; // variable so we can tell 
                // later add methods which node to add from

 rootNode =  ourBT.addRoot(4);

, это дало бы нам это, поскольку это двоичное дерево, называемое ourBT (простокорневой узел)

           4

тогда мы можем пойти:

ourBT.addLeftChild(rootNode, 3); // remember the parameter rootNode refers 
                                 // to the root node we just added before

, который оставил бы наше двоичное дерево в этом состоянии:

           4
         /
        3

Тогда мы могли бы пойти:

   ourBT.addRightChild(rootNode, 1); 

, который оставил бы наше двоичное дерево в этом состоянии:

           4
         /  \
        3    1

Затем, после создания нашего двоичного дерева, мы могли бы, возможно, сделать с ним другие интересные вещи (например: поиск, удаление)

Вероятно, это не лучший пример, но, надеюсь, я дал вам немного понимания того, как пользовательские структуры данных пишутся в ОО-стиле.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...